@narumitw/pi-sync 0.53.1 → 0.54.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 +6 -4
- package/dist/chunks/{chunk-TRCSEIAQ.ts → chunk-5ZWQ33XL.ts} +38 -18
- package/dist/chunks/{chunk-TRCSEIAQ.ts.map → chunk-5ZWQ33XL.ts.map} +2 -2
- package/dist/chunks/{chunk-FJGNDYGC.ts → chunk-VHXBCEVF.ts} +2 -2
- package/dist/chunks/{manager-result-dispatcher-AAEAYX57.ts → manager-result-dispatcher-6MQKFCWB.ts} +3 -3
- package/dist/chunks/{manager-ui-MHAHAKG3.ts → manager-ui-IURS4WXY.ts} +674 -609
- package/dist/chunks/manager-ui-IURS4WXY.ts.map +7 -0
- package/dist/chunks/{remote-selection-ui-Q2HUIBTL.ts → remote-selection-ui-6LJJ4IMQ.ts} +2 -2
- package/dist/chunks/{sync-operations-Y27F5T7W.ts → sync-operations-OWTE3ZL2.ts} +14 -8
- package/dist/chunks/{sync-operations-Y27F5T7W.ts.map → sync-operations-OWTE3ZL2.ts.map} +2 -2
- package/dist/index.ts +31 -6
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +11 -3
- package/package.json +4 -4
- package/src/commands/command-execution.ts +3 -0
- package/src/commands/command-handler.ts +8 -1
- package/src/sync/sync-mutations.ts +9 -7
- package/src/sync/sync-queries.ts +1 -0
- package/src/ui/cancellable-operation.ts +47 -17
- package/src/ui/manager-attention.ts +3 -5
- package/src/ui/manager-state.ts +32 -15
- package/src/ui/manager-ui.ts +59 -87
- package/src/ui/operation-dialogs.ts +29 -0
- package/src/ui/settings-ui.ts +140 -46
- package/src/ui/setup/setup-actions.ts +2 -2
- package/dist/chunks/manager-ui-MHAHAKG3.ts.map +0 -7
- /package/dist/chunks/{chunk-FJGNDYGC.ts.map → chunk-VHXBCEVF.ts.map} +0 -0
- /package/dist/chunks/{manager-result-dispatcher-AAEAYX57.ts.map → manager-result-dispatcher-6MQKFCWB.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-Q2HUIBTL.ts.map → remote-selection-ui-6LJJ4IMQ.ts.map} +0 -0
|
@@ -23,10 +23,10 @@ import {
|
|
|
23
23
|
} from "./chunk-4SBTAVFL.ts";
|
|
24
24
|
import {
|
|
25
25
|
dispatchManagerResult
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-VHXBCEVF.ts";
|
|
27
27
|
import {
|
|
28
28
|
runCancellableOperation
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-5ZWQ33XL.ts";
|
|
30
30
|
import "./chunk-V6VZ5TLG.ts";
|
|
31
31
|
import "./chunk-SDZCTCYQ.ts";
|
|
32
32
|
import {
|
|
@@ -161,7 +161,7 @@ function attentionMainMenuItems(manager) {
|
|
|
161
161
|
];
|
|
162
162
|
}
|
|
163
163
|
function blockedSyncMenuItem(label, manager) {
|
|
164
|
-
if (label !== "Sync now
|
|
164
|
+
if (label !== "Sync now" || !manager.attentionBlocksSync) return void 0;
|
|
165
165
|
return {
|
|
166
166
|
id: "sync",
|
|
167
167
|
label,
|
|
@@ -172,7 +172,7 @@ function blockedSyncMenuItem(label, manager) {
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
async function showManagerAttention(ctx, attention, runRoute, signal, onSelectionResolved) {
|
|
175
|
-
const { showRemoteSelectionReview } = await import("./remote-selection-ui-
|
|
175
|
+
const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
|
|
176
176
|
if (signal?.aborted) return "close";
|
|
177
177
|
const review = await showRemoteSelectionReview(ctx, attention.decision.setupName, signal, void 0, {
|
|
178
178
|
decision: attention.decision,
|
|
@@ -450,11 +450,12 @@ function storageLocation(config) {
|
|
|
450
450
|
|
|
451
451
|
// src/ui/manager-state.ts
|
|
452
452
|
var MAIN_MENU_ACTIONS = [
|
|
453
|
-
"Sync now
|
|
454
|
-
"
|
|
455
|
-
"
|
|
453
|
+
"Sync now",
|
|
454
|
+
"Pull from remote\u2026",
|
|
455
|
+
"Push to remote\u2026",
|
|
456
456
|
"Settings",
|
|
457
|
-
"
|
|
457
|
+
"History",
|
|
458
|
+
"Diagnostics"
|
|
458
459
|
];
|
|
459
460
|
async function describeManagerState(signal, attention, inspectOperation = inspectOperationAvailability, observation) {
|
|
460
461
|
let raw;
|
|
@@ -515,7 +516,9 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
|
|
|
515
516
|
const currentObservation = observation && observationMatchesConfig(observation, config) && syncState && syncStateFingerprint(syncState) === observation.inspection.stateIdentity ? observation : void 0;
|
|
516
517
|
const lastAppliedSnapshot = changesBlocked ? "Unavailable while operations are locked" : syncState?.lastAppliedSnapshot ? safeTerminalText(syncState.lastAppliedSnapshot) : syncState ? "Never synced" : "Unavailable";
|
|
517
518
|
const canSwitch = await countValidSyncSetups(configuredTargets, signal) > 1;
|
|
518
|
-
const mainActions = MAIN_MENU_ACTIONS
|
|
519
|
+
const mainActions = [...MAIN_MENU_ACTIONS];
|
|
520
|
+
if (canSwitch) mainActions.push("Switch sync setup");
|
|
521
|
+
if (syncState?.unresolved?.length) mainActions.unshift("Review unresolved conflicts");
|
|
519
522
|
const ordinaryTitle = [
|
|
520
523
|
"Manage sync",
|
|
521
524
|
"",
|
|
@@ -526,7 +529,7 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
|
|
|
526
529
|
`Automatic transfer at startup: ${config.automaticTransfer ? "On \u2014 upload/replace/delete at idle; no reload" : "Off"}`,
|
|
527
530
|
`Last applied: ${lastAppliedSnapshot}`,
|
|
528
531
|
...syncState?.unresolved?.length ? [
|
|
529
|
-
`Unresolved: ${syncState.unresolved.length} dependency groups \xB7
|
|
532
|
+
`Unresolved: ${syncState.unresolved.length} dependency groups \xB7 choose Review unresolved conflicts`,
|
|
530
533
|
`Last observed: ${safeTerminalText(syncState.lastObservedSnapshot ?? "unknown")}`
|
|
531
534
|
] : [],
|
|
532
535
|
...currentAttention ? [
|
|
@@ -565,18 +568,24 @@ async function describeManagerState(signal, attention, inspectOperation = inspec
|
|
|
565
568
|
"",
|
|
566
569
|
"What do you want to do?"
|
|
567
570
|
].join("\n"),
|
|
568
|
-
actions: ["Sync setups\u2026", "Storage connections\u2026", "History
|
|
571
|
+
actions: ["Sync setups\u2026", "Storage connections\u2026", "History", "Help"]
|
|
569
572
|
};
|
|
570
573
|
}
|
|
571
574
|
}
|
|
572
575
|
function operationActions(operation, noSyncedContent, canSwitch, mainActions) {
|
|
573
576
|
if (operationCanRecover(operation)) {
|
|
574
|
-
return ["Restore sync access\u2026 (recommended)", "
|
|
577
|
+
return ["Restore sync access\u2026 (recommended)", "History", "Help"];
|
|
575
578
|
}
|
|
576
579
|
if (operation.kind !== "free") {
|
|
577
|
-
return ["Refresh operation status", "
|
|
580
|
+
return ["Refresh operation status", "History", "Help"];
|
|
578
581
|
}
|
|
579
|
-
return noSyncedContent ? [
|
|
582
|
+
return noSyncedContent ? [
|
|
583
|
+
...mainActions.includes("Review unresolved conflicts") ? ["Review unresolved conflicts"] : [],
|
|
584
|
+
"Settings",
|
|
585
|
+
...canSwitch ? ["Switch sync setup"] : [],
|
|
586
|
+
"History",
|
|
587
|
+
"Diagnostics"
|
|
588
|
+
] : mainActions;
|
|
580
589
|
}
|
|
581
590
|
function operationStatusLines(operation) {
|
|
582
591
|
switch (operation.kind) {
|
|
@@ -584,16 +593,24 @@ function operationStatusLines(operation) {
|
|
|
584
593
|
return [];
|
|
585
594
|
case "live": {
|
|
586
595
|
const command = truncateToWidth(safeTerminalText(operation.lock.command), 16, "\u2026");
|
|
587
|
-
return [
|
|
596
|
+
return [
|
|
597
|
+
`Running: ${command} (pid ${operation.lock.pid}). Wait, then refresh; Transfer actions and Settings return.`
|
|
598
|
+
];
|
|
588
599
|
}
|
|
589
600
|
case "busy":
|
|
590
|
-
return [
|
|
601
|
+
return [
|
|
602
|
+
"Pi-sync may be starting or finishing. Wait, then refresh; Transfer actions and Settings remain unavailable."
|
|
603
|
+
];
|
|
591
604
|
case "recoverable-stale":
|
|
592
|
-
return [
|
|
605
|
+
return [
|
|
606
|
+
"Sync paused: old lock remains. Close other Pi sessions then restore; Transfer actions and Settings return."
|
|
607
|
+
];
|
|
593
608
|
case "recoverable-unreadable":
|
|
594
|
-
return [
|
|
609
|
+
return [
|
|
610
|
+
"Sync paused: owner unknown. Close other Pi sessions then restore; Transfer actions and Settings return."
|
|
611
|
+
];
|
|
595
612
|
case "inspection-error":
|
|
596
|
-
return ["Lock check failed. Fix path access, then refresh;
|
|
613
|
+
return ["Lock check failed. Fix path access, then refresh; Transfer actions and Settings remain unavailable."];
|
|
597
614
|
}
|
|
598
615
|
}
|
|
599
616
|
function backendStorageDescription(config) {
|
|
@@ -611,419 +628,96 @@ function backendStorageDescription(config) {
|
|
|
611
628
|
}
|
|
612
629
|
|
|
613
630
|
// src/ui/settings-ui.ts
|
|
631
|
+
import { defineMenu as defineMenu4, runMenu as runMenu4 } from "@narumitw/pi-tui-kit";
|
|
632
|
+
|
|
633
|
+
// src/ui/storage-connections-ui.ts
|
|
634
|
+
import { isDeepStrictEqual } from "node:util";
|
|
614
635
|
import { defineMenu as defineMenu2, runMenu as runMenu2 } from "@narumitw/pi-tui-kit";
|
|
615
|
-
async function
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
const initial = await loadConfig();
|
|
621
|
-
if (signal?.aborted) return;
|
|
622
|
-
const setupName = initial.setupName;
|
|
636
|
+
async function showStorageConnections(ctx, signal) {
|
|
637
|
+
let selectedName;
|
|
638
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
623
639
|
const menu = defineMenu2({
|
|
624
|
-
start: "
|
|
640
|
+
start: "list",
|
|
625
641
|
screens: {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
id: "
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
{
|
|
666
|
-
id: "include",
|
|
667
|
-
label: "Included content",
|
|
668
|
-
description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
|
|
669
|
-
currentValue: "Open editor",
|
|
670
|
-
action: "include"
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
id: "remoteInclude",
|
|
674
|
-
label: "Compare synced content",
|
|
675
|
-
description: "Review this device and remote content lists before choosing either one.",
|
|
676
|
-
currentValue: "Review",
|
|
677
|
-
action: "remote-include"
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
id: "automaticTransfer",
|
|
681
|
-
label: "Automatic transfer at startup",
|
|
682
|
-
description: "May upload, replace, or delete selected files once at idle startup in TUI/RPC. Requires a baseline and conditional/lease publication; never reloads resources. Turning off cancels pending work.",
|
|
683
|
-
currentValue: state.automaticTransfer ? "On" : "Off",
|
|
684
|
-
values: ["On", "Off"],
|
|
685
|
-
action: "automatic-transfer"
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
id: "mergeSettings",
|
|
689
|
-
label: "Settings field merge (experimental)",
|
|
690
|
-
description: "Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
|
|
691
|
-
currentValue: state.mergeSettings ? "On" : "Off",
|
|
692
|
-
values: ["On", "Off"],
|
|
693
|
-
action: "merge-settings"
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
id: "localFields",
|
|
697
|
-
label: "Machine-local settings fields",
|
|
698
|
-
description: "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
|
|
699
|
-
currentValue: `${state.localFields?.length ?? 0} fields \xB7 Edit`,
|
|
700
|
-
action: "local-fields"
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
id: "contentPolicy",
|
|
704
|
-
label: "Content / partial sync (experimental)",
|
|
705
|
-
description: "Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
|
|
706
|
-
currentValue: state.mergeContent ? state.partialSync ? "Content & partial" : "Content only" : state.partialSync ? "Partial only" : "Off",
|
|
707
|
-
values: ["Off", "Content only", "Content & partial", "Partial only"],
|
|
708
|
-
action: "content-policy"
|
|
709
|
-
}
|
|
710
|
-
]
|
|
642
|
+
list: ({ state }) => {
|
|
643
|
+
nameById.clear();
|
|
644
|
+
const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
|
|
645
|
+
return {
|
|
646
|
+
kind: "actions",
|
|
647
|
+
title: "Storage connections",
|
|
648
|
+
lines: state.supportedVersion ? ["Server addresses and sign-in details, reusable across sync setups."] : ["Create supported pi-sync settings before managing storage connections."],
|
|
649
|
+
items: state.supportedVersion ? [
|
|
650
|
+
{ id: "add", label: "Add storage connection", action: "add" },
|
|
651
|
+
...names.map((name, index) => {
|
|
652
|
+
const id = `connection:${index}`;
|
|
653
|
+
nameById.set(id, name);
|
|
654
|
+
return {
|
|
655
|
+
id,
|
|
656
|
+
label: safeTerminalText(name),
|
|
657
|
+
description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
|
|
658
|
+
action: "select"
|
|
659
|
+
};
|
|
660
|
+
})
|
|
661
|
+
] : [],
|
|
662
|
+
hint: "back"
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
detail: ({ state }) => ({
|
|
666
|
+
kind: "actions",
|
|
667
|
+
title: state.selected ? `Storage connection \u201C${safeTerminalText(state.selected.name)}\u201D` : "Storage connection",
|
|
668
|
+
lines: state.selected?.lines ?? ["This storage connection no longer exists."],
|
|
669
|
+
items: state.selected ? [
|
|
670
|
+
{ id: "edit", label: "Edit storage connection\u2026", action: "edit" },
|
|
671
|
+
...state.selected.usedBy.length === 0 ? [
|
|
672
|
+
{
|
|
673
|
+
id: "remove",
|
|
674
|
+
label: "Remove storage connection\u2026",
|
|
675
|
+
action: "remove"
|
|
676
|
+
}
|
|
677
|
+
] : [],
|
|
678
|
+
{ id: "back", label: "Back", action: "back" }
|
|
679
|
+
] : [{ id: "back", label: "Back", action: "back" }],
|
|
680
|
+
hint: "back"
|
|
711
681
|
})
|
|
712
682
|
},
|
|
713
683
|
actions: {
|
|
714
|
-
|
|
715
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
716
|
-
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
684
|
+
add: async () => {
|
|
717
685
|
try {
|
|
718
|
-
|
|
719
|
-
validate();
|
|
720
|
-
const mergeContent = value === "Content only" || value === "Content & partial";
|
|
721
|
-
const partialSync = value === "Partial only" || value === "Content & partial";
|
|
722
|
-
if (!await ctx.ui.confirm(
|
|
723
|
-
"Save experimental content policy?",
|
|
724
|
-
"This explicitly upgrades settings to version 5; partial snapshots use version 3 and older clients must refuse them. Text/session merge is conservative. Partial sync preserves withheld versions and old baselines. Disabling does not clear unresolved groups or recovery evidence. Review a directional migration first if portable policy changes.",
|
|
725
|
-
{ signal: mutationSignal }
|
|
726
|
-
))
|
|
727
|
-
return { kind: "rejected" };
|
|
728
|
-
validate();
|
|
729
|
-
await updateLocalConfig((current) => {
|
|
730
|
-
validate();
|
|
731
|
-
const setup = current.syncSetups[setupName];
|
|
732
|
-
if (!setup || Boolean(setup.sync.mergeContent) !== Boolean(previous.mergeContent) || Boolean(setup.sync.partialSync) !== Boolean(previous.partialSync))
|
|
733
|
-
throw new Error("Content policy changed during review.");
|
|
734
|
-
return {
|
|
735
|
-
...current,
|
|
736
|
-
version: 5,
|
|
737
|
-
syncSetups: {
|
|
738
|
-
...current.syncSetups,
|
|
739
|
-
[setupName]: { ...setup, sync: { ...setup.sync, mergeContent, partialSync } }
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
}, mutationSignal);
|
|
743
|
-
validate();
|
|
744
|
-
return { kind: "stay" };
|
|
686
|
+
await showAddStorageConnection(ctx, signal);
|
|
745
687
|
} catch (error) {
|
|
746
|
-
if (!
|
|
747
|
-
|
|
688
|
+
if (!signal?.aborted) {
|
|
689
|
+
ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
|
|
690
|
+
}
|
|
748
691
|
}
|
|
692
|
+
return { kind: "stay" };
|
|
749
693
|
},
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
694
|
+
select: async ({ itemId }) => {
|
|
695
|
+
selectedName = nameById.get(itemId);
|
|
696
|
+
return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
|
|
697
|
+
},
|
|
698
|
+
edit: async ({ state }) => {
|
|
699
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
753
700
|
try {
|
|
754
|
-
|
|
755
|
-
await updateSyncSetup(
|
|
756
|
-
setupName,
|
|
757
|
-
(setup) => ({ ...setup, sync: { ...setup.sync, mergeSettings: value === "On" } }),
|
|
758
|
-
{ signal: mutationSignal }
|
|
759
|
-
);
|
|
760
|
-
validate();
|
|
761
|
-
return { kind: "stay" };
|
|
701
|
+
await editStorageConnection(ctx, state.selected.name, state.selected.profile, state.selected.usedBy, signal);
|
|
762
702
|
} catch (error) {
|
|
763
|
-
|
|
764
|
-
return { kind: "rejected" };
|
|
703
|
+
notifyConnectionError(ctx, state.selected.name, error, signal);
|
|
765
704
|
}
|
|
705
|
+
return { kind: "stay" };
|
|
766
706
|
},
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
707
|
+
remove: async ({ state }) => {
|
|
708
|
+
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
709
|
+
const name = state.selected.name;
|
|
710
|
+
const confirmed = await ctx.ui.confirm(
|
|
711
|
+
"Remove storage connection?",
|
|
712
|
+
`Remove local storage connection \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
|
|
713
|
+
{ signal }
|
|
714
|
+
);
|
|
715
|
+
if (!confirmed || signal?.aborted) return { kind: "rejected" };
|
|
770
716
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
JSON.stringify(previous.localFields ?? []),
|
|
776
|
-
{ signal: mutationSignal }
|
|
777
|
-
);
|
|
778
|
-
validate();
|
|
779
|
-
if (input === void 0) return { kind: "rejected" };
|
|
780
|
-
let fields;
|
|
781
|
-
try {
|
|
782
|
-
fields = normalizeLocalFields(JSON.parse(input));
|
|
783
|
-
} catch {
|
|
784
|
-
throw new Error("Invalid localFields JSON array; no policy was changed.");
|
|
785
|
-
}
|
|
786
|
-
if (sameLocalFields(previous.localFields, fields)) return { kind: "stay" };
|
|
787
|
-
const confirmed = await ctx.ui.confirm(
|
|
788
|
-
"Save portable field policy?",
|
|
789
|
-
"This opts into settings/snapshot version 4/2; older clients must refuse them. Future snapshots omit these fields, but old remote history is NOT erased. Review an explicit force push/pull migration before further sync. Removed rules can expose or replace local-only values.",
|
|
790
|
-
{ signal: mutationSignal }
|
|
791
|
-
);
|
|
792
|
-
validate();
|
|
793
|
-
if (!confirmed) return { kind: "rejected" };
|
|
794
|
-
await updateLocalConfig((current) => {
|
|
795
|
-
validate();
|
|
796
|
-
const setup = current.syncSetups[setupName];
|
|
797
|
-
if (!setup || !sameLocalFields(setup.sync.localFields, previous.localFields))
|
|
798
|
-
throw new Error("Field policy changed while under review; reopen settings.");
|
|
799
|
-
return {
|
|
800
|
-
...current,
|
|
801
|
-
version: current.version === 5 ? 5 : 4,
|
|
802
|
-
syncSetups: {
|
|
803
|
-
...current.syncSetups,
|
|
804
|
-
[setupName]: { ...setup, sync: { ...setup.sync, localFields: fields } }
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
}, mutationSignal);
|
|
808
|
-
validate();
|
|
809
|
-
return { kind: "stay" };
|
|
810
|
-
} catch (error) {
|
|
811
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
812
|
-
return { kind: "rejected" };
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
automatic: async ({ value, signal: actionSignal }) => {
|
|
816
|
-
const automatic = value === "On";
|
|
817
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
818
|
-
try {
|
|
819
|
-
const latest = await loadConfig(setupName);
|
|
820
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
821
|
-
if (latest.automatic === automatic) return { kind: "stay" };
|
|
822
|
-
await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automatic } }), {
|
|
823
|
-
signal: mutationSignal
|
|
824
|
-
});
|
|
825
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
826
|
-
ctx.ui.notify(
|
|
827
|
-
`Automatic sync ${automatic ? "enabled" : "disabled"} for \u201C${safeTerminalText(setupName)}\u201D.`,
|
|
828
|
-
"info"
|
|
829
|
-
);
|
|
830
|
-
return { kind: "stay" };
|
|
831
|
-
} catch (error) {
|
|
832
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
833
|
-
return { kind: "rejected" };
|
|
834
|
-
}
|
|
835
|
-
},
|
|
836
|
-
"automatic-transfer": async ({ value, signal: actionSignal }) => {
|
|
837
|
-
const automaticTransfer = value === "On";
|
|
838
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
839
|
-
try {
|
|
840
|
-
await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automaticTransfer } }), {
|
|
841
|
-
signal: mutationSignal
|
|
842
|
-
});
|
|
843
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
844
|
-
ctx.ui.notify(
|
|
845
|
-
`Automatic startup transfer ${automaticTransfer ? "enabled for the next session start" : "disabled"}. Completed transfers are not undone.`,
|
|
846
|
-
"info"
|
|
847
|
-
);
|
|
848
|
-
return { kind: "stay" };
|
|
849
|
-
} catch (error) {
|
|
850
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
851
|
-
return { kind: "rejected" };
|
|
852
|
-
}
|
|
853
|
-
},
|
|
854
|
-
"skip-secret-scan": async ({ value, signal: actionSignal }) => {
|
|
855
|
-
const skipSecretScan = value === "On";
|
|
856
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
857
|
-
try {
|
|
858
|
-
const latest = await loadConfig(setupName);
|
|
859
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
860
|
-
if (latest.skipSecretScan === skipSecretScan) return { kind: "stay" };
|
|
861
|
-
await updateLocalConfig((settings) => ({ ...settings, skipSecretScan }), mutationSignal);
|
|
862
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
863
|
-
ctx.ui.notify(`Secret scan ${skipSecretScan ? "disabled" : "enabled"} for pushes in all setups.`, "info");
|
|
864
|
-
return { kind: "stay" };
|
|
865
|
-
} catch (error) {
|
|
866
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
867
|
-
return { kind: "rejected" };
|
|
868
|
-
}
|
|
869
|
-
},
|
|
870
|
-
"show-status": async ({ value, signal: actionSignal }) => {
|
|
871
|
-
const showStatus = value === "On";
|
|
872
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
873
|
-
try {
|
|
874
|
-
const latest = await loadConfig(setupName);
|
|
875
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
876
|
-
if (latest.showStatus === showStatus) return { kind: "stay" };
|
|
877
|
-
await updateLocalConfig((settings) => ({ ...settings, showStatus }), mutationSignal);
|
|
878
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
879
|
-
configureSyncStatus(ctx, showStatus);
|
|
880
|
-
ctx.ui.notify(`Pi Sync status ${showStatus ? "enabled" : "disabled"} for all setups.`, "info");
|
|
881
|
-
return { kind: "stay" };
|
|
882
|
-
} catch (error) {
|
|
883
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
884
|
-
return { kind: "rejected" };
|
|
885
|
-
}
|
|
886
|
-
},
|
|
887
|
-
"on-switch": async ({ value, signal: actionSignal }) => {
|
|
888
|
-
const action = value ? setupSwitchActionFromLabel(value) : void 0;
|
|
889
|
-
if (!action) return { kind: "rejected" };
|
|
890
|
-
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
891
|
-
try {
|
|
892
|
-
const latest = await loadConfig(setupName);
|
|
893
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
894
|
-
if (latest.onSwitch === action) return { kind: "stay" };
|
|
895
|
-
await saveOnSwitch(action, mutationSignal);
|
|
896
|
-
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
897
|
-
ctx.ui.notify(`After switching setup: ${value}.`, "info");
|
|
898
|
-
return { kind: "stay" };
|
|
899
|
-
} catch (error) {
|
|
900
|
-
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
901
|
-
return { kind: "rejected" };
|
|
902
|
-
}
|
|
903
|
-
},
|
|
904
|
-
include: async ({ signal: actionSignal }) => {
|
|
905
|
-
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
906
|
-
await runRoute("files", editorSignal, void 0, setupName);
|
|
907
|
-
return editorSignal.aborted ? { kind: "rejected" } : { kind: "stay" };
|
|
908
|
-
},
|
|
909
|
-
"remote-include": async ({ signal: actionSignal }) => {
|
|
910
|
-
const reviewSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
911
|
-
const { showRemoteSelectionReview } = await import("./remote-selection-ui-Q2HUIBTL.ts");
|
|
912
|
-
if (reviewSignal.aborted) return { kind: "rejected" };
|
|
913
|
-
const review = await showRemoteSelectionReview(ctx, setupName, reviewSignal, void 0, {
|
|
914
|
-
origin: "settings",
|
|
915
|
-
runRoute
|
|
916
|
-
});
|
|
917
|
-
if (reviewSignal.aborted) return { kind: "rejected" };
|
|
918
|
-
if (review.kind === "route-result") {
|
|
919
|
-
const disposition = await dispatchManagerResult(ctx, review.result, review.route, runRoute, reviewSignal);
|
|
920
|
-
return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
|
|
921
|
-
}
|
|
922
|
-
return review.kind === "closed" || review.kind === "stale" ? { kind: "close" } : { kind: "stay" };
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
await runMenu2(ctx, menu, {
|
|
927
|
-
getState: () => loadConfig(setupName),
|
|
928
|
-
signal,
|
|
929
|
-
isCurrent: () => !signal?.aborted
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
function notifySaveFailure(ctx, error) {
|
|
933
|
-
ctx.ui.notify(
|
|
934
|
-
`Pi Sync settings save failed: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
|
|
935
|
-
"error"
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// src/ui/storage-connections-ui.ts
|
|
940
|
-
import { isDeepStrictEqual } from "node:util";
|
|
941
|
-
import { defineMenu as defineMenu3, runMenu as runMenu3 } from "@narumitw/pi-tui-kit";
|
|
942
|
-
async function showStorageConnections(ctx, signal) {
|
|
943
|
-
let selectedName;
|
|
944
|
-
const nameById = /* @__PURE__ */ new Map();
|
|
945
|
-
const menu = defineMenu3({
|
|
946
|
-
start: "list",
|
|
947
|
-
screens: {
|
|
948
|
-
list: ({ state }) => {
|
|
949
|
-
nameById.clear();
|
|
950
|
-
const names = Object.keys(state.profiles).sort((left, right) => left.localeCompare(right));
|
|
951
|
-
return {
|
|
952
|
-
kind: "actions",
|
|
953
|
-
title: "Storage connections",
|
|
954
|
-
lines: state.supportedVersion ? ["Server addresses and sign-in details, reusable across sync setups."] : ["Create supported pi-sync settings before managing storage connections."],
|
|
955
|
-
items: state.supportedVersion ? [
|
|
956
|
-
{ id: "add", label: "Add storage connection", action: "add" },
|
|
957
|
-
...names.map((name, index) => {
|
|
958
|
-
const id = `connection:${index}`;
|
|
959
|
-
nameById.set(id, name);
|
|
960
|
-
return {
|
|
961
|
-
id,
|
|
962
|
-
label: safeTerminalText(name),
|
|
963
|
-
description: connectionSummary(ownRecord(state.profiles[name]) ?? {}),
|
|
964
|
-
action: "select"
|
|
965
|
-
};
|
|
966
|
-
})
|
|
967
|
-
] : [],
|
|
968
|
-
hint: "back"
|
|
969
|
-
};
|
|
970
|
-
},
|
|
971
|
-
detail: ({ state }) => ({
|
|
972
|
-
kind: "actions",
|
|
973
|
-
title: state.selected ? `Storage connection \u201C${safeTerminalText(state.selected.name)}\u201D` : "Storage connection",
|
|
974
|
-
lines: state.selected?.lines ?? ["This storage connection no longer exists."],
|
|
975
|
-
items: state.selected ? [
|
|
976
|
-
{ id: "edit", label: "Edit storage connection\u2026", action: "edit" },
|
|
977
|
-
...state.selected.usedBy.length === 0 ? [
|
|
978
|
-
{
|
|
979
|
-
id: "remove",
|
|
980
|
-
label: "Remove storage connection\u2026",
|
|
981
|
-
action: "remove"
|
|
982
|
-
}
|
|
983
|
-
] : [],
|
|
984
|
-
{ id: "back", label: "Back", action: "back" }
|
|
985
|
-
] : [{ id: "back", label: "Back", action: "back" }],
|
|
986
|
-
hint: "back"
|
|
987
|
-
})
|
|
988
|
-
},
|
|
989
|
-
actions: {
|
|
990
|
-
add: async () => {
|
|
991
|
-
try {
|
|
992
|
-
await showAddStorageConnection(ctx, signal);
|
|
993
|
-
} catch (error) {
|
|
994
|
-
if (!signal?.aborted) {
|
|
995
|
-
ctx.ui.notify(`Storage connection was not added: ${syncErrorGuidance(error)}`, "error");
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
return { kind: "stay" };
|
|
999
|
-
},
|
|
1000
|
-
select: async ({ itemId }) => {
|
|
1001
|
-
selectedName = nameById.get(itemId);
|
|
1002
|
-
return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
|
|
1003
|
-
},
|
|
1004
|
-
edit: async ({ state }) => {
|
|
1005
|
-
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
1006
|
-
try {
|
|
1007
|
-
await editStorageConnection(ctx, state.selected.name, state.selected.profile, state.selected.usedBy, signal);
|
|
1008
|
-
} catch (error) {
|
|
1009
|
-
notifyConnectionError(ctx, state.selected.name, error, signal);
|
|
1010
|
-
}
|
|
1011
|
-
return { kind: "stay" };
|
|
1012
|
-
},
|
|
1013
|
-
remove: async ({ state }) => {
|
|
1014
|
-
if (!state.selected || state.selected.name !== selectedName) return { kind: "rejected" };
|
|
1015
|
-
const name = state.selected.name;
|
|
1016
|
-
const confirmed = await ctx.ui.confirm(
|
|
1017
|
-
"Remove storage connection?",
|
|
1018
|
-
`Remove local storage connection \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
|
|
1019
|
-
{ signal }
|
|
1020
|
-
);
|
|
1021
|
-
if (!confirmed || signal?.aborted) return { kind: "rejected" };
|
|
1022
|
-
try {
|
|
1023
|
-
await removeStorageConnection(name, signal);
|
|
1024
|
-
ctx.ui.notify(`Removed storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
|
|
1025
|
-
selectedName = void 0;
|
|
1026
|
-
return { kind: "back" };
|
|
717
|
+
await removeStorageConnection(name, signal);
|
|
718
|
+
ctx.ui.notify(`Removed storage connection \u201C${safeTerminalText(name)}\u201D.`, "info");
|
|
719
|
+
selectedName = void 0;
|
|
720
|
+
return { kind: "back" };
|
|
1027
721
|
} catch (error) {
|
|
1028
722
|
notifyConnectionError(ctx, name, error, signal);
|
|
1029
723
|
return { kind: "stay" };
|
|
@@ -1035,7 +729,7 @@ async function showStorageConnections(ctx, signal) {
|
|
|
1035
729
|
}
|
|
1036
730
|
}
|
|
1037
731
|
});
|
|
1038
|
-
await
|
|
732
|
+
await runMenu2(ctx, menu, {
|
|
1039
733
|
getState: () => loadStorageMenuState(selectedName, signal),
|
|
1040
734
|
signal,
|
|
1041
735
|
isCurrent: () => !signal?.aborted
|
|
@@ -1251,12 +945,12 @@ function credentialSource(profile) {
|
|
|
1251
945
|
}
|
|
1252
946
|
|
|
1253
947
|
// src/ui/setup/setup-switcher.ts
|
|
1254
|
-
import { defineMenu as
|
|
948
|
+
import { defineMenu as defineMenu3, runMenu as runMenu3 } from "@narumitw/pi-tui-kit";
|
|
1255
949
|
async function selectSetupForSwitch(ctx, raw, targets, active, signal) {
|
|
1256
950
|
let selectedName;
|
|
1257
951
|
const nameById = /* @__PURE__ */ new Map();
|
|
1258
952
|
const profiles = ownRecord(raw.storageConnections);
|
|
1259
|
-
const menu =
|
|
953
|
+
const menu = defineMenu3({
|
|
1260
954
|
start: "setups",
|
|
1261
955
|
screens: {
|
|
1262
956
|
setups: () => ({
|
|
@@ -1288,7 +982,7 @@ async function selectSetupForSwitch(ctx, raw, targets, active, signal) {
|
|
|
1288
982
|
}
|
|
1289
983
|
}
|
|
1290
984
|
});
|
|
1291
|
-
await
|
|
985
|
+
await runMenu3(ctx, menu, {
|
|
1292
986
|
getState: () => void 0,
|
|
1293
987
|
signal,
|
|
1294
988
|
isCurrent: () => !signal?.aborted
|
|
@@ -1372,124 +1066,530 @@ async function showSetupSwitcher(ctx, runRoute, selectedName, signal) {
|
|
|
1372
1066
|
if (pullResult.kind === "closed" || pullResult.kind.endsWith("required")) {
|
|
1373
1067
|
pullClosed = disposition.kind === "close";
|
|
1374
1068
|
}
|
|
1375
|
-
if (disposition.appliedRoute === "pull") return "applied";
|
|
1376
|
-
if (pullResult.kind === "completed") return pullResult.outcome;
|
|
1377
|
-
return pullResult.kind === "cancelled" ? "cancelled" : void 0;
|
|
1069
|
+
if (disposition.appliedRoute === "pull") return "applied";
|
|
1070
|
+
if (pullResult.kind === "completed") return pullResult.outcome;
|
|
1071
|
+
return pullResult.kind === "cancelled" ? "cancelled" : void 0;
|
|
1072
|
+
},
|
|
1073
|
+
onSwitch,
|
|
1074
|
+
signal,
|
|
1075
|
+
syncConfigReviewIdentity(config)
|
|
1076
|
+
);
|
|
1077
|
+
if (pullClosed) return "closed";
|
|
1078
|
+
return result.pullApplied ? "pull-attempted" : "switched";
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
if (signal?.aborted) return false;
|
|
1081
|
+
ctx.ui.notify(
|
|
1082
|
+
`Sync setup \u201C${safeTerminalText(name)}\u201D was not switched: ${safeTerminalText(errorMessage(error))}`,
|
|
1083
|
+
"error"
|
|
1084
|
+
);
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/ui/setup/setup-actions.ts
|
|
1090
|
+
async function showSyncSetupManager(ctx, runRoute, signal) {
|
|
1091
|
+
return showSyncSetups(
|
|
1092
|
+
ctx,
|
|
1093
|
+
{
|
|
1094
|
+
add: async (setupSignal) => {
|
|
1095
|
+
await showAddTarget(ctx, setupSignal);
|
|
1096
|
+
},
|
|
1097
|
+
edit: async (name, setupSignal) => {
|
|
1098
|
+
await showEditSyncSetupStorage(ctx, name, setupSignal);
|
|
1099
|
+
},
|
|
1100
|
+
makeCurrent: async (name, setupSignal) => {
|
|
1101
|
+
const result = await showSetupSwitcher(ctx, runRoute, name, setupSignal);
|
|
1102
|
+
return result === "pull-attempted" || result === "closed" ? "exit" : void 0;
|
|
1103
|
+
},
|
|
1104
|
+
remove: async (name, setupSignal) => {
|
|
1105
|
+
await showRemoveTarget(ctx, name, setupSignal);
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
signal
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
async function showAddTarget(ctx, signal) {
|
|
1112
|
+
let raw = await readLocalConfigObject();
|
|
1113
|
+
if (signal?.aborted) return;
|
|
1114
|
+
if (!raw) return void ctx.ui.notify("Set up the first sync setup before adding another.", "info");
|
|
1115
|
+
if (raw.version !== 3 && raw.version !== 4 && raw.version !== 5) {
|
|
1116
|
+
ctx.ui.notify("Version 1 and version 2 settings are unsupported and are never migrated.", "error");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
let profiles = ownRecord(raw.storageConnections) ?? {};
|
|
1120
|
+
const name = await promptResourceName(ctx, "sync setup", "work", signal);
|
|
1121
|
+
if (!name) return;
|
|
1122
|
+
const createConnection = "Add a new storage connection\u2026";
|
|
1123
|
+
let profile = await ctx.ui.select(
|
|
1124
|
+
"Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
|
|
1125
|
+
[...Object.keys(profiles).sort(), createConnection, "Cancel"],
|
|
1126
|
+
{ signal }
|
|
1127
|
+
);
|
|
1128
|
+
if (signal?.aborted || !profile || profile === "Cancel") return;
|
|
1129
|
+
if (profile === createConnection) {
|
|
1130
|
+
const previousNames = new Set(Object.keys(profiles));
|
|
1131
|
+
if (!await showAddStorageConnection(ctx, signal)) return;
|
|
1132
|
+
if (signal?.aborted) return;
|
|
1133
|
+
raw = await readLocalConfigObject() ?? raw;
|
|
1134
|
+
if (signal?.aborted) return;
|
|
1135
|
+
profiles = ownRecord(raw.storageConnections) ?? {};
|
|
1136
|
+
profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
|
|
1137
|
+
if (!profile) return;
|
|
1138
|
+
ctx.ui.notify(
|
|
1139
|
+
`Connection \u201C${safeTerminalText(profile)}\u201D saved. Cancelling setup will keep this connection.`,
|
|
1140
|
+
"info"
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const storageKind = ownRecord(profiles[profile])?.type;
|
|
1144
|
+
if (storageKind === "webdav") {
|
|
1145
|
+
const saved = await showAddWebDavTarget(ctx, name, profile, signal);
|
|
1146
|
+
if (signal?.aborted) return;
|
|
1147
|
+
if (saved) await refreshTargetCompletions();
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (storageKind === "git") {
|
|
1151
|
+
const saved = await showAddGitTarget(ctx, name, profile, signal);
|
|
1152
|
+
if (signal?.aborted) return;
|
|
1153
|
+
if (saved) await refreshTargetCompletions();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
await showAddS3Target(ctx, raw, profile, name, signal);
|
|
1157
|
+
}
|
|
1158
|
+
async function showEditSyncSetupStorage(ctx, name, signal) {
|
|
1159
|
+
const partial = await loadPartialConfig(name);
|
|
1160
|
+
if (signal?.aborted) return;
|
|
1161
|
+
if (!partial.setupName) {
|
|
1162
|
+
ctx.ui.notify("Create supported pi-sync settings before editing a named sync setup.", "info");
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (partial.storageKind === "webdav") {
|
|
1166
|
+
await showEditWebDavTarget(ctx, partial, signal);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (partial.storageKind === "git") {
|
|
1170
|
+
await showEditGitTarget(ctx, partial, signal);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
await showEditS3Target(ctx, partial, signal);
|
|
1174
|
+
}
|
|
1175
|
+
async function showRemoveTarget(ctx, name, signal) {
|
|
1176
|
+
const confirmed = await ctx.ui.confirm(
|
|
1177
|
+
"Remove sync setup?",
|
|
1178
|
+
`Remove local sync setup \u201C${safeTerminalText(name)}\u201D? Remote data and history are not deleted.`,
|
|
1179
|
+
{ signal }
|
|
1180
|
+
);
|
|
1181
|
+
if (signal?.aborted || !confirmed) return;
|
|
1182
|
+
await removeSyncSetup(name, signal);
|
|
1183
|
+
if (signal?.aborted) return;
|
|
1184
|
+
await refreshTargetCompletions();
|
|
1185
|
+
if (signal?.aborted) return;
|
|
1186
|
+
ctx.ui.notify(`Removed sync setup \u201C${safeTerminalText(name)}\u201D; remote data was not deleted.`, "info");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/ui/settings-ui.ts
|
|
1190
|
+
async function showSyncSettings(ctx, runRoute, signal) {
|
|
1191
|
+
let disposition = "back";
|
|
1192
|
+
const exitSettings = () => {
|
|
1193
|
+
disposition = "exit";
|
|
1194
|
+
return { kind: "close" };
|
|
1195
|
+
};
|
|
1196
|
+
if (ctx.mode !== "tui") {
|
|
1197
|
+
ctx.ui.notify(`Edit pi-sync settings manually: ${safeTerminalText(localConfigPath())}`, "info");
|
|
1198
|
+
if (ctx.mode === "rpc" && ctx.hasUI) {
|
|
1199
|
+
const management = defineMenu4({
|
|
1200
|
+
start: "management",
|
|
1201
|
+
screens: {
|
|
1202
|
+
management: () => ({
|
|
1203
|
+
kind: "actions",
|
|
1204
|
+
title: "Sync settings management",
|
|
1205
|
+
items: [
|
|
1206
|
+
{ id: "setups", label: "Manage sync setups", action: "setups" },
|
|
1207
|
+
{ id: "connections", label: "Manage storage connections", action: "connections" }
|
|
1208
|
+
],
|
|
1209
|
+
hint: "back"
|
|
1210
|
+
})
|
|
1211
|
+
},
|
|
1212
|
+
actions: {
|
|
1213
|
+
setups: async ({ signal: actionSignal }) => {
|
|
1214
|
+
const result = await showSyncSetupManager(ctx, runRoute, actionSignal);
|
|
1215
|
+
return result === "exit" ? exitSettings() : { kind: "stay" };
|
|
1216
|
+
},
|
|
1217
|
+
connections: async ({ signal: actionSignal }) => {
|
|
1218
|
+
await showStorageConnections(ctx, actionSignal);
|
|
1219
|
+
return { kind: "stay" };
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
await runMenu4(ctx, management, { getState: () => void 0, signal, isCurrent: () => !signal?.aborted });
|
|
1224
|
+
}
|
|
1225
|
+
return disposition;
|
|
1226
|
+
}
|
|
1227
|
+
const initial = await loadConfig();
|
|
1228
|
+
if (signal?.aborted) return "back";
|
|
1229
|
+
const setupName = initial.setupName;
|
|
1230
|
+
const menu = defineMenu4({
|
|
1231
|
+
start: "settings",
|
|
1232
|
+
screens: {
|
|
1233
|
+
settings: ({ state }) => ({
|
|
1234
|
+
kind: "settings",
|
|
1235
|
+
title: "Pi Sync Settings",
|
|
1236
|
+
lines: [
|
|
1237
|
+
`Sync setup: ${safeTerminalText(state.setupName)} \xB7 Storage connection: ${safeTerminalText(state.connectionName)}`
|
|
1238
|
+
],
|
|
1239
|
+
// Flat, searchable task ordering: Content, Destination, Automation, Merge, Other.
|
|
1240
|
+
// Kit has no heading rows; avoid focusable fake headings or a new component.
|
|
1241
|
+
items: [
|
|
1242
|
+
{
|
|
1243
|
+
id: "include",
|
|
1244
|
+
label: "Included content",
|
|
1245
|
+
description: `${state.include.length} selected path${state.include.length === 1 ? "" : "s"}. Choose which paths this setup syncs.`,
|
|
1246
|
+
currentValue: "Open editor",
|
|
1247
|
+
action: "include"
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
id: "remoteInclude",
|
|
1251
|
+
label: "Compare synced content",
|
|
1252
|
+
description: "Review this device and remote content lists before choosing either one.",
|
|
1253
|
+
currentValue: "Review",
|
|
1254
|
+
action: "remote-include"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
id: "localFields",
|
|
1258
|
+
label: "Machine-local settings fields",
|
|
1259
|
+
description: "Root field names omitted from future portable snapshots. Policy changes require directional migration; old history remains.",
|
|
1260
|
+
currentValue: `${state.localFields?.length ?? 0} fields \xB7 Edit`,
|
|
1261
|
+
action: "local-fields"
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
id: "storageLocation",
|
|
1265
|
+
label: "Storage location",
|
|
1266
|
+
description: "Destination: edit this setup's bucket, branch, or path; remote history is not moved.",
|
|
1267
|
+
currentValue: state.backend.type === "s3" ? `${safeTerminalText(state.backend.destination.bucket)} \xB7 ${safeTerminalText(state.backend.destination.prefix)}` : state.backend.type === "git" ? `${safeTerminalText(state.backend.destination.branch)} \xB7 ${safeTerminalText(state.backend.destination.directory)}` : safeTerminalText(state.backend.destination.path),
|
|
1268
|
+
action: "storage-location"
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
id: "setups",
|
|
1272
|
+
label: "Manage sync setups",
|
|
1273
|
+
description: "Destination: add, switch, edit, or remove sync setups.",
|
|
1274
|
+
currentValue: "Open",
|
|
1275
|
+
action: "setups"
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
id: "connections",
|
|
1279
|
+
label: "Manage storage connections",
|
|
1280
|
+
description: "Destination: manage shared server addresses and credentials; edits may affect multiple setups.",
|
|
1281
|
+
currentValue: "Open",
|
|
1282
|
+
action: "connections"
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
id: "automatic",
|
|
1286
|
+
label: "Automatic sync",
|
|
1287
|
+
description: AUTOMATIC_SYNC_DESCRIPTION,
|
|
1288
|
+
currentValue: state.automatic ? "On" : "Off",
|
|
1289
|
+
values: ["On", "Off"],
|
|
1290
|
+
action: "automatic"
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
id: "automaticTransfer",
|
|
1294
|
+
label: "Automatic transfer at startup",
|
|
1295
|
+
description: "May upload, replace, or delete selected files once at idle startup in TUI/RPC. Requires a baseline and conditional/lease publication; never reloads resources. Turning off cancels pending work.",
|
|
1296
|
+
currentValue: state.automaticTransfer ? "On" : "Off",
|
|
1297
|
+
values: ["On", "Off"],
|
|
1298
|
+
action: "automatic-transfer"
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
id: "onSwitch",
|
|
1302
|
+
label: "After switching setup (all setups)",
|
|
1303
|
+
description: "All setups: ask before a pull review, open it automatically, or switch without pulling.",
|
|
1304
|
+
currentValue: setupSwitchActionLabel(state.onSwitch),
|
|
1305
|
+
values: SETUP_SWITCH_ACTION_OPTIONS.map(({ label }) => label),
|
|
1306
|
+
action: "on-switch"
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
id: "mergeSettings",
|
|
1310
|
+
label: "Settings field merge (experimental)",
|
|
1311
|
+
description: "Combine independent global settings.json fields using a verified private ancestor; arrays and nested objects remain atomic. No reload.",
|
|
1312
|
+
currentValue: state.mergeSettings ? "On" : "Off",
|
|
1313
|
+
values: ["On", "Off"],
|
|
1314
|
+
action: "merge-settings"
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
id: "contentPolicy",
|
|
1318
|
+
label: "Content / partial sync (experimental)",
|
|
1319
|
+
description: "Version-5 opt-in: bounded text and prefix-only sessions; partial progress keeps full withheld versions and old baseline hashes.",
|
|
1320
|
+
currentValue: state.mergeContent ? state.partialSync ? "Content & partial" : "Content only" : state.partialSync ? "Partial only" : "Off",
|
|
1321
|
+
values: ["Off", "Content only", "Content & partial", "Partial only"],
|
|
1322
|
+
action: "content-policy"
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
id: "skipSecretScan",
|
|
1326
|
+
label: "Skip secret scan (all setups)",
|
|
1327
|
+
description: "All setups: allow pushes without checking managed local files for possible secrets.",
|
|
1328
|
+
currentValue: state.skipSecretScan ? "On" : "Off",
|
|
1329
|
+
values: ["On", "Off"],
|
|
1330
|
+
action: "skip-secret-scan"
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
id: "showStatus",
|
|
1334
|
+
label: "Show status (all setups)",
|
|
1335
|
+
description: "All setups: show sync progress and attention in Pi status.",
|
|
1336
|
+
currentValue: state.showStatus ? "On" : "Off",
|
|
1337
|
+
values: ["On", "Off"],
|
|
1338
|
+
action: "show-status"
|
|
1339
|
+
}
|
|
1340
|
+
]
|
|
1341
|
+
})
|
|
1342
|
+
},
|
|
1343
|
+
actions: {
|
|
1344
|
+
"storage-location": async ({ signal: actionSignal }) => {
|
|
1345
|
+
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1346
|
+
await showEditSyncSetupStorage(ctx, setupName, editorSignal);
|
|
1347
|
+
return resumeSettings(editorSignal);
|
|
1348
|
+
},
|
|
1349
|
+
setups: async ({ signal: actionSignal }) => {
|
|
1350
|
+
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1351
|
+
const result = await showSyncSetupManager(ctx, runRoute, editorSignal);
|
|
1352
|
+
return result === "exit" ? exitSettings() : resumeSettings(editorSignal);
|
|
1353
|
+
},
|
|
1354
|
+
connections: async ({ signal: actionSignal }) => {
|
|
1355
|
+
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1356
|
+
await showStorageConnections(ctx, editorSignal);
|
|
1357
|
+
return resumeSettings(editorSignal);
|
|
1358
|
+
},
|
|
1359
|
+
"content-policy": async ({ value, signal: actionSignal }) => {
|
|
1360
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1361
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
1362
|
+
try {
|
|
1363
|
+
const previous = await loadConfig(setupName);
|
|
1364
|
+
validate();
|
|
1365
|
+
const mergeContent = value === "Content only" || value === "Content & partial";
|
|
1366
|
+
const partialSync = value === "Partial only" || value === "Content & partial";
|
|
1367
|
+
if (!await ctx.ui.confirm(
|
|
1368
|
+
"Save experimental content policy?",
|
|
1369
|
+
"This explicitly upgrades settings to version 5; partial snapshots use version 3 and older clients must refuse them. Text/session merge is conservative. Partial sync preserves withheld versions and old baselines. Disabling does not clear unresolved groups or recovery evidence. Review a directional migration first if portable policy changes.",
|
|
1370
|
+
{ signal: mutationSignal }
|
|
1371
|
+
))
|
|
1372
|
+
return { kind: "rejected" };
|
|
1373
|
+
validate();
|
|
1374
|
+
await updateLocalConfig((current) => {
|
|
1375
|
+
validate();
|
|
1376
|
+
const setup = current.syncSetups[setupName];
|
|
1377
|
+
if (!setup || Boolean(setup.sync.mergeContent) !== Boolean(previous.mergeContent) || Boolean(setup.sync.partialSync) !== Boolean(previous.partialSync))
|
|
1378
|
+
throw new Error("Content policy changed during review.");
|
|
1379
|
+
return {
|
|
1380
|
+
...current,
|
|
1381
|
+
version: 5,
|
|
1382
|
+
syncSetups: {
|
|
1383
|
+
...current.syncSetups,
|
|
1384
|
+
[setupName]: { ...setup, sync: { ...setup.sync, mergeContent, partialSync } }
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
}, mutationSignal);
|
|
1388
|
+
validate();
|
|
1389
|
+
return { kind: "stay" };
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1392
|
+
return { kind: "rejected" };
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
"merge-settings": async ({ value, signal: actionSignal }) => {
|
|
1396
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1397
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
1398
|
+
try {
|
|
1399
|
+
validate();
|
|
1400
|
+
await updateSyncSetup(
|
|
1401
|
+
setupName,
|
|
1402
|
+
(setup) => ({ ...setup, sync: { ...setup.sync, mergeSettings: value === "On" } }),
|
|
1403
|
+
{ signal: mutationSignal }
|
|
1404
|
+
);
|
|
1405
|
+
validate();
|
|
1406
|
+
return { kind: "stay" };
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1409
|
+
return { kind: "rejected" };
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
"local-fields": async ({ signal: actionSignal }) => {
|
|
1413
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1414
|
+
const validate = captureMutationOwner(ctx, mutationSignal);
|
|
1415
|
+
try {
|
|
1416
|
+
const previous = await loadConfig(setupName);
|
|
1417
|
+
validate();
|
|
1418
|
+
const input = await ctx.ui.input(
|
|
1419
|
+
"Machine-local settings.json root fields (JSON string array)",
|
|
1420
|
+
JSON.stringify(previous.localFields ?? []),
|
|
1421
|
+
{ signal: mutationSignal }
|
|
1422
|
+
);
|
|
1423
|
+
validate();
|
|
1424
|
+
if (input === void 0) return { kind: "rejected" };
|
|
1425
|
+
let fields;
|
|
1426
|
+
try {
|
|
1427
|
+
fields = normalizeLocalFields(JSON.parse(input));
|
|
1428
|
+
} catch {
|
|
1429
|
+
throw new Error("Invalid localFields JSON array; no policy was changed.");
|
|
1430
|
+
}
|
|
1431
|
+
if (sameLocalFields(previous.localFields, fields)) return { kind: "stay" };
|
|
1432
|
+
const confirmed = await ctx.ui.confirm(
|
|
1433
|
+
"Save portable field policy?",
|
|
1434
|
+
"This opts into settings/snapshot version 4/2; older clients must refuse them. Future snapshots omit these fields, but old remote history is NOT erased. Review an explicit force push/pull migration before further sync. Removed rules can expose or replace local-only values.",
|
|
1435
|
+
{ signal: mutationSignal }
|
|
1436
|
+
);
|
|
1437
|
+
validate();
|
|
1438
|
+
if (!confirmed) return { kind: "rejected" };
|
|
1439
|
+
await updateLocalConfig((current) => {
|
|
1440
|
+
validate();
|
|
1441
|
+
const setup = current.syncSetups[setupName];
|
|
1442
|
+
if (!setup || !sameLocalFields(setup.sync.localFields, previous.localFields))
|
|
1443
|
+
throw new Error("Field policy changed while under review; reopen settings.");
|
|
1444
|
+
return {
|
|
1445
|
+
...current,
|
|
1446
|
+
version: current.version === 5 ? 5 : 4,
|
|
1447
|
+
syncSetups: {
|
|
1448
|
+
...current.syncSetups,
|
|
1449
|
+
[setupName]: { ...setup, sync: { ...setup.sync, localFields: fields } }
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}, mutationSignal);
|
|
1453
|
+
validate();
|
|
1454
|
+
return { kind: "stay" };
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1457
|
+
return { kind: "rejected" };
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
automatic: async ({ value, signal: actionSignal }) => {
|
|
1461
|
+
const automatic = value === "On";
|
|
1462
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1463
|
+
try {
|
|
1464
|
+
const latest = await loadConfig(setupName);
|
|
1465
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1466
|
+
if (latest.automatic === automatic) return { kind: "stay" };
|
|
1467
|
+
await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automatic } }), {
|
|
1468
|
+
signal: mutationSignal
|
|
1469
|
+
});
|
|
1470
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1471
|
+
ctx.ui.notify(
|
|
1472
|
+
`Automatic sync ${automatic ? "enabled" : "disabled"} for \u201C${safeTerminalText(setupName)}\u201D.`,
|
|
1473
|
+
"info"
|
|
1474
|
+
);
|
|
1475
|
+
return { kind: "stay" };
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1478
|
+
return { kind: "rejected" };
|
|
1479
|
+
}
|
|
1378
1480
|
},
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
async function showSyncSetupManager(ctx, runRoute, signal) {
|
|
1397
|
-
return showSyncSetups(
|
|
1398
|
-
ctx,
|
|
1399
|
-
{
|
|
1400
|
-
add: async (setupSignal) => {
|
|
1401
|
-
await showAddTarget(ctx, setupSignal);
|
|
1481
|
+
"automatic-transfer": async ({ value, signal: actionSignal }) => {
|
|
1482
|
+
const automaticTransfer = value === "On";
|
|
1483
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1484
|
+
try {
|
|
1485
|
+
await updateSyncSetup(setupName, (setup) => ({ ...setup, sync: { ...setup.sync, automaticTransfer } }), {
|
|
1486
|
+
signal: mutationSignal
|
|
1487
|
+
});
|
|
1488
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1489
|
+
ctx.ui.notify(
|
|
1490
|
+
`Automatic startup transfer ${automaticTransfer ? "enabled for the next session start" : "disabled"}. Completed transfers are not undone.`,
|
|
1491
|
+
"info"
|
|
1492
|
+
);
|
|
1493
|
+
return { kind: "stay" };
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1496
|
+
return { kind: "rejected" };
|
|
1497
|
+
}
|
|
1402
1498
|
},
|
|
1403
|
-
|
|
1404
|
-
|
|
1499
|
+
"skip-secret-scan": async ({ value, signal: actionSignal }) => {
|
|
1500
|
+
const skipSecretScan = value === "On";
|
|
1501
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1502
|
+
try {
|
|
1503
|
+
const latest = await loadConfig(setupName);
|
|
1504
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1505
|
+
if (latest.skipSecretScan === skipSecretScan) return { kind: "stay" };
|
|
1506
|
+
await updateLocalConfig((settings) => ({ ...settings, skipSecretScan }), mutationSignal);
|
|
1507
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1508
|
+
ctx.ui.notify(`Secret scan ${skipSecretScan ? "disabled" : "enabled"} for pushes in all setups.`, "info");
|
|
1509
|
+
return { kind: "stay" };
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1512
|
+
return { kind: "rejected" };
|
|
1513
|
+
}
|
|
1405
1514
|
},
|
|
1406
|
-
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1515
|
+
"show-status": async ({ value, signal: actionSignal }) => {
|
|
1516
|
+
const showStatus = value === "On";
|
|
1517
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1518
|
+
try {
|
|
1519
|
+
const latest = await loadConfig(setupName);
|
|
1520
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1521
|
+
if (latest.showStatus === showStatus) return { kind: "stay" };
|
|
1522
|
+
await updateLocalConfig((settings) => ({ ...settings, showStatus }), mutationSignal);
|
|
1523
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1524
|
+
configureSyncStatus(ctx, showStatus);
|
|
1525
|
+
ctx.ui.notify(`Pi Sync status ${showStatus ? "enabled" : "disabled"} for all setups.`, "info");
|
|
1526
|
+
return { kind: "stay" };
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1529
|
+
return { kind: "rejected" };
|
|
1530
|
+
}
|
|
1409
1531
|
},
|
|
1410
|
-
|
|
1411
|
-
|
|
1532
|
+
"on-switch": async ({ value, signal: actionSignal }) => {
|
|
1533
|
+
const action = value ? setupSwitchActionFromLabel(value) : void 0;
|
|
1534
|
+
if (!action) return { kind: "rejected" };
|
|
1535
|
+
const mutationSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1536
|
+
try {
|
|
1537
|
+
const latest = await loadConfig(setupName);
|
|
1538
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1539
|
+
if (latest.onSwitch === action) return { kind: "stay" };
|
|
1540
|
+
await saveOnSwitch(action, mutationSignal);
|
|
1541
|
+
if (mutationSignal.aborted) return { kind: "rejected" };
|
|
1542
|
+
ctx.ui.notify(`After switching setup: ${value}.`, "info");
|
|
1543
|
+
return { kind: "stay" };
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (!mutationSignal.aborted) notifySaveFailure(ctx, error);
|
|
1546
|
+
return { kind: "rejected" };
|
|
1547
|
+
}
|
|
1548
|
+
},
|
|
1549
|
+
include: async ({ signal: actionSignal }) => {
|
|
1550
|
+
const editorSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1551
|
+
await runRoute("files", editorSignal, void 0, setupName);
|
|
1552
|
+
return editorSignal.aborted ? { kind: "rejected" } : { kind: "stay" };
|
|
1553
|
+
},
|
|
1554
|
+
"remote-include": async ({ signal: actionSignal }) => {
|
|
1555
|
+
const reviewSignal = signal ? AbortSignal.any([signal, actionSignal]) : actionSignal;
|
|
1556
|
+
const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
|
|
1557
|
+
if (reviewSignal.aborted) return { kind: "rejected" };
|
|
1558
|
+
const review = await showRemoteSelectionReview(ctx, setupName, reviewSignal, void 0, {
|
|
1559
|
+
origin: "settings",
|
|
1560
|
+
runRoute
|
|
1561
|
+
});
|
|
1562
|
+
if (reviewSignal.aborted) return { kind: "rejected" };
|
|
1563
|
+
if (review.kind === "route-result") {
|
|
1564
|
+
const disposition2 = await dispatchManagerResult(ctx, review.result, review.route, runRoute, reviewSignal);
|
|
1565
|
+
return disposition2.kind === "close" ? exitSettings() : { kind: "stay" };
|
|
1566
|
+
}
|
|
1567
|
+
return review.kind === "closed" || review.kind === "stale" ? exitSettings() : { kind: "stay" };
|
|
1412
1568
|
}
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
)
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
let profiles = ownRecord(raw.storageConnections) ?? {};
|
|
1426
|
-
const name = await promptResourceName(ctx, "sync setup", "work", signal);
|
|
1427
|
-
if (!name) return;
|
|
1428
|
-
const createConnection = "Add a new storage connection\u2026";
|
|
1429
|
-
let profile = await ctx.ui.select(
|
|
1430
|
-
"Choose a storage connection\n\nReuse a server address and sign-in details. A new connection is saved separately and remains if you cancel this setup.",
|
|
1431
|
-
[...Object.keys(profiles).sort(), createConnection, "Cancel"],
|
|
1432
|
-
{ signal }
|
|
1433
|
-
);
|
|
1434
|
-
if (signal?.aborted || !profile || profile === "Cancel") return;
|
|
1435
|
-
if (profile === createConnection) {
|
|
1436
|
-
const previousNames = new Set(Object.keys(profiles));
|
|
1437
|
-
if (!await showAddStorageConnection(ctx, signal)) return;
|
|
1438
|
-
if (signal?.aborted) return;
|
|
1439
|
-
raw = await readLocalConfigObject() ?? raw;
|
|
1440
|
-
if (signal?.aborted) return;
|
|
1441
|
-
profiles = ownRecord(raw.storageConnections) ?? {};
|
|
1442
|
-
profile = Object.keys(profiles).find((candidate) => !previousNames.has(candidate));
|
|
1443
|
-
if (!profile) return;
|
|
1444
|
-
ctx.ui.notify(
|
|
1445
|
-
`Connection \u201C${safeTerminalText(profile)}\u201D saved. Cancelling setup will keep this connection.`,
|
|
1446
|
-
"info"
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
const storageKind = ownRecord(profiles[profile])?.type;
|
|
1450
|
-
if (storageKind === "webdav") {
|
|
1451
|
-
const saved = await showAddWebDavTarget(ctx, name, profile, signal);
|
|
1452
|
-
if (signal?.aborted) return;
|
|
1453
|
-
if (saved) await refreshTargetCompletions();
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
if (storageKind === "git") {
|
|
1457
|
-
const saved = await showAddGitTarget(ctx, name, profile, signal);
|
|
1458
|
-
if (signal?.aborted) return;
|
|
1459
|
-
if (saved) await refreshTargetCompletions();
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
await showAddS3Target(ctx, raw, profile, name, signal);
|
|
1463
|
-
}
|
|
1464
|
-
async function showEditTarget(ctx, name, signal) {
|
|
1465
|
-
const partial = await loadPartialConfig(name);
|
|
1466
|
-
if (signal?.aborted) return;
|
|
1467
|
-
if (!partial.setupName) {
|
|
1468
|
-
ctx.ui.notify("Create supported pi-sync settings before editing a named sync setup.", "info");
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
if (partial.storageKind === "webdav") {
|
|
1472
|
-
await showEditWebDavTarget(ctx, partial, signal);
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
if (partial.storageKind === "git") {
|
|
1476
|
-
await showEditGitTarget(ctx, partial, signal);
|
|
1477
|
-
return;
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
async function resumeSettings(editorSignal) {
|
|
1572
|
+
if (editorSignal.aborted) return { kind: "close" };
|
|
1573
|
+
try {
|
|
1574
|
+
const active = await loadConfig();
|
|
1575
|
+
if (editorSignal.aborted || active.setupName !== setupName) return { kind: "close" };
|
|
1576
|
+
return { kind: "stay" };
|
|
1577
|
+
} catch {
|
|
1578
|
+
return { kind: "close" };
|
|
1579
|
+
}
|
|
1478
1580
|
}
|
|
1479
|
-
await
|
|
1581
|
+
await runMenu4(ctx, menu, {
|
|
1582
|
+
getState: () => loadConfig(setupName),
|
|
1583
|
+
signal,
|
|
1584
|
+
isCurrent: () => !signal?.aborted
|
|
1585
|
+
});
|
|
1586
|
+
return disposition;
|
|
1480
1587
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
{ signal }
|
|
1588
|
+
function notifySaveFailure(ctx, error) {
|
|
1589
|
+
ctx.ui.notify(
|
|
1590
|
+
`Pi Sync settings save failed: ${safeTerminalText(error instanceof Error ? error.message : String(error))}`,
|
|
1591
|
+
"error"
|
|
1486
1592
|
);
|
|
1487
|
-
if (signal?.aborted || !confirmed) return;
|
|
1488
|
-
await removeSyncSetup(name, signal);
|
|
1489
|
-
if (signal?.aborted) return;
|
|
1490
|
-
await refreshTargetCompletions();
|
|
1491
|
-
if (signal?.aborted) return;
|
|
1492
|
-
ctx.ui.notify(`Removed sync setup \u201C${safeTerminalText(name)}\u201D; remote data was not deleted.`, "info");
|
|
1493
1593
|
}
|
|
1494
1594
|
|
|
1495
1595
|
// src/ui/manager-ui.ts
|
|
@@ -1510,52 +1610,15 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1510
1610
|
return {
|
|
1511
1611
|
kind: "actions",
|
|
1512
1612
|
title: "Manage sync",
|
|
1513
|
-
|
|
1613
|
+
// Frequent transfers stay visible; status is advisory and Help remains a direct route.
|
|
1614
|
+
lines: [
|
|
1615
|
+
...state.manager.title.split("\n").slice(1),
|
|
1616
|
+
"Help: /sync help \xB7 Fresh status: /sync status \xB7 Differences: /sync diff"
|
|
1617
|
+
],
|
|
1514
1618
|
items: operationFirst ? [...managerItems, ...attentionItems] : [...attentionItems, ...managerItems],
|
|
1515
1619
|
hint: "close"
|
|
1516
1620
|
};
|
|
1517
|
-
}
|
|
1518
|
-
more: () => ({
|
|
1519
|
-
kind: "actions",
|
|
1520
|
-
title: "More options",
|
|
1521
|
-
items: [
|
|
1522
|
-
{ id: "pull", label: "Pull from remote\u2026", action: "pull" },
|
|
1523
|
-
{ id: "push", label: "Push to remote\u2026", action: "push" },
|
|
1524
|
-
{ id: "setups", label: "Sync setups\u2026", action: "setups" },
|
|
1525
|
-
{
|
|
1526
|
-
id: "connections",
|
|
1527
|
-
label: "Storage connections\u2026",
|
|
1528
|
-
action: "connections"
|
|
1529
|
-
},
|
|
1530
|
-
{
|
|
1531
|
-
id: "doctor",
|
|
1532
|
-
label: "Check setup",
|
|
1533
|
-
description: "Check current setup access. WebDAV also probes writes and repairs history.",
|
|
1534
|
-
action: "doctor"
|
|
1535
|
-
},
|
|
1536
|
-
{ id: "recovery", label: "History & recovery\u2026", to: "recovery" },
|
|
1537
|
-
{ id: "help", label: "Help", action: "help" },
|
|
1538
|
-
{ id: "back", label: "Back", action: "back" }
|
|
1539
|
-
],
|
|
1540
|
-
hint: "back"
|
|
1541
|
-
}),
|
|
1542
|
-
recovery: ({ state }) => ({
|
|
1543
|
-
kind: "actions",
|
|
1544
|
-
title: "History & recovery",
|
|
1545
|
-
items: [
|
|
1546
|
-
{ id: "history", label: "Browse history", action: "history" },
|
|
1547
|
-
{ id: "conflicts", label: "Review unresolved conflicts", action: "conflicts" },
|
|
1548
|
-
{
|
|
1549
|
-
id: "doctor",
|
|
1550
|
-
label: "Check setup",
|
|
1551
|
-
description: "WebDAV also probes writes and repairs history.",
|
|
1552
|
-
action: "doctor"
|
|
1553
|
-
},
|
|
1554
|
-
...state.manager.operation && operationCanRecover(state.manager.operation) ? [{ id: "unlock", label: "Recover stale operation", action: "unlock" }] : [],
|
|
1555
|
-
{ id: "back", label: "Back", action: "back" }
|
|
1556
|
-
],
|
|
1557
|
-
hint: "back"
|
|
1558
|
-
})
|
|
1621
|
+
}
|
|
1559
1622
|
},
|
|
1560
1623
|
actions: {
|
|
1561
1624
|
"review-attention": async () => {
|
|
@@ -1563,7 +1626,7 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1563
1626
|
if (!attention) {
|
|
1564
1627
|
const observation = options.getObservation?.();
|
|
1565
1628
|
if (!observation) return { kind: "stay" };
|
|
1566
|
-
const { showRemoteSelectionReview } = await import("./remote-selection-ui-
|
|
1629
|
+
const { showRemoteSelectionReview } = await import("./remote-selection-ui-6LJJ4IMQ.ts");
|
|
1567
1630
|
if (sessionSignal?.aborted) return { kind: "close" };
|
|
1568
1631
|
const config = await loadConfig();
|
|
1569
1632
|
if (sessionSignal?.aborted) return { kind: "close" };
|
|
@@ -1615,15 +1678,9 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1615
1678
|
const result = await showSetupSwitcher(ctx, runRoute, void 0, sessionSignal);
|
|
1616
1679
|
return result === "pull-attempted" || result === "closed" ? { kind: "close" } : { kind: "stay" };
|
|
1617
1680
|
},
|
|
1618
|
-
diff: async () => {
|
|
1619
|
-
const result = await runCancellableOperation(ctx, "Checking current sync setup\u2026", "diff", runRoute, {
|
|
1620
|
-
signal: sessionSignal
|
|
1621
|
-
});
|
|
1622
|
-
return result.kind === "closed" ? { kind: "close" } : { kind: "stay" };
|
|
1623
|
-
},
|
|
1624
1681
|
settings: async () => {
|
|
1625
|
-
await showSyncSettings(ctx, runRoute, sessionSignal);
|
|
1626
|
-
return { kind: "stay" };
|
|
1682
|
+
const result = await showSyncSettings(ctx, runRoute, sessionSignal);
|
|
1683
|
+
return { kind: result === "exit" ? "close" : "stay" };
|
|
1627
1684
|
},
|
|
1628
1685
|
pull: async () => {
|
|
1629
1686
|
const result = await runCancellableOperation(ctx, "Checking remote changes\u2026", "pull", runRoute, {
|
|
@@ -1652,8 +1709,13 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1652
1709
|
return { kind: "stay" };
|
|
1653
1710
|
},
|
|
1654
1711
|
history: async () => {
|
|
1655
|
-
await
|
|
1656
|
-
|
|
1712
|
+
const result = await runCancellableOperation(ctx, "Loading sync history\u2026", "history", runRoute, {
|
|
1713
|
+
signal: sessionSignal,
|
|
1714
|
+
commitAware: true,
|
|
1715
|
+
cancelAcrossDialogs: true,
|
|
1716
|
+
cancelledMessage: "History review cancelled; no files were changed."
|
|
1717
|
+
});
|
|
1718
|
+
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
1657
1719
|
},
|
|
1658
1720
|
doctor: async () => {
|
|
1659
1721
|
const result = await runCancellableOperation(ctx, "Checking setup access\u2026", "doctor", runRoute, {
|
|
@@ -1661,11 +1723,6 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1661
1723
|
});
|
|
1662
1724
|
return { kind: result.kind === "closed" ? "close" : "stay" };
|
|
1663
1725
|
},
|
|
1664
|
-
unlock: async ({ state, signal: actionSignal }) => {
|
|
1665
|
-
const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
|
|
1666
|
-
if (result === "close") return { kind: "close" };
|
|
1667
|
-
return result === "restored" ? { kind: "to", screen: "main" } : { kind: "stay" };
|
|
1668
|
-
},
|
|
1669
1726
|
recover: async ({ state, signal: actionSignal }) => {
|
|
1670
1727
|
const result = await recoverSyncAccess(ctx, state.manager, runRoute, sessionSignal, actionSignal);
|
|
1671
1728
|
return { kind: result === "close" ? "close" : "stay" };
|
|
@@ -1686,8 +1743,7 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1686
1743
|
});
|
|
1687
1744
|
const disposition = await dispatchManagerResult(ctx, result, "sync", runRoute, sessionSignal);
|
|
1688
1745
|
return disposition.kind === "close" ? { kind: "close" } : { kind: "stay" };
|
|
1689
|
-
}
|
|
1690
|
-
back: async () => ({ kind: "back" })
|
|
1746
|
+
}
|
|
1691
1747
|
}
|
|
1692
1748
|
});
|
|
1693
1749
|
await runMenu5(ctx, menu, {
|
|
@@ -1709,24 +1765,33 @@ async function showSyncManager(ctx, runRoute, sessionSignal, options = {}) {
|
|
|
1709
1765
|
});
|
|
1710
1766
|
}
|
|
1711
1767
|
function syncMainMenuItem(label) {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1768
|
+
const actions = {
|
|
1769
|
+
"Sync now": "sync",
|
|
1770
|
+
"Pull from remote\u2026": "pull",
|
|
1771
|
+
"Push to remote\u2026": "push",
|
|
1772
|
+
"Switch sync setup": "switch",
|
|
1773
|
+
Settings: "settings",
|
|
1774
|
+
History: "history",
|
|
1775
|
+
Diagnostics: "doctor",
|
|
1776
|
+
"Review unresolved conflicts": "conflicts",
|
|
1777
|
+
"Restore sync access\u2026 (recommended)": "recover",
|
|
1778
|
+
"Refresh operation status": "refresh",
|
|
1779
|
+
"Sync setups\u2026": "setups",
|
|
1780
|
+
"Storage connections\u2026": "connections",
|
|
1781
|
+
Help: "help",
|
|
1782
|
+
"Set up sync": "init",
|
|
1783
|
+
"Use existing settings": "init"
|
|
1784
|
+
};
|
|
1785
|
+
const action = actions[label];
|
|
1786
|
+
if (!action) throw new Error(`Unknown sync manager action: ${label}`);
|
|
1787
|
+
return {
|
|
1788
|
+
id: action,
|
|
1789
|
+
label,
|
|
1790
|
+
action,
|
|
1791
|
+
...action === "doctor" ? { description: "Check setup access; WebDAV also probes writes and repairs history." } : {}
|
|
1792
|
+
};
|
|
1728
1793
|
}
|
|
1729
1794
|
export {
|
|
1730
1795
|
showSyncManager
|
|
1731
1796
|
};
|
|
1732
|
-
//# sourceMappingURL=manager-ui-
|
|
1797
|
+
//# sourceMappingURL=manager-ui-IURS4WXY.ts.map
|