@narumitw/pi-sync 0.35.0 → 0.38.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 +14 -2
- package/package.json +2 -1
- package/src/file-selection.ts +88 -73
- package/src/manager-state.ts +155 -0
- package/src/manager-ui.ts +230 -289
- package/src/settings-ui.ts +103 -145
- package/src/storage-connections-ui.ts +157 -83
- package/src/sync-setups-ui.ts +186 -99
package/src/manager-ui.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BorderedLoader, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ActionMenuItem, defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
3
|
import { setSyncSetupCompletions } from "./command.js";
|
|
3
4
|
import {
|
|
4
|
-
activeLocalConfigPath,
|
|
5
5
|
configuredSyncSetupNames,
|
|
6
6
|
isCloudflareR2Endpoint,
|
|
7
7
|
loadConfig,
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
loadPartialConfig,
|
|
10
10
|
localConfigPath,
|
|
11
11
|
readLocalConfigObject,
|
|
12
|
-
readStateForConfig,
|
|
13
12
|
syncConfigReviewIdentity,
|
|
14
13
|
} from "./config.js";
|
|
15
14
|
import { showAddGitTarget, showEditGitTarget, showGitSetup } from "./git-ui.js";
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
requiredInput,
|
|
22
21
|
safeTerminalText,
|
|
23
22
|
} from "./manager-helpers.js";
|
|
23
|
+
import { backendStorageDescription, describeManagerState } from "./manager-state.js";
|
|
24
24
|
import { chooseS3Credentials } from "./s3-credentials-ui.js";
|
|
25
25
|
import {
|
|
26
26
|
addSyncSetup,
|
|
@@ -32,29 +32,10 @@ import { showSyncSettings } from "./settings-ui.js";
|
|
|
32
32
|
import { type SetupPullOutcome, useSyncSetup } from "./setup-switch.js";
|
|
33
33
|
import { showAddStorageConnection, showStorageConnections } from "./storage-connections-ui.js";
|
|
34
34
|
import { DEFAULT_SYNC_INCLUDE, syncIncludeSelection } from "./sync-policy.js";
|
|
35
|
-
import {
|
|
35
|
+
import { showSyncSetups } from "./sync-setups-ui.js";
|
|
36
36
|
import type { AnySyncConfig } from "./types.js";
|
|
37
37
|
import { showAddWebDavTarget, showEditWebDavTarget, showWebDavSetup } from "./webdav-ui.js";
|
|
38
38
|
|
|
39
|
-
export const MAIN_MENU_ACTIONS = [
|
|
40
|
-
"Sync now (recommended)",
|
|
41
|
-
"Switch sync setup",
|
|
42
|
-
"Status & changes",
|
|
43
|
-
"Settings",
|
|
44
|
-
"More…",
|
|
45
|
-
] as const;
|
|
46
|
-
const MORE_MENU_ACTIONS = [
|
|
47
|
-
"Pull from remote…",
|
|
48
|
-
"Push to remote…",
|
|
49
|
-
"Sync setups…",
|
|
50
|
-
"Storage connections…",
|
|
51
|
-
"History & recovery…",
|
|
52
|
-
"Help",
|
|
53
|
-
"Back",
|
|
54
|
-
] as const;
|
|
55
|
-
const BACK = "Back";
|
|
56
|
-
|
|
57
|
-
type MainMenuAction = (typeof MAIN_MENU_ACTIONS)[number];
|
|
58
39
|
type RunRoute = (
|
|
59
40
|
route: string,
|
|
60
41
|
signal?: AbortSignal,
|
|
@@ -71,97 +52,178 @@ export async function showSyncManager(
|
|
|
71
52
|
await runRoute("help");
|
|
72
53
|
return;
|
|
73
54
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
55
|
+
type Screen = "main" | "more" | "recovery";
|
|
56
|
+
type Action =
|
|
57
|
+
| "sync"
|
|
58
|
+
| "switch"
|
|
59
|
+
| "diff"
|
|
60
|
+
| "settings"
|
|
61
|
+
| "pull"
|
|
62
|
+
| "push"
|
|
63
|
+
| "setups"
|
|
64
|
+
| "connections"
|
|
65
|
+
| "history"
|
|
66
|
+
| "doctor"
|
|
67
|
+
| "unlock"
|
|
68
|
+
| "help"
|
|
69
|
+
| "init"
|
|
70
|
+
| "back";
|
|
71
|
+
interface State {
|
|
72
|
+
manager: Awaited<ReturnType<typeof describeManagerState>>;
|
|
73
|
+
canRecover: boolean;
|
|
74
|
+
}
|
|
75
|
+
const menu = defineMenu<State, Screen, Action, ExtensionCommandContext>({
|
|
76
|
+
start: "main",
|
|
77
|
+
screens: {
|
|
78
|
+
main: ({ state }) => ({
|
|
79
|
+
kind: "actions",
|
|
80
|
+
title: "Manage sync",
|
|
81
|
+
lines: state.manager.title.split("\n").slice(1),
|
|
82
|
+
items: state.manager.actions.map(syncMainMenuItem),
|
|
83
|
+
hint: "close",
|
|
84
|
+
}),
|
|
85
|
+
more: () => ({
|
|
86
|
+
kind: "actions",
|
|
87
|
+
title: "More options",
|
|
88
|
+
items: [
|
|
89
|
+
{ id: "pull", label: "Pull from remote…", action: "pull" },
|
|
90
|
+
{ id: "push", label: "Push to remote…", action: "push" },
|
|
91
|
+
{ id: "setups", label: "Sync setups…", action: "setups" },
|
|
92
|
+
{
|
|
93
|
+
id: "connections",
|
|
94
|
+
label: "Storage connections…",
|
|
95
|
+
action: "connections",
|
|
96
|
+
},
|
|
97
|
+
{ id: "recovery", label: "History & recovery…", to: "recovery" },
|
|
98
|
+
{ id: "help", label: "Help", action: "help" },
|
|
99
|
+
{ id: "back", label: "Back", action: "back" },
|
|
100
|
+
],
|
|
101
|
+
hint: "back",
|
|
102
|
+
}),
|
|
103
|
+
recovery: ({ state }) => ({
|
|
104
|
+
kind: "actions",
|
|
105
|
+
title: "History & recovery",
|
|
106
|
+
items: [
|
|
107
|
+
{ id: "history", label: "Browse history", action: "history" },
|
|
108
|
+
{ id: "doctor", label: "Check setup", action: "doctor" },
|
|
109
|
+
...(state.canRecover
|
|
110
|
+
? [{ id: "unlock", label: "Recover stale operation", action: "unlock" as const }]
|
|
111
|
+
: []),
|
|
112
|
+
{ id: "back", label: "Back", action: "back" },
|
|
113
|
+
],
|
|
114
|
+
hint: "back",
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
actions: {
|
|
118
|
+
sync: async () => {
|
|
91
119
|
await runCancellableOperation(ctx, "Checking current sync setup…", "sync", runRoute, true);
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
return { kind: "stay" };
|
|
121
|
+
},
|
|
122
|
+
switch: async () => {
|
|
94
123
|
const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
case "Status & changes":
|
|
124
|
+
return result === "pull-attempted" ? { kind: "close" } : { kind: "stay" };
|
|
125
|
+
},
|
|
126
|
+
diff: async () => {
|
|
100
127
|
await runCancellableOperation(ctx, "Checking current sync setup…", "diff", runRoute);
|
|
101
|
-
|
|
102
|
-
|
|
128
|
+
return { kind: "stay" };
|
|
129
|
+
},
|
|
130
|
+
settings: async () => {
|
|
103
131
|
await showSyncSettings(ctx, runRoute, sessionSignal);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
return { kind: "stay" };
|
|
133
|
+
},
|
|
134
|
+
pull: async () => {
|
|
135
|
+
const result = await runCancellableOperation(
|
|
136
|
+
ctx,
|
|
137
|
+
"Checking remote changes…",
|
|
138
|
+
"pull",
|
|
139
|
+
runRoute,
|
|
140
|
+
true,
|
|
141
|
+
"Pull check cancelled; no local files were changed.",
|
|
142
|
+
);
|
|
143
|
+
return result === "applied" ? { kind: "close" } : { kind: "stay" };
|
|
144
|
+
},
|
|
145
|
+
push: async () => {
|
|
146
|
+
await runCancellableOperation(
|
|
147
|
+
ctx,
|
|
148
|
+
"Preparing push preview…",
|
|
149
|
+
"push",
|
|
150
|
+
runRoute,
|
|
151
|
+
true,
|
|
152
|
+
"Push preparation cancelled; no remote files were changed.",
|
|
153
|
+
);
|
|
154
|
+
return { kind: "stay" };
|
|
155
|
+
},
|
|
156
|
+
setups: async () => {
|
|
157
|
+
const result = await showSyncSetupManager(ctx, runRoute, sessionSignal);
|
|
158
|
+
return result === "exit" ? { kind: "close" } : { kind: "stay" };
|
|
159
|
+
},
|
|
160
|
+
connections: async () => {
|
|
112
161
|
await showStorageConnections(ctx, sessionSignal);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
return { kind: "stay" };
|
|
163
|
+
},
|
|
164
|
+
history: async () => {
|
|
165
|
+
await runRoute("history");
|
|
166
|
+
return { kind: "stay" };
|
|
167
|
+
},
|
|
168
|
+
doctor: async () => {
|
|
169
|
+
await runRoute("doctor");
|
|
170
|
+
return { kind: "stay" };
|
|
171
|
+
},
|
|
172
|
+
unlock: async () => {
|
|
173
|
+
await runRoute("unlock --stale");
|
|
174
|
+
return { kind: "stay" };
|
|
175
|
+
},
|
|
176
|
+
help: async () => {
|
|
118
177
|
await runRoute("help");
|
|
119
|
-
return;
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
return { kind: "close" };
|
|
179
|
+
},
|
|
180
|
+
init: async () => {
|
|
122
181
|
await runRoute("init");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
182
|
+
return { kind: "stay" };
|
|
183
|
+
},
|
|
184
|
+
back: async () => ({ kind: "back" }),
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
await runMenu(ctx, menu, {
|
|
188
|
+
getState: async () => {
|
|
189
|
+
const [manager, lock] = await Promise.all([
|
|
190
|
+
describeManagerState(sessionSignal),
|
|
191
|
+
inspectLock(),
|
|
192
|
+
]);
|
|
193
|
+
return {
|
|
194
|
+
manager,
|
|
195
|
+
canRecover:
|
|
196
|
+
lock.status === "unreadable" || (lock.status === "valid" && isStaleLock(lock.lock)),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
signal: sessionSignal,
|
|
200
|
+
isCurrent: () => !sessionSignal?.aborted,
|
|
201
|
+
});
|
|
126
202
|
}
|
|
127
203
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"push",
|
|
152
|
-
runRoute,
|
|
153
|
-
true,
|
|
154
|
-
"Push preparation cancelled; no remote files were changed.",
|
|
155
|
-
);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (selected === "Sync setups…") return showSyncSetupManager(ctx, runRoute, signal);
|
|
159
|
-
else if (selected === "Storage connections…") await showStorageConnections(ctx, signal);
|
|
160
|
-
else if (selected === "History & recovery…") await showRecoveryMenu(ctx, runRoute);
|
|
161
|
-
else {
|
|
162
|
-
await runRoute("help");
|
|
163
|
-
return "exit" as const;
|
|
164
|
-
}
|
|
204
|
+
function syncMainMenuItem(
|
|
205
|
+
label: string,
|
|
206
|
+
): ActionMenuItem<
|
|
207
|
+
"main" | "more" | "recovery",
|
|
208
|
+
"sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "help" | "init"
|
|
209
|
+
> {
|
|
210
|
+
if (label === "More…") return { id: "more", label, to: "more" };
|
|
211
|
+
if (label === "History & recovery…") return { id: "recovery", label, to: "recovery" };
|
|
212
|
+
const actions = new Map<
|
|
213
|
+
string,
|
|
214
|
+
"sync" | "switch" | "diff" | "settings" | "setups" | "connections" | "help" | "init"
|
|
215
|
+
>([
|
|
216
|
+
["Sync now (recommended)", "sync"],
|
|
217
|
+
["Switch sync setup", "switch"],
|
|
218
|
+
["Status & changes", "diff"],
|
|
219
|
+
["Settings", "settings"],
|
|
220
|
+
["Sync setups…", "setups"],
|
|
221
|
+
["Storage connections…", "connections"],
|
|
222
|
+
["Help", "help"],
|
|
223
|
+
["Set up sync", "init"],
|
|
224
|
+
["Use existing settings", "init"],
|
|
225
|
+
]);
|
|
226
|
+
return { id: actions.get(label) ?? "help", label, action: actions.get(label) ?? "help" };
|
|
165
227
|
}
|
|
166
228
|
|
|
167
229
|
async function runCancellableOperation(
|
|
@@ -219,146 +281,6 @@ async function runCancellableOperation(
|
|
|
219
281
|
return routeResult;
|
|
220
282
|
}
|
|
221
283
|
|
|
222
|
-
async function describeManagerState(
|
|
223
|
-
signal?: AbortSignal,
|
|
224
|
-
): Promise<{ title: string; actions: string[] }> {
|
|
225
|
-
let raw: Record<string, unknown> | undefined;
|
|
226
|
-
try {
|
|
227
|
-
raw = await readLocalConfigObject();
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return {
|
|
230
|
-
title: [
|
|
231
|
-
"Manage sync",
|
|
232
|
-
"",
|
|
233
|
-
"Settings file needs repair. Automatic sync and settings writes are paused.",
|
|
234
|
-
`Error: ${safeTerminalText(errorMessage(error))}`,
|
|
235
|
-
`File: ${safeTerminalText(await activeLocalConfigPath())}`,
|
|
236
|
-
"",
|
|
237
|
-
"Repair the JSON file, then reopen /sync.",
|
|
238
|
-
].join("\n"),
|
|
239
|
-
actions: ["Help"],
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
if (!raw) {
|
|
243
|
-
return {
|
|
244
|
-
title: ["Manage sync", "", "Not set up.", "", "What do you want to do?"].join("\n"),
|
|
245
|
-
actions: ["Set up sync", "Help"],
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
const configuredTargets = ownRecord(raw.syncSetups);
|
|
249
|
-
if (raw.version === 3 && configuredTargets && Object.keys(configuredTargets).length === 0) {
|
|
250
|
-
return {
|
|
251
|
-
title: [
|
|
252
|
-
"Manage sync",
|
|
253
|
-
"",
|
|
254
|
-
"No sync setups are configured.",
|
|
255
|
-
"Add a sync setup using an existing storage connection.",
|
|
256
|
-
"",
|
|
257
|
-
"What do you want to do?",
|
|
258
|
-
].join("\n"),
|
|
259
|
-
actions: ["Sync setups…", "Storage connections…", "Help"],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
const config = await loadConfig();
|
|
264
|
-
const target = config.setupName;
|
|
265
|
-
const storage = backendStorageDescription(config);
|
|
266
|
-
const warnings: string[] = [];
|
|
267
|
-
const lock = await inspectLock();
|
|
268
|
-
const liveLock = lock.status === "valid" && !isStaleLock(lock.lock);
|
|
269
|
-
const recoverableLock =
|
|
270
|
-
lock.status === "unreadable" || (lock.status === "valid" && isStaleLock(lock.lock));
|
|
271
|
-
const selection = syncIncludeSelection(config.include);
|
|
272
|
-
const builtInCount = selection.builtIns.length;
|
|
273
|
-
const extraFileCount = selection.custom.length;
|
|
274
|
-
const noSyncedContent = config.include.length === 0;
|
|
275
|
-
const stateReadDisabled = liveLock || recoverableLock;
|
|
276
|
-
const syncState = stateReadDisabled
|
|
277
|
-
? undefined
|
|
278
|
-
: await readStateForConfig(config).catch(() => undefined);
|
|
279
|
-
const lastAppliedSnapshot = stateReadDisabled
|
|
280
|
-
? "Unavailable while operations are locked"
|
|
281
|
-
: syncState?.lastAppliedSnapshot
|
|
282
|
-
? safeTerminalText(syncState.lastAppliedSnapshot)
|
|
283
|
-
: syncState
|
|
284
|
-
? "Never synced"
|
|
285
|
-
: "Unavailable";
|
|
286
|
-
const canSwitch = (await countValidSyncSetups(configuredTargets, signal)) > 1;
|
|
287
|
-
const mainActions = MAIN_MENU_ACTIONS.filter(
|
|
288
|
-
(action) => action !== "Switch sync setup" || canSwitch,
|
|
289
|
-
);
|
|
290
|
-
return {
|
|
291
|
-
title: [
|
|
292
|
-
"Manage sync",
|
|
293
|
-
"",
|
|
294
|
-
`Current sync setup: ${safeTerminalText(target)}`,
|
|
295
|
-
`Storage: ${storage}`,
|
|
296
|
-
`Included: ${builtInCount} built-in group${builtInCount === 1 ? "" : "s"} · ${extraFileCount} extra path${extraFileCount === 1 ? "" : "s"} · Sessions ${selection.sessions ? "on" : "off"}`,
|
|
297
|
-
`Automatic sync: ${config.automatic ? "On" : "Off"}`,
|
|
298
|
-
`Last applied: ${lastAppliedSnapshot}`,
|
|
299
|
-
"Remote status: Not checked",
|
|
300
|
-
...warnings.map((warning) => `Warning: ${safeTerminalText(warning)}`),
|
|
301
|
-
...(noSyncedContent
|
|
302
|
-
? [
|
|
303
|
-
"",
|
|
304
|
-
"No included content is selected. Choose included content in Settings before syncing.",
|
|
305
|
-
]
|
|
306
|
-
: []),
|
|
307
|
-
...(liveLock
|
|
308
|
-
? [
|
|
309
|
-
"",
|
|
310
|
-
`Operation in progress: ${safeTerminalText(lock.lock.command)} (pid ${lock.lock.pid}). Sync and settings changes are disabled.`,
|
|
311
|
-
]
|
|
312
|
-
: []),
|
|
313
|
-
...(recoverableLock
|
|
314
|
-
? ["", "Recovery required: lock metadata is stale or unreadable."]
|
|
315
|
-
: []),
|
|
316
|
-
"",
|
|
317
|
-
"What do you want to do?",
|
|
318
|
-
].join("\n"),
|
|
319
|
-
actions:
|
|
320
|
-
liveLock || recoverableLock
|
|
321
|
-
? ["Status & changes", "History & recovery…", "Help"]
|
|
322
|
-
: noSyncedContent
|
|
323
|
-
? ["Settings", ...(canSwitch ? ["Switch sync setup"] : []), "Status & changes", "More…"]
|
|
324
|
-
: mainActions,
|
|
325
|
-
};
|
|
326
|
-
} catch (error) {
|
|
327
|
-
if (signal?.aborted) throw error;
|
|
328
|
-
return {
|
|
329
|
-
title: [
|
|
330
|
-
"Manage sync",
|
|
331
|
-
"",
|
|
332
|
-
"Settings need attention. Automatic sync is paused.",
|
|
333
|
-
`Current sync setup: ${safeTerminalText(typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : "none")}`,
|
|
334
|
-
`Error: ${safeTerminalText(errorMessage(error))}`,
|
|
335
|
-
`File: ${safeTerminalText(await activeLocalConfigPath())}`,
|
|
336
|
-
"",
|
|
337
|
-
"What do you want to do?",
|
|
338
|
-
].join("\n"),
|
|
339
|
-
actions: ["Sync setups…", "Storage connections…", "History & recovery…", "Help"],
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function backendStorageDescription(config: AnySyncConfig) {
|
|
345
|
-
const connection = safeTerminalText(config.connectionName);
|
|
346
|
-
switch (config.backend.type) {
|
|
347
|
-
case "s3": {
|
|
348
|
-
const type =
|
|
349
|
-
config.backend.profile.kind === "r2" ||
|
|
350
|
-
isCloudflareR2Endpoint(config.backend.profile.endpoint)
|
|
351
|
-
? "Cloudflare R2"
|
|
352
|
-
: "S3-compatible";
|
|
353
|
-
return `${type} · ${connection} · ${safeTerminalText(config.backend.destination.bucket)}`;
|
|
354
|
-
}
|
|
355
|
-
case "webdav":
|
|
356
|
-
return `WebDAV · ${connection} · ${safeTerminalText(config.backend.destination.path)}`;
|
|
357
|
-
case "git":
|
|
358
|
-
return `Git · ${connection} · ${safeTerminalText(config.backend.destination.branch)}`;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
284
|
export async function showSetupWizard(ctx: ExtensionCommandContext, signal?: AbortSignal) {
|
|
363
285
|
if (ctx.mode !== "tui") {
|
|
364
286
|
ctx.ui.notify(
|
|
@@ -494,6 +416,65 @@ export async function showSetupWizard(ctx: ExtensionCommandContext, signal?: Abo
|
|
|
494
416
|
return true;
|
|
495
417
|
}
|
|
496
418
|
|
|
419
|
+
async function selectSetupForSwitch(
|
|
420
|
+
ctx: ExtensionCommandContext,
|
|
421
|
+
raw: Record<string, unknown>,
|
|
422
|
+
targets: Record<string, unknown>,
|
|
423
|
+
active: string | undefined,
|
|
424
|
+
signal?: AbortSignal,
|
|
425
|
+
) {
|
|
426
|
+
let selectedName: string | undefined;
|
|
427
|
+
const nameById = new Map<string, string>();
|
|
428
|
+
const profiles = ownRecord(raw.storageConnections);
|
|
429
|
+
const menu = defineMenu<undefined, "setups", "select", ExtensionCommandContext>({
|
|
430
|
+
start: "setups",
|
|
431
|
+
screens: {
|
|
432
|
+
setups: () => ({
|
|
433
|
+
kind: "actions",
|
|
434
|
+
title: "Switch sync setup",
|
|
435
|
+
lines: [`Current sync setup: ${safeTerminalText(active ?? "none")}`],
|
|
436
|
+
items: Object.keys(targets)
|
|
437
|
+
.sort((left, right) => left.localeCompare(right))
|
|
438
|
+
.map((candidate, index) => {
|
|
439
|
+
const target = ownRecord(targets[candidate]);
|
|
440
|
+
const storage = ownRecord(target?.storage);
|
|
441
|
+
const profileName =
|
|
442
|
+
typeof storage?.connection === "string" ? storage.connection : undefined;
|
|
443
|
+
const profile = profileName && profiles ? ownRecord(profiles[profileName]) : undefined;
|
|
444
|
+
const location = profile
|
|
445
|
+
? profile.type === "git"
|
|
446
|
+
? `${String(storage?.branch ?? "missing branch")}:${String(storage?.path ?? "missing path")}`
|
|
447
|
+
: profile.type === "s3"
|
|
448
|
+
? `${String(storage?.bucket ?? "missing bucket")}/${String(storage?.path ?? "missing path")}`
|
|
449
|
+
: String(storage?.path ?? "missing path")
|
|
450
|
+
: `invalid: missing connection ${profileName ?? "reference"}`;
|
|
451
|
+
const id = `setup:${index}`;
|
|
452
|
+
nameById.set(id, candidate);
|
|
453
|
+
return {
|
|
454
|
+
id,
|
|
455
|
+
label: `${safeTerminalText(candidate)}${candidate === active ? " (current)" : ""}`,
|
|
456
|
+
description: `${safeTerminalText(profileName ?? "unknown")} · ${safeTerminalText(location)}`,
|
|
457
|
+
action: "select" as const,
|
|
458
|
+
};
|
|
459
|
+
}),
|
|
460
|
+
hint: "close",
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
actions: {
|
|
464
|
+
select: async ({ itemId }) => {
|
|
465
|
+
selectedName = nameById.get(itemId);
|
|
466
|
+
return { kind: "close" };
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
await runMenu(ctx, menu, {
|
|
471
|
+
getState: () => undefined,
|
|
472
|
+
signal,
|
|
473
|
+
isCurrent: () => !signal?.aborted,
|
|
474
|
+
});
|
|
475
|
+
return selectedName;
|
|
476
|
+
}
|
|
477
|
+
|
|
497
478
|
async function showSetupSwitcher(
|
|
498
479
|
ctx: ExtensionCommandContext,
|
|
499
480
|
runRoute: RunRoute,
|
|
@@ -514,30 +495,8 @@ async function showSetupSwitcher(
|
|
|
514
495
|
const active = typeof raw.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
|
|
515
496
|
let name = selectedName;
|
|
516
497
|
if (!name) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
for (const candidate of Object.keys(targets).sort((left, right) => left.localeCompare(right))) {
|
|
520
|
-
const target = ownRecord(targets[candidate]);
|
|
521
|
-
const storage = ownRecord(target?.storage);
|
|
522
|
-
const profileName = typeof storage?.connection === "string" ? storage.connection : undefined;
|
|
523
|
-
const profile = profileName && profiles ? ownRecord(profiles[profileName]) : undefined;
|
|
524
|
-
const location = profile
|
|
525
|
-
? profile.type === "git"
|
|
526
|
-
? `${String(storage?.branch ?? "missing branch")}:${String(storage?.path ?? "missing path")}`
|
|
527
|
-
: profile.type === "s3"
|
|
528
|
-
? `${String(storage?.bucket ?? "missing bucket")}/${String(storage?.path ?? "missing path")}`
|
|
529
|
-
: String(storage?.path ?? "missing path")
|
|
530
|
-
: `invalid: missing connection ${profileName ?? "reference"}`;
|
|
531
|
-
const label = `${safeTerminalText(candidate)}${candidate === active ? " (current)" : ""} · ${safeTerminalText(profileName ?? "unknown")} · ${safeTerminalText(location)}`;
|
|
532
|
-
labels.set(label, candidate);
|
|
533
|
-
}
|
|
534
|
-
const selected = await ctx.ui.select(
|
|
535
|
-
`Switch sync setup\n\nCurrent sync setup: ${safeTerminalText(active ?? "none")}`,
|
|
536
|
-
[...labels.keys(), BACK],
|
|
537
|
-
{ signal },
|
|
538
|
-
);
|
|
539
|
-
if (!selected || selected === BACK) return false;
|
|
540
|
-
name = labels.get(selected);
|
|
498
|
+
name = await selectSetupForSwitch(ctx, raw, targets, active, signal);
|
|
499
|
+
if (!name) return false;
|
|
541
500
|
}
|
|
542
501
|
if (!name || !Object.hasOwn(targets, name)) {
|
|
543
502
|
ctx.ui.notify(
|
|
@@ -569,10 +528,9 @@ async function showSetupSwitcher(
|
|
|
569
528
|
: onSwitch === "pull-after-switch"
|
|
570
529
|
? "After switching, pi-sync will check this setup and show exact changes before applying them."
|
|
571
530
|
: "After switching, pi-sync will not pull or modify synced files.";
|
|
572
|
-
const
|
|
531
|
+
const confirmed = await ctx.ui.confirm(
|
|
532
|
+
"Switch sync setup?",
|
|
573
533
|
[
|
|
574
|
-
"Switch sync setup",
|
|
575
|
-
"",
|
|
576
534
|
`From: ${safeTerminalText(active ?? "none")}`,
|
|
577
535
|
`To: ${safeTerminalText(name)}`,
|
|
578
536
|
`Storage: ${backendStorageDescription(config)}`,
|
|
@@ -581,10 +539,9 @@ async function showSetupSwitcher(
|
|
|
581
539
|
"",
|
|
582
540
|
switchEffect,
|
|
583
541
|
].join("\n"),
|
|
584
|
-
[`Switch to ${safeTerminalText(name)}`, "Cancel"],
|
|
585
542
|
{ signal },
|
|
586
543
|
);
|
|
587
|
-
if (signal?.aborted ||
|
|
544
|
+
if (signal?.aborted || !confirmed) return false;
|
|
588
545
|
try {
|
|
589
546
|
const result = await useSyncSetup(
|
|
590
547
|
ctx,
|
|
@@ -806,22 +763,6 @@ async function refreshTargetCompletions() {
|
|
|
806
763
|
setSyncSetupCompletions(await configuredSyncSetupNames());
|
|
807
764
|
}
|
|
808
765
|
|
|
809
|
-
async function showRecoveryMenu(ctx: ExtensionCommandContext, runRoute: RunRoute) {
|
|
810
|
-
const lock = await inspectLock();
|
|
811
|
-
const canRecover =
|
|
812
|
-
lock.status === "unreadable" || (lock.status === "valid" && isStaleLock(lock.lock));
|
|
813
|
-
const selected = await ctx.ui.select("History & recovery", [
|
|
814
|
-
"Browse history",
|
|
815
|
-
"Check setup",
|
|
816
|
-
...(canRecover ? ["Recover stale operation"] : []),
|
|
817
|
-
BACK,
|
|
818
|
-
]);
|
|
819
|
-
if (!selected || selected === BACK) return;
|
|
820
|
-
if (selected === "Browse history") await runRoute("history");
|
|
821
|
-
else if (selected === "Check setup") await runRoute("doctor");
|
|
822
|
-
else await runRoute("unlock --stale");
|
|
823
|
-
}
|
|
824
|
-
|
|
825
766
|
interface ChosenRemoteLocation {
|
|
826
767
|
connectionName: string;
|
|
827
768
|
bucket: string;
|