@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/README.md +15 -3
- package/package.json +2 -1
- package/src/config-file.ts +98 -60
- package/src/file-selection.ts +88 -73
- package/src/manager-state.ts +155 -0
- package/src/manager-ui.ts +230 -289
- package/src/settings-ui.ts +103 -145
- package/src/storage-connections-ui.ts +157 -83
- package/src/sync-setups-ui.ts +186 -99
package/src/sync-setups-ui.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
2
3
|
import { loadConfig, readLocalConfigObject } from "./config.js";
|
|
3
4
|
import { errorMessage, ownRecord, safeTerminalText } from "./manager-helpers.js";
|
|
4
5
|
import { syncIncludeSelection } from "./sync-policy.js";
|
|
5
6
|
|
|
6
|
-
const BACK = "Back";
|
|
7
|
-
|
|
8
7
|
export async function countValidSyncSetups(
|
|
9
8
|
setups: Record<string, unknown> | undefined,
|
|
10
9
|
signal?: AbortSignal,
|
|
@@ -39,115 +38,203 @@ type SyncSetupActions = {
|
|
|
39
38
|
remove(name: string, signal?: AbortSignal): Promise<void>;
|
|
40
39
|
};
|
|
41
40
|
|
|
41
|
+
interface SetupMenuState {
|
|
42
|
+
setups: Record<string, unknown>;
|
|
43
|
+
active?: string;
|
|
44
|
+
selected?: {
|
|
45
|
+
name: string;
|
|
46
|
+
detail: string[];
|
|
47
|
+
valid: boolean;
|
|
48
|
+
removeUnavailable: boolean;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export async function showSyncSetups(
|
|
43
53
|
ctx: ExtensionCommandContext,
|
|
44
54
|
actions: SyncSetupActions,
|
|
45
55
|
signal?: AbortSignal,
|
|
46
56
|
): Promise<"exit" | undefined> {
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
let selectedName: string | undefined;
|
|
58
|
+
let exit = false;
|
|
59
|
+
const nameById = new Map<string, string>();
|
|
60
|
+
type Screen = "list" | "detail";
|
|
61
|
+
type Action = "add" | "select" | "make-current" | "edit" | "remove" | "back";
|
|
62
|
+
const menu = defineMenu<SetupMenuState, Screen, Action, ExtensionCommandContext>({
|
|
63
|
+
start: "list",
|
|
64
|
+
screens: {
|
|
65
|
+
list: ({ state }) => {
|
|
66
|
+
nameById.clear();
|
|
67
|
+
const names = Object.keys(state.setups).sort((left, right) => left.localeCompare(right));
|
|
68
|
+
return {
|
|
69
|
+
kind: "actions",
|
|
70
|
+
title: "Sync setups",
|
|
71
|
+
items: [
|
|
72
|
+
{ id: "add", label: "Add sync setup", action: "add" },
|
|
73
|
+
...names.map((name, index) => {
|
|
74
|
+
const id = `setup:${index}`;
|
|
75
|
+
nameById.set(id, name);
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
label: `${safeTerminalText(name)}${name === state.active ? " (current)" : ""}`,
|
|
79
|
+
action: "select" as const,
|
|
80
|
+
};
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
hint: "back",
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
detail: ({ state }) => ({
|
|
87
|
+
kind: "actions",
|
|
88
|
+
title: state.selected
|
|
89
|
+
? `Sync setup “${safeTerminalText(state.selected.name)}”`
|
|
90
|
+
: "Sync setup",
|
|
91
|
+
lines: state.selected?.detail ?? ["This sync setup no longer exists."],
|
|
92
|
+
items: state.selected
|
|
93
|
+
? [
|
|
94
|
+
...(!state.selected.name ||
|
|
95
|
+
state.selected.name === state.active ||
|
|
96
|
+
!state.selected.valid
|
|
97
|
+
? []
|
|
98
|
+
: [
|
|
99
|
+
{
|
|
100
|
+
id: "make-current",
|
|
101
|
+
label: "Make current…",
|
|
102
|
+
action: "make-current" as const,
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
{ id: "edit", label: "Edit sync setup…", action: "edit" },
|
|
106
|
+
...(state.selected.removeUnavailable
|
|
107
|
+
? []
|
|
108
|
+
: [
|
|
109
|
+
{
|
|
110
|
+
id: "remove",
|
|
111
|
+
label: "Remove sync setup…",
|
|
112
|
+
action: "remove" as const,
|
|
113
|
+
},
|
|
114
|
+
]),
|
|
115
|
+
{ id: "back", label: "Back", action: "back" },
|
|
116
|
+
]
|
|
117
|
+
: [{ id: "back", label: "Back", action: "back" }],
|
|
118
|
+
hint: "back",
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
actions: {
|
|
122
|
+
add: async () => {
|
|
123
|
+
try {
|
|
124
|
+
await actions.add(signal);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!signal?.aborted) {
|
|
127
|
+
ctx.ui.notify(
|
|
128
|
+
`Sync setup was not added: ${menuErrorMessage(error)} Retry from Add sync setup.`,
|
|
129
|
+
"error",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { kind: "stay" };
|
|
134
|
+
},
|
|
135
|
+
select: async ({ itemId }) => {
|
|
136
|
+
selectedName = nameById.get(itemId);
|
|
137
|
+
return selectedName ? { kind: "to", screen: "detail" } : { kind: "rejected" };
|
|
138
|
+
},
|
|
139
|
+
"make-current": async () => {
|
|
140
|
+
if (!selectedName) return { kind: "rejected" };
|
|
141
|
+
try {
|
|
142
|
+
exit = (await actions.makeCurrent(selectedName, signal)) === "exit";
|
|
143
|
+
return exit ? { kind: "close" } : { kind: "stay" };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
notifySetupChangeError(ctx, selectedName, error, signal);
|
|
146
|
+
return { kind: "stay" };
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
edit: async () => {
|
|
150
|
+
if (!selectedName) return { kind: "rejected" };
|
|
151
|
+
try {
|
|
152
|
+
await actions.edit(selectedName, signal);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
notifySetupChangeError(ctx, selectedName, error, signal);
|
|
155
|
+
}
|
|
156
|
+
return { kind: "stay" };
|
|
157
|
+
},
|
|
158
|
+
remove: async () => {
|
|
159
|
+
if (!selectedName) return { kind: "rejected" };
|
|
160
|
+
const name = selectedName;
|
|
161
|
+
try {
|
|
162
|
+
await actions.remove(name, signal);
|
|
163
|
+
selectedName = undefined;
|
|
164
|
+
return { kind: "back" };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
notifySetupChangeError(ctx, name, error, signal);
|
|
167
|
+
return { kind: "stay" };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
back: async () => {
|
|
171
|
+
selectedName = undefined;
|
|
172
|
+
return { kind: "back" };
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
await runMenu(ctx, menu, {
|
|
177
|
+
getState: async () => loadSetupMenuState(selectedName, signal),
|
|
178
|
+
signal,
|
|
179
|
+
isCurrent: () => !signal?.aborted,
|
|
180
|
+
});
|
|
181
|
+
return exit ? "exit" : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function loadSetupMenuState(
|
|
185
|
+
selectedName: string | undefined,
|
|
186
|
+
signal?: AbortSignal,
|
|
187
|
+
): Promise<SetupMenuState> {
|
|
188
|
+
const raw = await readLocalConfigObject();
|
|
189
|
+
throwIfAborted(signal);
|
|
190
|
+
const setups = ownRecord(raw?.syncSetups) ?? {};
|
|
191
|
+
const active = typeof raw?.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
|
|
192
|
+
if (!selectedName || !ownRecord(setups[selectedName])) return { setups, active };
|
|
193
|
+
const setupCount = Object.keys(setups).length;
|
|
194
|
+
const isCurrent = selectedName === active;
|
|
195
|
+
let detail: string[];
|
|
196
|
+
let valid = true;
|
|
197
|
+
try {
|
|
198
|
+
const config = await loadConfig(selectedName);
|
|
49
199
|
throwIfAborted(signal);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (signal?.aborted) return;
|
|
68
|
-
ctx.ui.notify(
|
|
69
|
-
`Sync setup was not added: ${menuErrorMessage(error)} Retry from Add sync setup.`,
|
|
70
|
-
"error",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const name = labels.get(selected);
|
|
76
|
-
if (!name) continue;
|
|
77
|
-
const result = await showSyncSetupDetail(ctx, name, actions, signal);
|
|
78
|
-
if (result === "exit") return "exit";
|
|
200
|
+
const selection = syncIncludeSelection(config.include);
|
|
201
|
+
detail = [
|
|
202
|
+
`Status: ${isCurrent ? "Current" : "Not current"}`,
|
|
203
|
+
`Storage connection: ${safeTerminalText(config.connectionName)}`,
|
|
204
|
+
`Endpoint: ${storageEndpoint(config)}`,
|
|
205
|
+
`Storage location: ${storageLocation(config)}`,
|
|
206
|
+
`Included content: ${selection.builtIns.length} built-in groups · ${selection.custom.length} extra paths`,
|
|
207
|
+
`Sessions: ${selection.sessions ? "On — privacy-sensitive" : "Off"}`,
|
|
208
|
+
`Automatic sync: ${config.automatic ? "On" : "Off"}`,
|
|
209
|
+
];
|
|
210
|
+
} catch (error) {
|
|
211
|
+
valid = false;
|
|
212
|
+
detail = [
|
|
213
|
+
`Status: Invalid${isCurrent ? " current setup" : ""}`,
|
|
214
|
+
`Reason: ${menuErrorMessage(error)}`,
|
|
215
|
+
"Make current and sync are unavailable until this setup is repaired.",
|
|
216
|
+
];
|
|
79
217
|
}
|
|
218
|
+
const removeUnavailable = isCurrent && setupCount > 1;
|
|
219
|
+
if (removeUnavailable) detail.push("Remove unavailable: switch to another setup first.");
|
|
220
|
+
return {
|
|
221
|
+
setups,
|
|
222
|
+
active,
|
|
223
|
+
selected: { name: selectedName, detail, valid, removeUnavailable },
|
|
224
|
+
};
|
|
80
225
|
}
|
|
81
226
|
|
|
82
|
-
|
|
227
|
+
function notifySetupChangeError(
|
|
83
228
|
ctx: ExtensionCommandContext,
|
|
84
229
|
name: string,
|
|
85
|
-
|
|
230
|
+
error: unknown,
|
|
86
231
|
signal?: AbortSignal,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!setup) {
|
|
94
|
-
ctx.ui.notify(`Sync setup “${safeTerminalText(name)}” no longer exists.`, "warning");
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const active = typeof raw?.activeSyncSetup === "string" ? raw.activeSyncSetup : undefined;
|
|
98
|
-
const setupCount = Object.keys(setups).length;
|
|
99
|
-
const isCurrent = name === active;
|
|
100
|
-
let detail: string[];
|
|
101
|
-
let valid = true;
|
|
102
|
-
try {
|
|
103
|
-
const config = await loadConfig(name);
|
|
104
|
-
throwIfAborted(signal);
|
|
105
|
-
const selection = syncIncludeSelection(config.include);
|
|
106
|
-
detail = [
|
|
107
|
-
`Status: ${isCurrent ? "Current" : "Not current"}`,
|
|
108
|
-
`Storage connection: ${safeTerminalText(config.connectionName)}`,
|
|
109
|
-
`Endpoint: ${storageEndpoint(config)}`,
|
|
110
|
-
`Storage location: ${storageLocation(config)}`,
|
|
111
|
-
`Included content: ${selection.builtIns.length} built-in groups · ${selection.custom.length} extra paths`,
|
|
112
|
-
`Sessions: ${selection.sessions ? "On — privacy-sensitive" : "Off"}`,
|
|
113
|
-
`Automatic sync: ${config.automatic ? "On" : "Off"}`,
|
|
114
|
-
];
|
|
115
|
-
} catch (error) {
|
|
116
|
-
valid = false;
|
|
117
|
-
detail = [
|
|
118
|
-
`Status: Invalid${isCurrent ? " current setup" : ""}`,
|
|
119
|
-
`Reason: ${menuErrorMessage(error)}`,
|
|
120
|
-
"Make current and sync are unavailable until this setup is repaired.",
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
const removeUnavailable = isCurrent && setupCount > 1;
|
|
124
|
-
if (removeUnavailable) detail.push("Remove unavailable: switch to another setup first.");
|
|
125
|
-
const selected = await ctx.ui.select(
|
|
126
|
-
[`Sync setup “${safeTerminalText(name)}”`, "", ...detail].join("\n"),
|
|
127
|
-
[
|
|
128
|
-
...(!isCurrent && valid ? ["Make current…"] : []),
|
|
129
|
-
"Edit sync setup…",
|
|
130
|
-
...(removeUnavailable ? [] : ["Remove sync setup…"]),
|
|
131
|
-
BACK,
|
|
132
|
-
],
|
|
133
|
-
{ signal },
|
|
134
|
-
);
|
|
135
|
-
if (signal?.aborted || !selected || selected === BACK) return;
|
|
136
|
-
try {
|
|
137
|
-
if (selected === "Make current…") return actions.makeCurrent(name, signal);
|
|
138
|
-
if (selected === "Edit sync setup…") await actions.edit(name, signal);
|
|
139
|
-
else if (selected === "Remove sync setup…") {
|
|
140
|
-
await actions.remove(name, signal);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
} catch (error) {
|
|
144
|
-
if (signal?.aborted) return;
|
|
145
|
-
ctx.ui.notify(
|
|
146
|
-
`Sync setup “${safeTerminalText(name)}” was not changed: ${menuErrorMessage(error)} Reopen it and retry.`,
|
|
147
|
-
"error",
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
232
|
+
) {
|
|
233
|
+
if (signal?.aborted) return;
|
|
234
|
+
ctx.ui.notify(
|
|
235
|
+
`Sync setup “${safeTerminalText(name)}” was not changed: ${menuErrorMessage(error)} Reopen it and retry.`,
|
|
236
|
+
"error",
|
|
237
|
+
);
|
|
151
238
|
}
|
|
152
239
|
|
|
153
240
|
function menuErrorMessage(error: unknown) {
|