@nklisch/pi-enhanced 0.2.4 → 0.2.5
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/CHANGELOG.md +6 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-plugins/README.md +52 -22
- package/node_modules/@nklisch/pi-plugins/dist/host.d.ts +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/host.js +278 -16
- package/node_modules/@nklisch/pi-plugins/dist/host.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/index.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/index.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/index.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/commands.js +30 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/commands.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/extension.d.ts +3 -3
- package/node_modules/@nklisch/pi-plugins/dist/pi/extension.js +21 -4
- package/node_modules/@nklisch/pi-plugins/dist/pi/extension.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-model.d.ts +59 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-model.js +96 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-model.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +133 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +1251 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +26 -13
- package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/types.d.ts +72 -4
- package/node_modules/@nklisch/pi-plugins/dist/types.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { DEFAULT_REFRESH_TIMEOUT_MS } from "../host.js";
|
|
3
|
+
import { filterPluginRows, pluginIdentity, pluginManagerKeyAction, pluginManagerMarketplaceRows, projectInstalledPluginDetail, prunePluginSelection, restorePluginCursor, } from "./plugin-manager-model.js";
|
|
4
|
+
const TAB_ORDER = ["installed", "discover", "marketplaces", "issues"];
|
|
5
|
+
const BATCH_ACTIONS = ["install", "update", "enable", "disable", "remove"];
|
|
6
|
+
function errorMessage(error) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function catalogEntry(catalog, name) {
|
|
10
|
+
return catalog?.plugins.find((item) => item.name === name);
|
|
11
|
+
}
|
|
12
|
+
function receiptVersion(plugin) {
|
|
13
|
+
const value = plugin?.receipt?.version;
|
|
14
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function identityOf(plugin) {
|
|
17
|
+
return pluginIdentity(plugin.name, plugin.marketplace);
|
|
18
|
+
}
|
|
19
|
+
function actionLabel(action) {
|
|
20
|
+
return action[0].toLocaleUpperCase() + action.slice(1);
|
|
21
|
+
}
|
|
22
|
+
function actionVerb(action) {
|
|
23
|
+
if (action === "remove")
|
|
24
|
+
return "Remove";
|
|
25
|
+
return actionLabel(action);
|
|
26
|
+
}
|
|
27
|
+
export class PluginManager {
|
|
28
|
+
host;
|
|
29
|
+
tui;
|
|
30
|
+
theme;
|
|
31
|
+
keybindings;
|
|
32
|
+
done;
|
|
33
|
+
confirm;
|
|
34
|
+
input;
|
|
35
|
+
notify;
|
|
36
|
+
searchInput = new Input();
|
|
37
|
+
_focused = false;
|
|
38
|
+
tab = "installed";
|
|
39
|
+
view = "list";
|
|
40
|
+
query = "";
|
|
41
|
+
searchFocused = false;
|
|
42
|
+
cursor = 0;
|
|
43
|
+
detailActionCursor = 0;
|
|
44
|
+
detailId;
|
|
45
|
+
selected = new Set();
|
|
46
|
+
installed = [];
|
|
47
|
+
marketplaces = [];
|
|
48
|
+
catalogs = new Map();
|
|
49
|
+
localEpoch = 0;
|
|
50
|
+
runtime;
|
|
51
|
+
issues = [];
|
|
52
|
+
issuesLoading = false;
|
|
53
|
+
marketplaceCursor = 0;
|
|
54
|
+
checkOnOpen = false;
|
|
55
|
+
checkOnOpenLoaded = false;
|
|
56
|
+
checkOnOpenSaving = false;
|
|
57
|
+
checking = false;
|
|
58
|
+
updatesChecked = false;
|
|
59
|
+
checkedMarketplaces = 0;
|
|
60
|
+
checkingMarketplaces = new Set();
|
|
61
|
+
checkController;
|
|
62
|
+
checkPromise;
|
|
63
|
+
checkRun = 0;
|
|
64
|
+
openCheckPending = true;
|
|
65
|
+
reloadNeeded = false;
|
|
66
|
+
toast;
|
|
67
|
+
toastTimer;
|
|
68
|
+
batch;
|
|
69
|
+
batchResults = [];
|
|
70
|
+
batchRunning = false;
|
|
71
|
+
batchCancelledAfterCurrent = false;
|
|
72
|
+
batchController;
|
|
73
|
+
batchItemActive = false;
|
|
74
|
+
destroyed = false;
|
|
75
|
+
completed = false;
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.host = options.host;
|
|
78
|
+
this.tui = options.tui;
|
|
79
|
+
this.theme = options.theme;
|
|
80
|
+
this.keybindings = options.keybindings;
|
|
81
|
+
this.done = options.done;
|
|
82
|
+
this.confirm = options.confirm;
|
|
83
|
+
this.input = options.input;
|
|
84
|
+
this.notify = options.notify;
|
|
85
|
+
this.searchInput.onSubmit = () => this.leaveSearch();
|
|
86
|
+
// Opening is local-first: the first render does not wait for either a
|
|
87
|
+
// catalog read or a network request. Both tasks update this view in place.
|
|
88
|
+
void this.loadLocal().catch((error) => this.showToast(`Could not load plugin data: ${errorMessage(error)}`, "warning"));
|
|
89
|
+
void this.loadCheckOnOpen();
|
|
90
|
+
}
|
|
91
|
+
get focused() {
|
|
92
|
+
return this._focused;
|
|
93
|
+
}
|
|
94
|
+
set focused(value) {
|
|
95
|
+
this._focused = value;
|
|
96
|
+
this.searchInput.focused = value && this.searchFocused;
|
|
97
|
+
}
|
|
98
|
+
render(width) {
|
|
99
|
+
const renderWidth = Math.max(1, width);
|
|
100
|
+
const lines = [];
|
|
101
|
+
lines.push(this.renderTabs(renderWidth));
|
|
102
|
+
lines.push(this.theme.fg("border", "─".repeat(renderWidth)));
|
|
103
|
+
if (this.view === "detail")
|
|
104
|
+
lines.push(...this.renderDetail(renderWidth));
|
|
105
|
+
else if (this.view === "confirm")
|
|
106
|
+
lines.push(...this.renderConfirmation(renderWidth));
|
|
107
|
+
else if (this.view === "batch")
|
|
108
|
+
lines.push(...this.renderBatch(renderWidth));
|
|
109
|
+
else if (this.tab === "installed")
|
|
110
|
+
lines.push(...this.renderInstalled(renderWidth));
|
|
111
|
+
else if (this.tab === "discover")
|
|
112
|
+
lines.push(...this.renderDiscover(renderWidth));
|
|
113
|
+
else if (this.tab === "marketplaces")
|
|
114
|
+
lines.push(...this.renderMarketplaces(renderWidth));
|
|
115
|
+
else
|
|
116
|
+
lines.push(...this.renderIssues(renderWidth));
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(this.renderFooter(renderWidth));
|
|
119
|
+
if (this.toast !== undefined)
|
|
120
|
+
lines.push(this.theme.fg("accent", truncateToWidth(` ${this.toast}`, renderWidth, "")));
|
|
121
|
+
return lines.map((line) => truncateToWidth(line, renderWidth, ""));
|
|
122
|
+
}
|
|
123
|
+
handleInput(data) {
|
|
124
|
+
if (this.destroyed)
|
|
125
|
+
return;
|
|
126
|
+
if (this.searchFocused) {
|
|
127
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
|
|
128
|
+
this.leaveSearch();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.searchInput.handleInput(data);
|
|
132
|
+
this.query = this.searchInput.getValue();
|
|
133
|
+
this.cursor = 0;
|
|
134
|
+
this.requestRender();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this.view === "batch") {
|
|
138
|
+
this.handleBatchInput(data);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (this.view === "list" && this.checking && matchesKey(data, Key.escape)) {
|
|
142
|
+
this.cancelMarketplaceCheck();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (this.view === "confirm") {
|
|
146
|
+
this.handleConfirmationInput(data);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (this.view === "detail") {
|
|
150
|
+
this.handleDetailInput(data);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (this.tab === "marketplaces") {
|
|
154
|
+
if (data.toLocaleLowerCase() === "x" && this.marketplaceCursor >= 3) {
|
|
155
|
+
const marketplace = this.marketplaces[this.marketplaceCursor - 3];
|
|
156
|
+
if (marketplace !== undefined)
|
|
157
|
+
void this.removeMarketplace(marketplace.name).catch((error) => this.showToast(`Could not remove marketplace: ${errorMessage(error)}`, "error"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (matchesKey(data, Key.space) && this.marketplaceCursor === 2) {
|
|
161
|
+
void this.toggleCheckOnOpen();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (data.toLocaleLowerCase() === "r") {
|
|
165
|
+
this.startMarketplaceCheck();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (this.tab === "issues" && data.toLocaleLowerCase() === "r") {
|
|
170
|
+
void this.loadIssues();
|
|
171
|
+
this.startMarketplaceCheck();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const action = this.managerKeyAction(data);
|
|
175
|
+
if (action === undefined)
|
|
176
|
+
return;
|
|
177
|
+
this.handleListAction(action);
|
|
178
|
+
}
|
|
179
|
+
invalidate() {
|
|
180
|
+
this.searchInput.invalidate();
|
|
181
|
+
}
|
|
182
|
+
dispose() {
|
|
183
|
+
this.destroyed = true;
|
|
184
|
+
this.checkRun++;
|
|
185
|
+
this.checkController?.abort("manager closed");
|
|
186
|
+
this.batchController?.abort("manager closed");
|
|
187
|
+
if (this.toastTimer !== undefined)
|
|
188
|
+
clearTimeout(this.toastTimer);
|
|
189
|
+
}
|
|
190
|
+
renderTabs(width) {
|
|
191
|
+
const labels = {
|
|
192
|
+
installed: `Installed (${this.installed.length})`,
|
|
193
|
+
discover: `Discover (${this.discoverRows().length})`,
|
|
194
|
+
marketplaces: "Marketplaces",
|
|
195
|
+
issues: `Issues${this.issues.length === 0 ? "" : ` (${this.issues.length})`}`,
|
|
196
|
+
};
|
|
197
|
+
const rendered = TAB_ORDER.map((tab) => {
|
|
198
|
+
const text = ` ${labels[tab]} `;
|
|
199
|
+
return tab === this.tab ? this.theme.bg("selectedBg", this.theme.fg("text", text)) : this.theme.fg("muted", text);
|
|
200
|
+
}).join(" ");
|
|
201
|
+
const status = this.statusText(width);
|
|
202
|
+
if (width < 80)
|
|
203
|
+
return truncateToWidth(rendered, width, "");
|
|
204
|
+
const gap = Math.max(1, width - visibleWidth(rendered) - visibleWidth(status));
|
|
205
|
+
return truncateToWidth(rendered + " ".repeat(gap) + status, width, "");
|
|
206
|
+
}
|
|
207
|
+
statusText(width) {
|
|
208
|
+
if (width < 80)
|
|
209
|
+
return "";
|
|
210
|
+
if (this.checking)
|
|
211
|
+
return this.theme.fg("accent", `◌ Checking ${this.checkedMarketplaces}/${this.checkingMarketplaces.size} marketplaces · manager remains available`);
|
|
212
|
+
if (this.reloadNeeded)
|
|
213
|
+
return this.theme.fg("warning", "● Reload needed");
|
|
214
|
+
if (this.updatesChecked)
|
|
215
|
+
return this.theme.fg("muted", "Local data · updates checked just now");
|
|
216
|
+
return this.theme.fg("muted", "Local data · updates not checked");
|
|
217
|
+
}
|
|
218
|
+
renderInstalled(width) {
|
|
219
|
+
const rows = this.filteredRows(this.installedRows());
|
|
220
|
+
const lines = [this.heading(`Installed plugins (${rows.length})`), this.renderSearch(width, "Search installed plugins…")];
|
|
221
|
+
if (rows.length === 0)
|
|
222
|
+
lines.push(this.theme.fg("muted", " No installed plugins match this search."));
|
|
223
|
+
else
|
|
224
|
+
lines.push(...this.renderRows(rows, width));
|
|
225
|
+
lines.push(...this.renderBatchBar(width, "installed"));
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
renderDiscover(width) {
|
|
229
|
+
const rows = this.filteredRows(this.discoverRows());
|
|
230
|
+
const lines = [this.heading(`Discover plugins (${rows.length})`), this.renderSearch(width, "Search all marketplaces…")];
|
|
231
|
+
if (rows.length === 0)
|
|
232
|
+
lines.push(this.theme.fg("muted", " No discoverable plugins match this search."));
|
|
233
|
+
else
|
|
234
|
+
lines.push(...this.renderRows(rows, width));
|
|
235
|
+
lines.push(...this.renderBatchBar(width, "discover"));
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
renderMarketplaces(width) {
|
|
239
|
+
const lines = [this.heading("Manage marketplaces")];
|
|
240
|
+
const rows = pluginManagerMarketplaceRows(this.marketplaces, this.checkOnOpen);
|
|
241
|
+
for (const [index, row] of rows.entries()) {
|
|
242
|
+
if (row.kind === "marketplace") {
|
|
243
|
+
const marketplace = row.marketplace;
|
|
244
|
+
const status = this.marketplaceStatus(marketplace.name);
|
|
245
|
+
const installedCount = this.installed.filter((plugin) => plugin.marketplace === marketplace.name).length;
|
|
246
|
+
const availableCount = this.catalogs.get(marketplace.name)?.plugins.length;
|
|
247
|
+
lines.push(this.renderCursorLine(this.marketplaceCursor === index, this.theme.fg("accent", `○ ${marketplace.name}`)));
|
|
248
|
+
lines.push(this.indent(`${marketplace.source.value} · ${availableCount ?? "?"} available · ${installedCount} installed · ${status}`, width));
|
|
249
|
+
lines.push(this.indent(this.theme.fg("dim", "Enter refresh · x remove"), width));
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const label = row.kind === "check-on-open"
|
|
253
|
+
? `${row.enabled ? this.theme.fg("success", "●") : this.theme.fg("muted", "○")} ${row.label}${this.checkOnOpenSaving ? this.theme.fg("muted", " · saving…") : ""}`
|
|
254
|
+
: this.theme.fg("accent", row.label);
|
|
255
|
+
lines.push(this.renderCursorLine(this.marketplaceCursor === index, label));
|
|
256
|
+
}
|
|
257
|
+
if (this.marketplaces.length === 0)
|
|
258
|
+
lines.push(this.theme.fg("muted", " No marketplaces. Add one to discover plugins."));
|
|
259
|
+
lines.push(this.indent(this.theme.fg("muted", "The manager opens from local data, then refreshes sources without blocking navigation."), width));
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
262
|
+
renderIssues(width) {
|
|
263
|
+
const lines = [this.heading(`Issues${this.issues.length === 0 ? "" : ` (${this.issues.length})`}`)];
|
|
264
|
+
if (this.issuesLoading)
|
|
265
|
+
lines.push(this.theme.fg("accent", " ◌ Scanning current runtime…"));
|
|
266
|
+
if (!this.issuesLoading && this.issues.length === 0)
|
|
267
|
+
lines.push(this.theme.fg("success", " No current plugin issues."));
|
|
268
|
+
for (let index = 0; index < this.issues.length; index++) {
|
|
269
|
+
const issue = this.issues[index];
|
|
270
|
+
const selected = index === this.cursor;
|
|
271
|
+
const color = issue.severity === "error" ? "error" : "warning";
|
|
272
|
+
lines.push(this.renderCursorLine(selected, this.theme.fg(color, `${issue.severity === "error" ? "×" : "△"} ${issue.title}`)));
|
|
273
|
+
lines.push(this.indent(this.theme.fg("muted", issue.message), width));
|
|
274
|
+
}
|
|
275
|
+
return lines;
|
|
276
|
+
}
|
|
277
|
+
renderDetail(width) {
|
|
278
|
+
const row = this.currentDetailRow();
|
|
279
|
+
if (row === undefined)
|
|
280
|
+
return [this.theme.fg("warning", "Plugin is no longer present."), this.theme.fg("muted", "Press Esc to return.")];
|
|
281
|
+
const installed = this.installed.find((item) => identityOf(item) === row.id);
|
|
282
|
+
const runtimePlugin = this.runtime?.plugins.find((item) => identityOf(item.info) === row.id);
|
|
283
|
+
const catalog = this.catalogs.get(row.marketplace);
|
|
284
|
+
const entry = catalogEntry(catalog, row.name);
|
|
285
|
+
const details = installed === undefined ? undefined : projectInstalledPluginDetail(installed, runtimePlugin);
|
|
286
|
+
const lines = [
|
|
287
|
+
this.theme.fg("accent", `${this.tab === "discover" ? "Discover" : "Installed"} / Plugin details`),
|
|
288
|
+
this.heading(`${row.name} @ ${row.marketplace}`),
|
|
289
|
+
this.fact("Version", row.availableVersion === undefined
|
|
290
|
+
? receiptVersion(installed) ?? row.version ?? "not declared"
|
|
291
|
+
: `${receiptVersion(installed) ?? "unversioned"} → ${row.availableVersion}`),
|
|
292
|
+
this.fact("Status", installed === undefined ? "not installed" : this.statusLabel(installed, entry)),
|
|
293
|
+
this.fact("Source", entry?.source.kind === "local"
|
|
294
|
+
? entry.source.path
|
|
295
|
+
: entry === undefined
|
|
296
|
+
? row.marketplace
|
|
297
|
+
: `${entry.source.url}${entry.source.path === undefined ? "" : `#${entry.source.path}`}`),
|
|
298
|
+
"",
|
|
299
|
+
this.wrapLine(row.description ?? "No description declared.", width),
|
|
300
|
+
"",
|
|
301
|
+
];
|
|
302
|
+
if (installed !== undefined && details !== undefined) {
|
|
303
|
+
lines.push(this.fact("Install path", details.installPath));
|
|
304
|
+
lines.push(this.fact("Data path", details.dataPath));
|
|
305
|
+
lines.push(this.theme.fg("accent", "Components"));
|
|
306
|
+
if (this.runtime === undefined) {
|
|
307
|
+
lines.push(this.indent(this.theme.fg("muted", "Loading runtime components…"), width));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
lines.push(this.fact("Skills", details.skills.length === 0 ? "none" : details.skills.join(", ")));
|
|
311
|
+
lines.push(this.fact("Hooks", details.hooks.length === 0 ? "none" : details.hooks.join(", ")));
|
|
312
|
+
lines.push(this.fact("MCP servers", details.mcpServers.length === 0 ? "none" : details.mcpServers.join(", ")));
|
|
313
|
+
}
|
|
314
|
+
lines.push("");
|
|
315
|
+
const marker = installed.autoUpdate ? this.theme.fg("success", "[on]") : this.theme.fg("muted", "[off]");
|
|
316
|
+
lines.push(`${marker} ${this.theme.bold("Update automatically on Pi startup")}`);
|
|
317
|
+
lines.push(this.indent(this.theme.fg("muted", "Enabling grants standing authorization to replace executable plugin content when its declared catalog version changes."), width));
|
|
318
|
+
lines.push("");
|
|
319
|
+
}
|
|
320
|
+
lines.push(this.theme.fg("accent", "Actions"));
|
|
321
|
+
const actions = this.detailActions(row, installed);
|
|
322
|
+
for (let index = 0; index < actions.length; index++) {
|
|
323
|
+
const action = actions[index];
|
|
324
|
+
const text = action.label;
|
|
325
|
+
lines.push(this.renderCursorLine(index === this.detailActionCursor, action.destructive ? this.theme.fg("error", text) : index === 0 ? this.theme.fg("accent", text) : this.theme.fg("text", text)));
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
renderConfirmation(width) {
|
|
330
|
+
const batch = this.batch;
|
|
331
|
+
if (batch === undefined)
|
|
332
|
+
return [this.theme.fg("warning", "Nothing selected."), this.theme.fg("muted", "Press Esc to return.")];
|
|
333
|
+
const rows = batch.identities.map((identity) => this.rowById(identityToString(identity))).filter((row) => row !== undefined);
|
|
334
|
+
const executable = batch.action === "install" || batch.action === "update" || batch.action === "enable";
|
|
335
|
+
const destructive = batch.action === "remove";
|
|
336
|
+
const lines = [
|
|
337
|
+
this.theme.fg("accent", `${this.tabLabel()} / Confirm ${actionLabel(batch.action.toString()).toLocaleLowerCase()}`),
|
|
338
|
+
this.heading(`${actionVerb(batch.action)} ${rows.length} plugin${rows.length === 1 ? "" : "s"}?`),
|
|
339
|
+
this.theme.fg("text", `This batch affects ${rows.length} selected plugin${rows.length === 1 ? "" : "s"}.`),
|
|
340
|
+
];
|
|
341
|
+
for (const row of rows)
|
|
342
|
+
lines.push(this.indent(`• ${row.name} @ ${row.marketplace} · ${row.version ?? "unversioned"}`, width));
|
|
343
|
+
lines.push("");
|
|
344
|
+
if (executable) {
|
|
345
|
+
lines.push(this.theme.bg("toolPendingBg", this.theme.fg("warning", " Executable content")));
|
|
346
|
+
lines.push(this.indent(this.theme.fg("muted", "Plugin hooks and MCP servers run local code. Review the source before continuing."), width));
|
|
347
|
+
}
|
|
348
|
+
else if (destructive) {
|
|
349
|
+
lines.push(this.theme.bg("toolErrorBg", this.theme.fg("error", " Destructive change")));
|
|
350
|
+
lines.push(this.indent(this.theme.fg("muted", "Successful removals remain removed; failed items are reported without rolling back other items."), width));
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
lines.push(this.theme.bg("toolPendingBg", this.theme.fg("warning", " Runtime mutation")));
|
|
354
|
+
lines.push(this.indent(this.theme.fg("muted", "Each item runs sequentially and Pi reloads once when the manager closes."), width));
|
|
355
|
+
}
|
|
356
|
+
lines.push("");
|
|
357
|
+
lines.push(this.renderCursorLine(this.detailActionCursor === 0, this.theme.fg("accent", `${actionVerb(batch.action)} ${rows.length} plugin${rows.length === 1 ? "" : "s"}`)));
|
|
358
|
+
lines.push(this.renderCursorLine(this.detailActionCursor === 1, this.theme.fg("text", "Back to selection")));
|
|
359
|
+
return lines;
|
|
360
|
+
}
|
|
361
|
+
renderBatch(width) {
|
|
362
|
+
const batch = this.batch;
|
|
363
|
+
if (batch === undefined)
|
|
364
|
+
return [this.theme.fg("muted", "No batch in progress.")];
|
|
365
|
+
const succeeded = this.batchResults.filter((result) => result.ok).length;
|
|
366
|
+
const failed = this.batchResults.length - succeeded;
|
|
367
|
+
const lines = [
|
|
368
|
+
this.theme.fg("accent", `${this.tabLabel()} / ${actionVerb(batch.action)} selected`),
|
|
369
|
+
this.heading(this.batchRunning ? `${actionVerb(batch.action)} ${batch.identities.length} plugins` : "Batch complete"),
|
|
370
|
+
this.theme.fg("muted", `${this.batchRunning ? "Items run sequentially; the manager stays available." : "Settled items remain visible in filesystem truth."}`),
|
|
371
|
+
"",
|
|
372
|
+
];
|
|
373
|
+
for (let index = 0; index < batch.identities.length; index++) {
|
|
374
|
+
const identity = batch.identities[index];
|
|
375
|
+
const result = this.batchResults[index];
|
|
376
|
+
const prefix = result === undefined ? this.theme.fg("dim", "○") : result.ok ? this.theme.fg("success", "✓") : this.theme.fg("error", "×");
|
|
377
|
+
const note = result === undefined ? (this.batchRunning && index === this.batchResults.length ? "working…" : "waiting") : result.ok ? "complete" : result.error ?? "failed";
|
|
378
|
+
lines.push(this.indent(`${prefix} ${identity.plugin} @ ${identity.marketplace} ${this.theme.fg("muted", note)}`, width));
|
|
379
|
+
}
|
|
380
|
+
if (!this.batchRunning) {
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(this.theme.bg("toolPendingBg", this.theme.fg("text", ` ${succeeded} succeeded · ${failed} failed${this.batchCancelledAfterCurrent ? " · cancelled before next item" : ""}`)));
|
|
383
|
+
if (succeeded > 0)
|
|
384
|
+
lines.push(this.theme.fg("warning", "Reload will run once when this manager closes."));
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push(this.renderCursorLine(this.detailActionCursor === 0, this.theme.fg("accent", "View installed plugins")));
|
|
387
|
+
lines.push(this.renderCursorLine(this.detailActionCursor === 1, this.theme.fg("text", "Close manager")));
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
lines.push("");
|
|
391
|
+
lines.push(this.theme.fg("error", this.batchCancelledAfterCurrent ? "Stop after current plugin" : "Esc stop after current plugin"));
|
|
392
|
+
}
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
395
|
+
renderRows(rows, width) {
|
|
396
|
+
const lines = [];
|
|
397
|
+
for (const [index, row] of rows.entries()) {
|
|
398
|
+
const status = this.rowStatus(row);
|
|
399
|
+
const selected = this.selected.has(row.id);
|
|
400
|
+
const main = `${selected ? "◉" : "○"} ${row.name} · ${row.marketplace} · ${row.version ?? "unversioned"}`;
|
|
401
|
+
const suffix = row.installed && this.tab === "discover" ? ` · ${this.theme.fg("success", "installed")}` : status;
|
|
402
|
+
const title = this.theme.fg("accent", main);
|
|
403
|
+
const cursor = index === this.cursor ? this.theme.fg("accent", "›") : " ";
|
|
404
|
+
const content = `${cursor} ${title}${suffix.length > 0 ? ` ${suffix}` : ""}`;
|
|
405
|
+
lines.push(this.theme.bg(index === this.cursor ? "selectedBg" : "toolPendingBg", truncateToWidth(content, width, "")));
|
|
406
|
+
lines.push(this.indent(this.theme.fg("muted", row.description ?? "No description declared."), width));
|
|
407
|
+
}
|
|
408
|
+
return lines;
|
|
409
|
+
}
|
|
410
|
+
renderBatchBar(width, mode) {
|
|
411
|
+
if (this.selected.size === 0)
|
|
412
|
+
return [];
|
|
413
|
+
const actions = mode === "discover" ? "i Install selected" : "u Update · e Enable · d Disable · x Remove";
|
|
414
|
+
return [
|
|
415
|
+
this.theme.fg("border", "─".repeat(Math.max(1, width))),
|
|
416
|
+
`${this.theme.fg("accent", `${this.selected.size} selected`)} ${this.theme.fg("text", actions)} ${this.theme.fg("muted", "Esc clear")}`,
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
renderFooter(width) {
|
|
420
|
+
let footer;
|
|
421
|
+
if (this.view === "detail" || this.view === "confirm")
|
|
422
|
+
footer = "↑↓ navigate · Enter run · Esc back";
|
|
423
|
+
else if (this.view === "batch")
|
|
424
|
+
footer = this.batchRunning ? "Esc stop after current · manager stays open" : "Enter view installed · Esc close";
|
|
425
|
+
else if (this.checking)
|
|
426
|
+
footer = "Esc cancel checks · navigation remains available";
|
|
427
|
+
else
|
|
428
|
+
footer = "Ctrl+←/→ tabs · Ctrl+F search · ↑↓ navigate · Space select · a all · Enter details · r check · Esc close";
|
|
429
|
+
if (width < 80) {
|
|
430
|
+
const compact = this.checking ? "◌ checking · Esc cancel" : this.reloadNeeded ? "● reload needed" : "";
|
|
431
|
+
footer = compact.length > 0 ? compact : footer;
|
|
432
|
+
}
|
|
433
|
+
return this.theme.fg("dim", footer);
|
|
434
|
+
}
|
|
435
|
+
heading(text) {
|
|
436
|
+
return this.theme.fg("accent", this.theme.bold(text));
|
|
437
|
+
}
|
|
438
|
+
fact(label, value) {
|
|
439
|
+
return `${this.theme.fg("muted", label.padEnd(10))} ${value}`;
|
|
440
|
+
}
|
|
441
|
+
indent(text, width) {
|
|
442
|
+
return truncateToWidth(` ${text}`, width, "");
|
|
443
|
+
}
|
|
444
|
+
renderCursorLine(cursor, text) {
|
|
445
|
+
return `${cursor ? this.theme.fg("accent", "›") : " "} ${text}`;
|
|
446
|
+
}
|
|
447
|
+
renderSearch(width, placeholder) {
|
|
448
|
+
this.searchInput.focused = this._focused && this.searchFocused;
|
|
449
|
+
const text = this.searchInput.getValue().length === 0 && !this.searchFocused
|
|
450
|
+
? this.theme.fg("muted", `⌕ ${placeholder}`)
|
|
451
|
+
: `⌕ ${this.searchInput.render(Math.max(1, width - 4))[0] ?? ""}`;
|
|
452
|
+
return truncateToWidth(this.theme.fg("border", "[") + text + this.theme.fg("border", "]"), width, "");
|
|
453
|
+
}
|
|
454
|
+
wrapLine(text, width) {
|
|
455
|
+
return truncateToWidth(text, width, "");
|
|
456
|
+
}
|
|
457
|
+
managerKeyAction(data) {
|
|
458
|
+
const state = { view: this.view, tab: this.tab, selectedCount: this.selected.size, searchFocused: this.searchFocused };
|
|
459
|
+
if (this.keybindings.matches(data, "tui.select.up"))
|
|
460
|
+
return "up";
|
|
461
|
+
if (this.keybindings.matches(data, "tui.select.down"))
|
|
462
|
+
return "down";
|
|
463
|
+
if (this.keybindings.matches(data, "tui.select.confirm"))
|
|
464
|
+
return "details";
|
|
465
|
+
if (this.keybindings.matches(data, "tui.select.cancel"))
|
|
466
|
+
return this.view === "list" ? "close" : "back";
|
|
467
|
+
return pluginManagerKeyAction(data, state);
|
|
468
|
+
}
|
|
469
|
+
handleListAction(action) {
|
|
470
|
+
if (action === "up" || action === "down") {
|
|
471
|
+
if (this.tab === "marketplaces") {
|
|
472
|
+
const max = Math.max(0, pluginManagerMarketplaceRows(this.marketplaces, this.checkOnOpen).length - 1);
|
|
473
|
+
this.marketplaceCursor = action === "up" ? Math.max(0, this.marketplaceCursor - 1) : Math.min(max, this.marketplaceCursor + 1);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
const rows = this.filteredRows(this.currentRows());
|
|
477
|
+
this.cursor = action === "up" ? Math.max(0, this.cursor - 1) : Math.min(Math.max(0, rows.length - 1), this.cursor + 1);
|
|
478
|
+
}
|
|
479
|
+
this.requestRender();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (action === "left" || action === "right") {
|
|
483
|
+
if (this.tab === "marketplaces" || this.tab === "issues" || this.isListTab()) {
|
|
484
|
+
const current = TAB_ORDER.indexOf(this.tab);
|
|
485
|
+
const next = (current + (action === "left" ? -1 : 1) + TAB_ORDER.length) % TAB_ORDER.length;
|
|
486
|
+
this.setTab(TAB_ORDER[next]);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (action === "select" && this.isListTab()) {
|
|
491
|
+
const row = this.filteredRows(this.currentRows())[this.cursor];
|
|
492
|
+
if (row !== undefined) {
|
|
493
|
+
if (this.tab === "discover" && row.installed) {
|
|
494
|
+
this.showToast(`${row.name} is already installed`, "warning");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (this.selected.has(row.id))
|
|
498
|
+
this.selected.delete(row.id);
|
|
499
|
+
else
|
|
500
|
+
this.selected.add(row.id);
|
|
501
|
+
this.requestRender();
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (action === "select-all" && this.isListTab()) {
|
|
506
|
+
for (const row of this.filteredRows(this.currentRows())) {
|
|
507
|
+
if (this.tab !== "discover" || !row.installed)
|
|
508
|
+
this.selected.add(row.id);
|
|
509
|
+
}
|
|
510
|
+
this.requestRender();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (action === "details") {
|
|
514
|
+
if (this.tab === "marketplaces") {
|
|
515
|
+
this.handleMarketplaceEnter();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (this.tab === "issues") {
|
|
519
|
+
const issue = this.issues[this.cursor];
|
|
520
|
+
if (issue?.pluginId !== undefined)
|
|
521
|
+
this.openDetail(issue.pluginId);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const row = this.filteredRows(this.currentRows())[this.cursor];
|
|
525
|
+
if (row !== undefined)
|
|
526
|
+
this.openDetail(row.id);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (action === "check") {
|
|
530
|
+
if (this.tab === "issues")
|
|
531
|
+
void this.loadIssues();
|
|
532
|
+
this.startMarketplaceCheck();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (action === "search") {
|
|
536
|
+
this.searchFocused = true;
|
|
537
|
+
this.searchInput.focused = this._focused;
|
|
538
|
+
this.searchInput.setValue(this.query);
|
|
539
|
+
this.requestRender();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (BATCH_ACTIONS.includes(action)) {
|
|
543
|
+
this.startBatch(action);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (action === "close") {
|
|
547
|
+
if (this.selected.size > 0) {
|
|
548
|
+
this.selected.clear();
|
|
549
|
+
this.requestRender();
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
this.close();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
handleConfirmationInput(data) {
|
|
557
|
+
if (matchesKey(data, Key.escape)) {
|
|
558
|
+
this.view = "list";
|
|
559
|
+
this.batch = undefined;
|
|
560
|
+
this.requestRender();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (matchesKey(data, Key.up))
|
|
564
|
+
this.detailActionCursor = Math.max(0, this.detailActionCursor - 1);
|
|
565
|
+
if (matchesKey(data, Key.down))
|
|
566
|
+
this.detailActionCursor = Math.min(1, this.detailActionCursor + 1);
|
|
567
|
+
if (matchesKey(data, Key.enter)) {
|
|
568
|
+
if (this.detailActionCursor === 0)
|
|
569
|
+
void this.confirmBatch().catch((error) => this.showToast(`Batch failed: ${errorMessage(error)}`, "error"));
|
|
570
|
+
else {
|
|
571
|
+
this.view = "list";
|
|
572
|
+
this.batch = undefined;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
this.requestRender();
|
|
576
|
+
}
|
|
577
|
+
handleBatchInput(data) {
|
|
578
|
+
if (this.batchRunning) {
|
|
579
|
+
if (matchesKey(data, Key.escape)) {
|
|
580
|
+
this.batchCancelledAfterCurrent = true;
|
|
581
|
+
// Abort only an in-flight marketplace refresh. Once an item has
|
|
582
|
+
// started, cancellation is observed at the next item boundary so Esc
|
|
583
|
+
// never interrupts the first/current plugin mutation.
|
|
584
|
+
if (!this.batchItemActive) {
|
|
585
|
+
this.batchController?.abort("cancelled before next item");
|
|
586
|
+
if (this.checking)
|
|
587
|
+
this.cancelMarketplaceCheck();
|
|
588
|
+
}
|
|
589
|
+
this.requestRender();
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (matchesKey(data, Key.up))
|
|
594
|
+
this.detailActionCursor = Math.max(0, this.detailActionCursor - 1);
|
|
595
|
+
if (matchesKey(data, Key.down))
|
|
596
|
+
this.detailActionCursor = Math.min(1, this.detailActionCursor + 1);
|
|
597
|
+
if (matchesKey(data, Key.enter)) {
|
|
598
|
+
if (this.detailActionCursor === 0) {
|
|
599
|
+
this.tab = "installed";
|
|
600
|
+
this.view = "list";
|
|
601
|
+
this.cursor = 0;
|
|
602
|
+
this.selected.clear();
|
|
603
|
+
void this.loadLocal().catch((error) => this.showToast(`Could not load plugin data: ${errorMessage(error)}`, "warning"));
|
|
604
|
+
}
|
|
605
|
+
else
|
|
606
|
+
this.close();
|
|
607
|
+
}
|
|
608
|
+
if (matchesKey(data, Key.escape))
|
|
609
|
+
this.close();
|
|
610
|
+
this.requestRender();
|
|
611
|
+
}
|
|
612
|
+
handleDetailInput(data) {
|
|
613
|
+
if (matchesKey(data, Key.escape)) {
|
|
614
|
+
this.view = "list";
|
|
615
|
+
this.detailId = undefined;
|
|
616
|
+
this.detailActionCursor = 0;
|
|
617
|
+
this.requestRender();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (matchesKey(data, Key.up))
|
|
621
|
+
this.detailActionCursor = Math.max(0, this.detailActionCursor - 1);
|
|
622
|
+
if (matchesKey(data, Key.down)) {
|
|
623
|
+
const actions = this.detailActions(this.currentDetailRow(), this.currentDetailInstalled());
|
|
624
|
+
this.detailActionCursor = Math.min(Math.max(0, actions.length - 1), this.detailActionCursor + 1);
|
|
625
|
+
}
|
|
626
|
+
if (matchesKey(data, Key.enter))
|
|
627
|
+
void this.runDetailAction().catch((error) => this.showToast(`Action failed: ${errorMessage(error)}`, "error"));
|
|
628
|
+
if (data.toLocaleLowerCase() === "r")
|
|
629
|
+
this.startMarketplaceCheck();
|
|
630
|
+
if (data.toLocaleLowerCase() === "i" && this.tab === "discover")
|
|
631
|
+
this.startBatch("install", this.detailId === undefined ? [] : [identityFromString(this.detailId)]);
|
|
632
|
+
if (data.toLocaleLowerCase() === "u" && this.tab === "installed")
|
|
633
|
+
this.startBatch("update", this.detailId === undefined ? [] : [identityFromString(this.detailId)]);
|
|
634
|
+
this.requestRender();
|
|
635
|
+
}
|
|
636
|
+
async runDetailAction() {
|
|
637
|
+
const row = this.currentDetailRow();
|
|
638
|
+
if (row === undefined)
|
|
639
|
+
return;
|
|
640
|
+
const installed = this.currentDetailInstalled();
|
|
641
|
+
const actions = this.detailActions(row, installed);
|
|
642
|
+
const action = actions[this.detailActionCursor]?.action;
|
|
643
|
+
if (action === "back") {
|
|
644
|
+
this.view = "list";
|
|
645
|
+
this.detailId = undefined;
|
|
646
|
+
this.requestRender();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (action === "toggle-auto" && installed !== undefined) {
|
|
650
|
+
if (!installed.autoUpdate) {
|
|
651
|
+
const approved = await this.confirm("Enable automatic plugin updates", "This grants standing authorization to replace executable hooks and MCP servers when Pi startup sees a declared catalog version change. Enable it?");
|
|
652
|
+
if (!approved)
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const updated = await this.host.setAutoUpdate(installed.marketplace, installed.name, !installed.autoUpdate);
|
|
657
|
+
this.installed = this.installed.map((item) => identityOf(item) === row.id ? updated : item);
|
|
658
|
+
this.showToast(updated.autoUpdate ? "Automatic startup updates enabled" : "Automatic startup updates disabled");
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
this.showToast(`Could not change automatic updates: ${errorMessage(error)}`, "error");
|
|
662
|
+
}
|
|
663
|
+
this.requestRender();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (action === "install" || action === "update" || action === "enable" || action === "disable" || action === "remove") {
|
|
667
|
+
this.startBatch(action, [identityFromString(row.id)]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
detailActions(row, installed) {
|
|
671
|
+
if (row === undefined)
|
|
672
|
+
return [{ action: "back", label: "Back to plugin list" }];
|
|
673
|
+
if (installed === undefined)
|
|
674
|
+
return [
|
|
675
|
+
{ action: "install", label: "Install plugin" },
|
|
676
|
+
{ action: "back", label: "Back to plugin list" },
|
|
677
|
+
];
|
|
678
|
+
return [
|
|
679
|
+
{ action: installed.enabled ? "disable" : "enable", label: installed.enabled ? "Disable plugin" : "Enable plugin" },
|
|
680
|
+
{ action: "update", label: "Update now" },
|
|
681
|
+
{ action: "toggle-auto", label: installed.autoUpdate ? "Turn off automatic updates" : "Turn on automatic updates" },
|
|
682
|
+
{ action: "remove", label: "Remove plugin", destructive: true },
|
|
683
|
+
{ action: "back", label: "Back to plugin list" },
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
startBatch(action, explicit) {
|
|
687
|
+
if (this.batchRunning)
|
|
688
|
+
return;
|
|
689
|
+
const current = this.currentRows();
|
|
690
|
+
const requested = explicit === undefined
|
|
691
|
+
? [...prunePluginSelection(this.selected, current).selected].map(identityFromString)
|
|
692
|
+
: explicit;
|
|
693
|
+
const pruned = explicit === undefined ? prunePluginSelection(this.selected, current) : { selected: new Set(), vanished: [] };
|
|
694
|
+
this.selected = new Set(pruned.selected);
|
|
695
|
+
if (pruned.vanished.length > 0)
|
|
696
|
+
this.notify(`Dropped ${pruned.vanished.length} vanished selection${pruned.vanished.length === 1 ? "" : "s"}.`, "warning");
|
|
697
|
+
const identities = action === "install"
|
|
698
|
+
? requested.filter((identity) => this.rowById(identityToString(identity))?.installed !== true)
|
|
699
|
+
: requested;
|
|
700
|
+
const alreadyInstalled = requested.length - identities.length;
|
|
701
|
+
if (alreadyInstalled > 0)
|
|
702
|
+
this.notify(`Skipped ${alreadyInstalled} already-installed plugin${alreadyInstalled === 1 ? "" : "s"}.`, "warning");
|
|
703
|
+
if (identities.length === 0) {
|
|
704
|
+
this.showToast("No current plugins are selected for this action", "warning");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (explicit === undefined)
|
|
708
|
+
this.selected = new Set(identities.map(identityToString));
|
|
709
|
+
this.batch = Object.freeze({ action, identities: Object.freeze(identities.map((identity) => Object.freeze({ ...identity }))) });
|
|
710
|
+
this.detailActionCursor = 0;
|
|
711
|
+
this.view = "confirm";
|
|
712
|
+
this.requestRender();
|
|
713
|
+
}
|
|
714
|
+
async confirmBatch() {
|
|
715
|
+
if (this.batch === undefined)
|
|
716
|
+
return;
|
|
717
|
+
this.view = "batch";
|
|
718
|
+
this.batchRunning = true;
|
|
719
|
+
this.batchResults = [];
|
|
720
|
+
this.batchCancelledAfterCurrent = false;
|
|
721
|
+
this.batchController = new AbortController();
|
|
722
|
+
this.batchItemActive = false;
|
|
723
|
+
this.requestRender();
|
|
724
|
+
const batch = this.batch;
|
|
725
|
+
const controller = this.batchController;
|
|
726
|
+
const run = async () => {
|
|
727
|
+
try {
|
|
728
|
+
let refresh = batch.action === "install" || batch.action === "update";
|
|
729
|
+
// If the same marketplace is already being checked, wait for that
|
|
730
|
+
// result rather than starting a second network operation. A failed
|
|
731
|
+
// check is retried by the batch so a transient check does not silently
|
|
732
|
+
// turn into an install from stale catalog data.
|
|
733
|
+
const batchMarketplaces = new Set(batch.identities.map((identity) => identity.marketplace));
|
|
734
|
+
const overlapsActiveCheck = [...batchMarketplaces].some((marketplace) => this.checkingMarketplaces.has(marketplace));
|
|
735
|
+
if (refresh && this.checkPromise !== undefined && overlapsActiveCheck) {
|
|
736
|
+
const activeCheck = this.checkPromise;
|
|
737
|
+
const checked = await activeCheck;
|
|
738
|
+
const checkedByName = new Map(checked.map((result) => [result.marketplace, result]));
|
|
739
|
+
refresh = !batch.identities.every((identity) => checkedByName.get(identity.marketplace)?.ok === true);
|
|
740
|
+
}
|
|
741
|
+
if (controller.signal.aborted)
|
|
742
|
+
return;
|
|
743
|
+
if (this.batchCancelledAfterCurrent)
|
|
744
|
+
return;
|
|
745
|
+
const options = {
|
|
746
|
+
refresh,
|
|
747
|
+
signal: controller.signal,
|
|
748
|
+
onBeforeItem: (identity) => {
|
|
749
|
+
if (this.destroyed || this.batch !== batch)
|
|
750
|
+
return;
|
|
751
|
+
this.batchItemActive = true;
|
|
752
|
+
this.requestRender();
|
|
753
|
+
},
|
|
754
|
+
onItem: (result) => {
|
|
755
|
+
if (this.destroyed || this.batch !== batch)
|
|
756
|
+
return;
|
|
757
|
+
this.batchItemActive = false;
|
|
758
|
+
this.batchResults.push(result);
|
|
759
|
+
if (result.ok)
|
|
760
|
+
this.reloadNeeded = true;
|
|
761
|
+
if (this.batchCancelledAfterCurrent)
|
|
762
|
+
controller.abort("cancelled before next item");
|
|
763
|
+
this.requestRender();
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
const outcome = await this.host.runPluginBatch(batch.action, batch.identities, options);
|
|
767
|
+
if (outcome.results.some((result) => result.ok))
|
|
768
|
+
this.reloadNeeded = true;
|
|
769
|
+
if (outcome.cancelled)
|
|
770
|
+
this.batchCancelledAfterCurrent = true;
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
this.showToast(`Batch failed: ${errorMessage(error)}`, "error");
|
|
774
|
+
}
|
|
775
|
+
finally {
|
|
776
|
+
if (this.destroyed || this.batch !== batch)
|
|
777
|
+
return;
|
|
778
|
+
this.batchRunning = false;
|
|
779
|
+
this.batchController = undefined;
|
|
780
|
+
void this.loadLocal().catch(() => undefined);
|
|
781
|
+
this.requestRender();
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
void run().catch((error) => {
|
|
785
|
+
if (!this.destroyed)
|
|
786
|
+
this.showToast(`Batch failed: ${errorMessage(error)}`, "error");
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
openDetail(id) {
|
|
790
|
+
this.detailId = id;
|
|
791
|
+
this.detailActionCursor = 0;
|
|
792
|
+
this.runtime = undefined;
|
|
793
|
+
this.view = "detail";
|
|
794
|
+
this.requestRender();
|
|
795
|
+
void this.loadDetailRuntime(id);
|
|
796
|
+
}
|
|
797
|
+
close() {
|
|
798
|
+
if (this.completed)
|
|
799
|
+
return;
|
|
800
|
+
this.completed = true;
|
|
801
|
+
this.done({ reloadNeeded: this.reloadNeeded });
|
|
802
|
+
}
|
|
803
|
+
setTab(tab) {
|
|
804
|
+
this.tab = tab;
|
|
805
|
+
this.view = "list";
|
|
806
|
+
this.detailId = undefined;
|
|
807
|
+
this.cursor = 0;
|
|
808
|
+
this.marketplaceCursor = 0;
|
|
809
|
+
this.selected.clear();
|
|
810
|
+
this.query = "";
|
|
811
|
+
this.searchInput.setValue("");
|
|
812
|
+
if (tab === "issues")
|
|
813
|
+
void this.loadIssues();
|
|
814
|
+
this.requestRender();
|
|
815
|
+
}
|
|
816
|
+
async loadCheckOnOpen() {
|
|
817
|
+
try {
|
|
818
|
+
const enabled = await this.host.getCheckOnOpen();
|
|
819
|
+
if (this.destroyed)
|
|
820
|
+
return;
|
|
821
|
+
this.checkOnOpen = enabled;
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (!this.destroyed)
|
|
825
|
+
this.showToast(`Could not load check-on-open preference: ${errorMessage(error)}`, "warning");
|
|
826
|
+
}
|
|
827
|
+
finally {
|
|
828
|
+
if (this.destroyed)
|
|
829
|
+
return;
|
|
830
|
+
this.checkOnOpenLoaded = true;
|
|
831
|
+
this.maybeStartOpenCheck();
|
|
832
|
+
this.requestRender();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
maybeStartOpenCheck() {
|
|
836
|
+
if (!this.openCheckPending || !this.checkOnOpenLoaded)
|
|
837
|
+
return;
|
|
838
|
+
if (!this.checkOnOpen) {
|
|
839
|
+
this.openCheckPending = false;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (this.marketplaces.length === 0)
|
|
843
|
+
return;
|
|
844
|
+
this.openCheckPending = false;
|
|
845
|
+
this.startMarketplaceCheck();
|
|
846
|
+
}
|
|
847
|
+
async toggleCheckOnOpen() {
|
|
848
|
+
if (this.checkOnOpenSaving)
|
|
849
|
+
return;
|
|
850
|
+
const enabled = !this.checkOnOpen;
|
|
851
|
+
this.checkOnOpenSaving = true;
|
|
852
|
+
this.requestRender();
|
|
853
|
+
try {
|
|
854
|
+
await this.host.setCheckOnOpen(enabled);
|
|
855
|
+
if (this.destroyed)
|
|
856
|
+
return;
|
|
857
|
+
this.checkOnOpen = enabled;
|
|
858
|
+
this.showToast(enabled ? "Check-on-open enabled" : "Check-on-open disabled");
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
if (!this.destroyed)
|
|
862
|
+
this.showToast(`Could not save check-on-open preference: ${errorMessage(error)}`, "error");
|
|
863
|
+
}
|
|
864
|
+
finally {
|
|
865
|
+
if (!this.destroyed) {
|
|
866
|
+
this.checkOnOpenSaving = false;
|
|
867
|
+
this.requestRender();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async loadDetailRuntime(id) {
|
|
872
|
+
try {
|
|
873
|
+
const runtime = await this.host.scanRuntime();
|
|
874
|
+
if (this.destroyed || this.view !== "detail" || this.detailId !== id)
|
|
875
|
+
return;
|
|
876
|
+
this.runtime = runtime;
|
|
877
|
+
this.requestRender();
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
if (!this.destroyed && this.view === "detail" && this.detailId === id) {
|
|
881
|
+
this.showToast(`Could not inspect installed plugin runtime: ${errorMessage(error)}`, "warning");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
leaveSearch() {
|
|
886
|
+
this.searchFocused = false;
|
|
887
|
+
this.searchInput.focused = false;
|
|
888
|
+
this.requestRender();
|
|
889
|
+
}
|
|
890
|
+
isListTab() {
|
|
891
|
+
return this.tab === "installed" || this.tab === "discover";
|
|
892
|
+
}
|
|
893
|
+
currentRows() {
|
|
894
|
+
if (this.tab === "installed")
|
|
895
|
+
return this.installedRows();
|
|
896
|
+
if (this.tab === "discover")
|
|
897
|
+
return this.discoverRows();
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
currentCursorId() {
|
|
901
|
+
if (!this.isListTab())
|
|
902
|
+
return undefined;
|
|
903
|
+
return this.filteredRows(this.currentRows())[this.cursor]?.id;
|
|
904
|
+
}
|
|
905
|
+
restoreCursor(id) {
|
|
906
|
+
if (!this.isListTab())
|
|
907
|
+
return;
|
|
908
|
+
const rows = this.filteredRows(this.currentRows());
|
|
909
|
+
this.cursor = restorePluginCursor(rows, this.cursor, id);
|
|
910
|
+
}
|
|
911
|
+
filteredRows(rows) {
|
|
912
|
+
return filterPluginRows(rows, this.query);
|
|
913
|
+
}
|
|
914
|
+
installedRows() {
|
|
915
|
+
return this.installed.map((plugin) => {
|
|
916
|
+
const entry = catalogEntry(this.catalogs.get(plugin.marketplace), plugin.name);
|
|
917
|
+
const installedVersion = receiptVersion(plugin);
|
|
918
|
+
const availableVersion = entry?.version !== undefined && entry.version !== installedVersion ? entry.version : undefined;
|
|
919
|
+
return {
|
|
920
|
+
id: identityOf(plugin),
|
|
921
|
+
name: plugin.name,
|
|
922
|
+
marketplace: plugin.marketplace,
|
|
923
|
+
description: typeof entry?.description === "string" ? entry.description : undefined,
|
|
924
|
+
version: installedVersion,
|
|
925
|
+
availableVersion,
|
|
926
|
+
installed: true,
|
|
927
|
+
enabled: plugin.enabled,
|
|
928
|
+
autoUpdate: plugin.autoUpdate,
|
|
929
|
+
issue: entry === undefined && this.catalogs.has(plugin.marketplace) ? "not declared in current catalog" : undefined,
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
discoverRows() {
|
|
934
|
+
const installedById = new Map(this.installed.map((plugin) => [identityOf(plugin), plugin]));
|
|
935
|
+
const rows = [];
|
|
936
|
+
for (const marketplace of this.marketplaces) {
|
|
937
|
+
const catalog = this.catalogs.get(marketplace.name);
|
|
938
|
+
for (const entry of catalog?.plugins ?? []) {
|
|
939
|
+
const id = pluginIdentity(entry.name, marketplace.name);
|
|
940
|
+
const installed = installedById.get(id);
|
|
941
|
+
rows.push({
|
|
942
|
+
id,
|
|
943
|
+
name: entry.name,
|
|
944
|
+
marketplace: marketplace.name,
|
|
945
|
+
description: entry.description,
|
|
946
|
+
version: entry.version,
|
|
947
|
+
availableVersion: installed !== undefined && entry.version !== receiptVersion(installed) ? entry.version : undefined,
|
|
948
|
+
installed: installed !== undefined,
|
|
949
|
+
enabled: installed?.enabled,
|
|
950
|
+
autoUpdate: installed?.autoUpdate,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return rows.sort((left, right) => left.id.localeCompare(right.id));
|
|
955
|
+
}
|
|
956
|
+
rowById(id) {
|
|
957
|
+
return [...this.installedRows(), ...this.discoverRows()].find((row) => row.id === id);
|
|
958
|
+
}
|
|
959
|
+
currentDetailRow() {
|
|
960
|
+
return this.detailId === undefined ? undefined : this.rowById(this.detailId);
|
|
961
|
+
}
|
|
962
|
+
currentDetailInstalled() {
|
|
963
|
+
const row = this.currentDetailRow();
|
|
964
|
+
return row === undefined ? undefined : this.installed.find((plugin) => identityOf(plugin) === row.id);
|
|
965
|
+
}
|
|
966
|
+
statusLabel(installed, entry) {
|
|
967
|
+
if (!installed.enabled)
|
|
968
|
+
return "disabled";
|
|
969
|
+
if (entry?.version !== undefined && entry.version !== receiptVersion(installed))
|
|
970
|
+
return "update available";
|
|
971
|
+
return "enabled";
|
|
972
|
+
}
|
|
973
|
+
rowStatus(row) {
|
|
974
|
+
if (row.issue !== undefined)
|
|
975
|
+
return this.theme.fg("error", "× issue");
|
|
976
|
+
if (row.availableVersion !== undefined)
|
|
977
|
+
return this.theme.fg("warning", `↑ ${row.availableVersion} available`);
|
|
978
|
+
if (row.installed && row.enabled === false)
|
|
979
|
+
return this.theme.fg("muted", "○ disabled");
|
|
980
|
+
if (row.installed)
|
|
981
|
+
return this.theme.fg("success", "✓ enabled");
|
|
982
|
+
return "";
|
|
983
|
+
}
|
|
984
|
+
tabLabel() {
|
|
985
|
+
return this.tab[0].toLocaleUpperCase() + this.tab.slice(1);
|
|
986
|
+
}
|
|
987
|
+
marketplaceStatus(name) {
|
|
988
|
+
if (this.checkingMarketplaces.has(name))
|
|
989
|
+
return this.theme.fg("accent", "checking…");
|
|
990
|
+
const catalog = this.catalogs.get(name);
|
|
991
|
+
return catalog === undefined ? this.theme.fg("warning", "catalog unavailable") : this.theme.fg("muted", "local checkout ready");
|
|
992
|
+
}
|
|
993
|
+
handleMarketplaceEnter() {
|
|
994
|
+
const actions = 3;
|
|
995
|
+
if (this.marketplaceCursor === 0) {
|
|
996
|
+
void this.addMarketplace();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (this.marketplaceCursor === 1) {
|
|
1000
|
+
this.startMarketplaceCheck();
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (this.marketplaceCursor === 2) {
|
|
1004
|
+
void this.toggleCheckOnOpen();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const marketplace = this.marketplaces[this.marketplaceCursor - actions];
|
|
1008
|
+
if (marketplace !== undefined)
|
|
1009
|
+
this.startMarketplaceCheck([marketplace.name]);
|
|
1010
|
+
}
|
|
1011
|
+
async addMarketplace() {
|
|
1012
|
+
try {
|
|
1013
|
+
const source = await this.input("Add marketplace", "owner/repository, Git URL, or local path");
|
|
1014
|
+
if (source === undefined || source.trim().length === 0)
|
|
1015
|
+
return;
|
|
1016
|
+
const added = await this.host.addMarketplace(source.trim());
|
|
1017
|
+
this.showToast(`Added marketplace ${added.name}`);
|
|
1018
|
+
await this.loadLocal();
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
this.showToast(`Could not add marketplace: ${errorMessage(error)}`, "error");
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
startMarketplaceCheck(names = this.marketplaces.map((marketplace) => marketplace.name)) {
|
|
1025
|
+
if (this.checking || names.length === 0)
|
|
1026
|
+
return;
|
|
1027
|
+
this.checkRun++;
|
|
1028
|
+
const run = this.checkRun;
|
|
1029
|
+
const controller = new AbortController();
|
|
1030
|
+
this.checkController = controller;
|
|
1031
|
+
this.checking = true;
|
|
1032
|
+
this.checkedMarketplaces = 0;
|
|
1033
|
+
this.checkingMarketplaces = new Set(names);
|
|
1034
|
+
this.localEpoch++;
|
|
1035
|
+
this.requestRender();
|
|
1036
|
+
const onResult = (result) => {
|
|
1037
|
+
if (this.destroyed || run !== this.checkRun)
|
|
1038
|
+
return;
|
|
1039
|
+
this.checkedMarketplaces++;
|
|
1040
|
+
if (result.ok && result.catalog !== undefined) {
|
|
1041
|
+
const cursorId = this.currentCursorId();
|
|
1042
|
+
this.catalogs.set(result.marketplace, result.catalog);
|
|
1043
|
+
this.restoreCursor(cursorId);
|
|
1044
|
+
}
|
|
1045
|
+
this.requestRender();
|
|
1046
|
+
};
|
|
1047
|
+
const runCheck = async () => {
|
|
1048
|
+
try {
|
|
1049
|
+
const results = await this.host.refreshMarketplaces(names, {
|
|
1050
|
+
signal: controller.signal,
|
|
1051
|
+
timeoutMs: DEFAULT_REFRESH_TIMEOUT_MS,
|
|
1052
|
+
concurrency: 2,
|
|
1053
|
+
onResult,
|
|
1054
|
+
});
|
|
1055
|
+
if (this.destroyed || run !== this.checkRun)
|
|
1056
|
+
return results;
|
|
1057
|
+
this.updatesChecked = true;
|
|
1058
|
+
const failures = results.filter((result) => !result.ok);
|
|
1059
|
+
if (failures.length > 0)
|
|
1060
|
+
this.showToast(`${failures.length} marketplace check${failures.length === 1 ? "" : "s"} failed`, "warning");
|
|
1061
|
+
return results;
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
if (!this.destroyed && run === this.checkRun)
|
|
1065
|
+
this.showToast(`Marketplace check failed: ${errorMessage(error)}`, "warning");
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
if (this.destroyed || run !== this.checkRun)
|
|
1070
|
+
return [];
|
|
1071
|
+
this.checking = false;
|
|
1072
|
+
this.checkingMarketplaces.clear();
|
|
1073
|
+
this.checkController = undefined;
|
|
1074
|
+
this.checkPromise = undefined;
|
|
1075
|
+
void this.loadLocal().catch(() => undefined);
|
|
1076
|
+
this.requestRender();
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
this.checkPromise = runCheck();
|
|
1080
|
+
void this.checkPromise.catch((error) => {
|
|
1081
|
+
if (!this.destroyed)
|
|
1082
|
+
this.showToast(`Marketplace check failed: ${errorMessage(error)}`, "warning");
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
cancelMarketplaceCheck() {
|
|
1086
|
+
if (!this.checking)
|
|
1087
|
+
return;
|
|
1088
|
+
this.checkRun++;
|
|
1089
|
+
this.checkController?.abort("cancelled");
|
|
1090
|
+
this.checkController = undefined;
|
|
1091
|
+
this.checkPromise = undefined;
|
|
1092
|
+
this.checking = false;
|
|
1093
|
+
this.checkingMarketplaces.clear();
|
|
1094
|
+
this.showToast("Marketplace check cancelled");
|
|
1095
|
+
this.requestRender();
|
|
1096
|
+
}
|
|
1097
|
+
async loadLocal() {
|
|
1098
|
+
const epoch = this.localEpoch;
|
|
1099
|
+
const marketplaces = await this.host.listMarketplaces();
|
|
1100
|
+
if (this.destroyed)
|
|
1101
|
+
return;
|
|
1102
|
+
let cursorId = this.currentCursorId();
|
|
1103
|
+
this.marketplaces = marketplaces;
|
|
1104
|
+
const marketplaceNames = new Set(marketplaces.map((marketplace) => marketplace.name));
|
|
1105
|
+
for (const name of this.catalogs.keys()) {
|
|
1106
|
+
if (!marketplaceNames.has(name))
|
|
1107
|
+
this.catalogs.delete(name);
|
|
1108
|
+
}
|
|
1109
|
+
this.restoreCursor(cursorId);
|
|
1110
|
+
this.requestRender();
|
|
1111
|
+
this.maybeStartOpenCheck();
|
|
1112
|
+
const installed = await this.host.listInstalled();
|
|
1113
|
+
if (this.destroyed)
|
|
1114
|
+
return;
|
|
1115
|
+
cursorId = this.currentCursorId();
|
|
1116
|
+
this.installed = installed;
|
|
1117
|
+
this.restoreCursor(cursorId);
|
|
1118
|
+
this.requestRender();
|
|
1119
|
+
await Promise.all(this.marketplaces.map(async (marketplace) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const catalog = await this.host.browseMarketplace(marketplace.name);
|
|
1122
|
+
if (!this.destroyed && epoch === this.localEpoch) {
|
|
1123
|
+
const currentCursorId = this.currentCursorId();
|
|
1124
|
+
this.catalogs.set(marketplace.name, catalog);
|
|
1125
|
+
this.restoreCursor(currentCursorId);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
if (!this.destroyed && epoch === this.localEpoch)
|
|
1130
|
+
this.showToast(`${marketplace.name}: ${errorMessage(error)}`, "warning");
|
|
1131
|
+
}
|
|
1132
|
+
if (!this.destroyed)
|
|
1133
|
+
this.requestRender();
|
|
1134
|
+
}));
|
|
1135
|
+
if (!this.destroyed && this.tab === "issues")
|
|
1136
|
+
await this.loadIssues();
|
|
1137
|
+
}
|
|
1138
|
+
async loadIssues() {
|
|
1139
|
+
this.issuesLoading = true;
|
|
1140
|
+
this.requestRender();
|
|
1141
|
+
try {
|
|
1142
|
+
const runtime = await this.host.scanRuntime();
|
|
1143
|
+
if (this.destroyed)
|
|
1144
|
+
return;
|
|
1145
|
+
this.runtime = runtime;
|
|
1146
|
+
this.issues = this.projectIssues(runtime);
|
|
1147
|
+
this.cursor = Math.min(this.cursor, Math.max(0, this.issues.length - 1));
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
if (!this.destroyed)
|
|
1151
|
+
this.issues = [{ id: "scan", title: "Runtime scan failed", message: errorMessage(error), severity: "error" }];
|
|
1152
|
+
}
|
|
1153
|
+
finally {
|
|
1154
|
+
if (!this.destroyed) {
|
|
1155
|
+
this.issuesLoading = false;
|
|
1156
|
+
this.requestRender();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
projectIssues(runtime) {
|
|
1161
|
+
const issues = [];
|
|
1162
|
+
for (const plugin of runtime.plugins) {
|
|
1163
|
+
for (const diagnostic of plugin.diagnostics) {
|
|
1164
|
+
issues.push({
|
|
1165
|
+
id: `${identityOf(plugin.info)}:${diagnostic.scope}`,
|
|
1166
|
+
title: identityOf(plugin.info),
|
|
1167
|
+
message: diagnostic.message,
|
|
1168
|
+
severity: "error",
|
|
1169
|
+
pluginId: identityOf(plugin.info),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
for (const diagnostic of runtime.diagnostics) {
|
|
1174
|
+
issues.push({ id: `runtime:${diagnostic.scope}`, title: diagnostic.scope, message: diagnostic.message, severity: "error" });
|
|
1175
|
+
}
|
|
1176
|
+
for (const plugin of this.installed) {
|
|
1177
|
+
const entry = catalogEntry(this.catalogs.get(plugin.marketplace), plugin.name);
|
|
1178
|
+
if (entry === undefined && this.catalogs.has(plugin.marketplace)) {
|
|
1179
|
+
issues.push({ id: `${identityOf(plugin)}:catalog`, title: identityOf(plugin), message: "Installed bundle is not declared in the current marketplace catalog; the installed copy remains available.", severity: "error", pluginId: identityOf(plugin) });
|
|
1180
|
+
}
|
|
1181
|
+
else if (plugin.autoUpdate && entry?.version === undefined) {
|
|
1182
|
+
issues.push({ id: `${identityOf(plugin)}:version`, title: identityOf(plugin), message: "Automatic updates are marked, but this catalog does not declare a version. Use /plugins update-marked for an explicit force update.", severity: "warning", pluginId: identityOf(plugin) });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const [marketplace, catalog] of this.catalogs) {
|
|
1186
|
+
for (const diagnostic of catalog.diagnostics ?? [])
|
|
1187
|
+
issues.push({ id: `${marketplace}:${diagnostic.scope}`, title: marketplace, message: diagnostic.message, severity: "warning" });
|
|
1188
|
+
}
|
|
1189
|
+
return issues;
|
|
1190
|
+
}
|
|
1191
|
+
async removeMarketplace(name) {
|
|
1192
|
+
const approved = await this.confirm("Remove marketplace", `Remove ${name}'s source checkout? Installed plugin bundles are left in place.`);
|
|
1193
|
+
if (!approved)
|
|
1194
|
+
return;
|
|
1195
|
+
try {
|
|
1196
|
+
await this.host.removeMarketplace(name);
|
|
1197
|
+
this.showToast(`Removed marketplace ${name}`);
|
|
1198
|
+
await this.loadLocal();
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
this.showToast(`Could not remove marketplace: ${errorMessage(error)}`, "error");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
showToast(message, type = "info") {
|
|
1205
|
+
this.toast = message;
|
|
1206
|
+
try {
|
|
1207
|
+
this.notify(message, type);
|
|
1208
|
+
}
|
|
1209
|
+
catch { /* notification failure must not escape the component callback */ }
|
|
1210
|
+
if (this.toastTimer !== undefined)
|
|
1211
|
+
clearTimeout(this.toastTimer);
|
|
1212
|
+
this.toastTimer = setTimeout(() => {
|
|
1213
|
+
if (this.destroyed)
|
|
1214
|
+
return;
|
|
1215
|
+
this.toast = undefined;
|
|
1216
|
+
this.requestRender();
|
|
1217
|
+
}, 2_000);
|
|
1218
|
+
this.toastTimer.unref?.();
|
|
1219
|
+
this.requestRender();
|
|
1220
|
+
}
|
|
1221
|
+
requestRender() {
|
|
1222
|
+
if (!this.destroyed)
|
|
1223
|
+
this.tui.requestRender();
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function identityFromString(value) {
|
|
1227
|
+
const at = value.lastIndexOf("@");
|
|
1228
|
+
if (at <= 0 || at === value.length - 1)
|
|
1229
|
+
throw new Error(`invalid plugin identity: ${value}`);
|
|
1230
|
+
return { plugin: value.slice(0, at), marketplace: value.slice(at + 1) };
|
|
1231
|
+
}
|
|
1232
|
+
function identityToString(identity) {
|
|
1233
|
+
return pluginIdentity(identity.plugin, identity.marketplace);
|
|
1234
|
+
}
|
|
1235
|
+
export async function openPluginManager(host, ctx) {
|
|
1236
|
+
if (ctx.mode !== "tui") {
|
|
1237
|
+
ctx.ui.notify("/plugins requires TUI mode for the plugin manager", "error");
|
|
1238
|
+
return undefined;
|
|
1239
|
+
}
|
|
1240
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => new PluginManager({
|
|
1241
|
+
host,
|
|
1242
|
+
tui,
|
|
1243
|
+
theme,
|
|
1244
|
+
keybindings,
|
|
1245
|
+
done,
|
|
1246
|
+
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
1247
|
+
input: (title, placeholder) => ctx.ui.input(title, placeholder),
|
|
1248
|
+
notify: (message, type) => ctx.ui.notify(message, type),
|
|
1249
|
+
}));
|
|
1250
|
+
}
|
|
1251
|
+
//# sourceMappingURL=plugin-manager.js.map
|