@narumitw/pi-sync 0.34.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/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 { countValidSyncSetups, showSyncSetups } from "./sync-setups-ui.js";
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
- while (true) {
75
- const state = await describeManagerState(sessionSignal);
76
- if (sessionSignal?.aborted) return;
77
- const selected = await ctx.ui.select(state.title, state.actions, { signal: sessionSignal });
78
- if (sessionSignal?.aborted) return;
79
- if (!selected) return;
80
- switch (
81
- selected as
82
- | MainMenuAction
83
- | "Sync setups…"
84
- | "Storage connections…"
85
- | "History & recovery…"
86
- | "Help"
87
- | "Set up sync"
88
- | "Use existing settings"
89
- ) {
90
- case "Sync now (recommended)":
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
- break;
93
- case "Switch sync setup": {
120
+ return { kind: "stay" };
121
+ },
122
+ switch: async () => {
94
123
  const result = await showSetupSwitcher(ctx, runRoute, undefined, sessionSignal);
95
- if (result === "pull-attempted") return;
96
- if (result === "switched") continue;
97
- break;
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
- break;
102
- case "Settings":
128
+ return { kind: "stay" };
129
+ },
130
+ settings: async () => {
103
131
  await showSyncSettings(ctx, runRoute, sessionSignal);
104
- break;
105
- case "More…":
106
- if ((await showMoreMenu(ctx, runRoute, sessionSignal)) === "exit") return;
107
- break;
108
- case "Sync setups…":
109
- if ((await showSyncSetupManager(ctx, runRoute, sessionSignal)) === "exit") return;
110
- break;
111
- case "Storage connections…":
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
- break;
114
- case "History & recovery…":
115
- await showRecoveryMenu(ctx, runRoute);
116
- break;
117
- case "Help":
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
- case "Set up sync":
121
- case "Use existing settings":
178
+ return { kind: "close" };
179
+ },
180
+ init: async () => {
122
181
  await runRoute("init");
123
- continue;
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
- async function showMoreMenu(
129
- ctx: ExtensionCommandContext,
130
- runRoute: RunRoute,
131
- signal?: AbortSignal,
132
- ) {
133
- const selected = await ctx.ui.select("More options", [...MORE_MENU_ACTIONS], { signal });
134
- if (!selected || selected === BACK) return;
135
- if (selected === "Pull from remote…") {
136
- const result = await runCancellableOperation(
137
- ctx,
138
- "Checking remote changes…",
139
- "pull",
140
- runRoute,
141
- true,
142
- "Pull check cancelled; no local files were changed.",
143
- );
144
- if (result === "applied") return "exit" as const;
145
- return;
146
- }
147
- if (selected === "Push to remote…") {
148
- await runCancellableOperation(
149
- ctx,
150
- "Preparing push preview…",
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
- const profiles = ownRecord(raw.storageConnections);
518
- const labels = new Map<string, string>();
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 choice = await ctx.ui.select(
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 || choice !== `Switch to ${safeTerminalText(name)}`) return false;
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;