claudeup 3.16.0 → 4.0.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/package.json +1 -1
- package/src/data/predefined-profiles.js +191 -0
- package/src/data/predefined-profiles.ts +205 -0
- package/src/ui/adapters/pluginsAdapter.js +139 -0
- package/src/ui/adapters/pluginsAdapter.ts +202 -0
- package/src/ui/adapters/settingsAdapter.js +111 -0
- package/src/ui/adapters/settingsAdapter.ts +165 -0
- package/src/ui/components/ScrollableList.js +4 -4
- package/src/ui/components/ScrollableList.tsx +4 -4
- package/src/ui/components/SearchInput.js +2 -2
- package/src/ui/components/SearchInput.tsx +3 -3
- package/src/ui/components/StyledText.js +1 -1
- package/src/ui/components/StyledText.tsx +5 -1
- package/src/ui/components/layout/ProgressBar.js +1 -1
- package/src/ui/components/layout/ProgressBar.tsx +1 -5
- package/src/ui/components/modals/InputModal.tsx +1 -6
- package/src/ui/components/modals/LoadingModal.js +1 -1
- package/src/ui/components/modals/LoadingModal.tsx +1 -3
- package/src/ui/hooks/index.js +3 -3
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useKeyboard.ts +1 -3
- package/src/ui/hooks/useKeyboardHandler.js +9 -9
- package/src/ui/hooks/useKeyboardHandler.ts +9 -9
- package/src/ui/renderers/cliToolRenderers.js +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +153 -0
- package/src/ui/renderers/mcpRenderers.js +26 -0
- package/src/ui/renderers/mcpRenderers.tsx +145 -0
- package/src/ui/renderers/pluginRenderers.js +124 -0
- package/src/ui/renderers/pluginRenderers.tsx +362 -0
- package/src/ui/renderers/profileRenderers.js +172 -0
- package/src/ui/renderers/profileRenderers.tsx +410 -0
- package/src/ui/renderers/settingsRenderers.js +69 -0
- package/src/ui/renderers/settingsRenderers.tsx +205 -0
- package/src/ui/screens/CliToolsScreen.js +14 -58
- package/src/ui/screens/CliToolsScreen.tsx +36 -196
- package/src/ui/screens/EnvVarsScreen.js +12 -168
- package/src/ui/screens/EnvVarsScreen.tsx +16 -327
- package/src/ui/screens/McpScreen.js +12 -62
- package/src/ui/screens/McpScreen.tsx +21 -190
- package/src/ui/screens/PluginsScreen.js +52 -425
- package/src/ui/screens/PluginsScreen.tsx +70 -758
- package/src/ui/screens/ProfilesScreen.js +104 -68
- package/src/ui/screens/ProfilesScreen.tsx +147 -221
- package/src/ui/screens/SkillsScreen.js +16 -16
- package/src/ui/screens/SkillsScreen.tsx +20 -23
|
@@ -3,11 +3,9 @@ import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
|
3
3
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
4
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
5
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
-
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
7
6
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
7
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
9
|
-
import {
|
|
10
|
-
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
8
|
+
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
11
9
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
12
10
|
import {
|
|
13
11
|
getAvailablePlugins,
|
|
@@ -40,25 +38,11 @@ import {
|
|
|
40
38
|
checkMissingDeps,
|
|
41
39
|
installPluginDeps,
|
|
42
40
|
} from "../../services/plugin-setup.js";
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const SPLIT_MARKETPLACE = "claude-plugins-official";
|
|
49
|
-
|
|
50
|
-
interface ListItem {
|
|
51
|
-
id: string;
|
|
52
|
-
type: "category" | "plugin";
|
|
53
|
-
label: string;
|
|
54
|
-
marketplace?: Marketplace;
|
|
55
|
-
marketplaceEnabled?: boolean;
|
|
56
|
-
plugin?: PluginInfo;
|
|
57
|
-
pluginCount?: number;
|
|
58
|
-
isExpanded?: boolean;
|
|
59
|
-
/** True for the virtual Community sub-section derived from claude-plugins-official */
|
|
60
|
-
isCommunitySection?: boolean;
|
|
61
|
-
}
|
|
41
|
+
import {
|
|
42
|
+
buildPluginBrowserItems,
|
|
43
|
+
type PluginBrowserItem,
|
|
44
|
+
} from "../adapters/pluginsAdapter.js";
|
|
45
|
+
import { renderPluginRow, renderPluginDetail } from "../renderers/pluginRenderers.js";
|
|
62
46
|
|
|
63
47
|
export function PluginsScreen() {
|
|
64
48
|
const { state, dispatch } = useApp();
|
|
@@ -78,10 +62,7 @@ export function PluginsScreen() {
|
|
|
78
62
|
try {
|
|
79
63
|
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
80
64
|
const allMarketplaces = getAllMarketplaces(localMarketplaces);
|
|
81
|
-
|
|
82
|
-
// Always use getAvailablePlugins which fetches all scope data
|
|
83
65
|
const pluginData = await getAvailablePlugins(state.projectPath);
|
|
84
|
-
|
|
85
66
|
dispatch({
|
|
86
67
|
type: "PLUGINS_DATA_SUCCESS",
|
|
87
68
|
marketplaces: allMarketplaces,
|
|
@@ -101,132 +82,18 @@ export function PluginsScreen() {
|
|
|
101
82
|
}, [fetchData, state.dataRefreshVersion]);
|
|
102
83
|
|
|
103
84
|
// Build list items (categories + plugins)
|
|
104
|
-
const allItems = useMemo(():
|
|
85
|
+
const allItems = useMemo((): PluginBrowserItem[] => {
|
|
105
86
|
if (
|
|
106
87
|
pluginsState.marketplaces.status !== "success" ||
|
|
107
88
|
pluginsState.plugins.status !== "success"
|
|
108
89
|
) {
|
|
109
90
|
return [];
|
|
110
91
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const pluginsByMarketplace = new Map<string, PluginInfo[]>();
|
|
117
|
-
for (const plugin of plugins) {
|
|
118
|
-
const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
|
|
119
|
-
existing.push(plugin);
|
|
120
|
-
pluginsByMarketplace.set(plugin.marketplace, existing);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Sort marketplaces: deprecated ones go to the bottom
|
|
124
|
-
const sortedMarketplaces = [...marketplaces].sort((a, b) => {
|
|
125
|
-
const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
|
|
126
|
-
const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
|
|
127
|
-
return aDeprecated - bDeprecated;
|
|
92
|
+
return buildPluginBrowserItems({
|
|
93
|
+
marketplaces: pluginsState.marketplaces.data,
|
|
94
|
+
plugins: pluginsState.plugins.data,
|
|
95
|
+
collapsedMarketplaces: pluginsState.collapsedMarketplaces,
|
|
128
96
|
});
|
|
129
|
-
|
|
130
|
-
const items: ListItem[] = [];
|
|
131
|
-
|
|
132
|
-
for (const marketplace of sortedMarketplaces) {
|
|
133
|
-
const marketplacePlugins =
|
|
134
|
-
pluginsByMarketplace.get(marketplace.name) || [];
|
|
135
|
-
const isCollapsed = collapsed.has(marketplace.name);
|
|
136
|
-
const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
|
|
137
|
-
const hasPlugins = marketplacePlugins.length > 0;
|
|
138
|
-
|
|
139
|
-
// Special handling: split claude-plugins-official into two sub-sections
|
|
140
|
-
if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
|
|
141
|
-
const anthropicPlugins = marketplacePlugins.filter(
|
|
142
|
-
(p) => p.author?.name?.toLowerCase() === "anthropic",
|
|
143
|
-
);
|
|
144
|
-
const communityPlugins = marketplacePlugins.filter(
|
|
145
|
-
(p) => p.author?.name?.toLowerCase() !== "anthropic",
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Sub-section 1: Anthropic Official (plugins by Anthropic)
|
|
149
|
-
const anthropicCollapsed = collapsed.has(marketplace.name);
|
|
150
|
-
const anthropicHasPlugins = anthropicPlugins.length > 0;
|
|
151
|
-
items.push({
|
|
152
|
-
id: `mp:${marketplace.name}`,
|
|
153
|
-
type: "category",
|
|
154
|
-
label: marketplace.displayName,
|
|
155
|
-
marketplace,
|
|
156
|
-
marketplaceEnabled: isEnabled,
|
|
157
|
-
pluginCount: anthropicPlugins.length,
|
|
158
|
-
isExpanded: !anthropicCollapsed && anthropicHasPlugins,
|
|
159
|
-
});
|
|
160
|
-
if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
|
|
161
|
-
for (const plugin of anthropicPlugins) {
|
|
162
|
-
items.push({
|
|
163
|
-
id: `pl:${plugin.id}`,
|
|
164
|
-
type: "plugin",
|
|
165
|
-
label: plugin.name,
|
|
166
|
-
plugin,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Sub-section 2: Community (third-party plugins in same marketplace)
|
|
172
|
-
if (communityPlugins.length > 0) {
|
|
173
|
-
const communityVirtualMp: Marketplace = {
|
|
174
|
-
name: COMMUNITY_VIRTUAL_MARKETPLACE,
|
|
175
|
-
displayName: "Anthropic Official — 3rd Party",
|
|
176
|
-
source: marketplace.source,
|
|
177
|
-
description: "Third-party plugins in the Anthropic Official marketplace",
|
|
178
|
-
};
|
|
179
|
-
const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
|
|
180
|
-
items.push({
|
|
181
|
-
id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
|
|
182
|
-
type: "category",
|
|
183
|
-
label: "Anthropic Official — 3rd Party",
|
|
184
|
-
marketplace: communityVirtualMp,
|
|
185
|
-
marketplaceEnabled: true,
|
|
186
|
-
pluginCount: communityPlugins.length,
|
|
187
|
-
isExpanded: !communityCollapsed,
|
|
188
|
-
isCommunitySection: true,
|
|
189
|
-
});
|
|
190
|
-
if (!communityCollapsed) {
|
|
191
|
-
for (const plugin of communityPlugins) {
|
|
192
|
-
items.push({
|
|
193
|
-
id: `pl:${plugin.id}`,
|
|
194
|
-
type: "plugin",
|
|
195
|
-
label: plugin.name,
|
|
196
|
-
plugin,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Category header (marketplace)
|
|
206
|
-
items.push({
|
|
207
|
-
id: `mp:${marketplace.name}`,
|
|
208
|
-
type: "category",
|
|
209
|
-
label: marketplace.displayName,
|
|
210
|
-
marketplace,
|
|
211
|
-
marketplaceEnabled: isEnabled,
|
|
212
|
-
pluginCount: marketplacePlugins.length,
|
|
213
|
-
isExpanded: !isCollapsed && hasPlugins,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Plugins under this marketplace (if expanded)
|
|
217
|
-
if (isEnabled && hasPlugins && !isCollapsed) {
|
|
218
|
-
for (const plugin of marketplacePlugins) {
|
|
219
|
-
items.push({
|
|
220
|
-
id: `pl:${plugin.id}`,
|
|
221
|
-
type: "plugin",
|
|
222
|
-
label: plugin.name,
|
|
223
|
-
plugin,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return items;
|
|
230
97
|
}, [
|
|
231
98
|
pluginsState.marketplaces,
|
|
232
99
|
pluginsState.plugins,
|
|
@@ -239,50 +106,43 @@ export function PluginsScreen() {
|
|
|
239
106
|
if (!query) return allItems;
|
|
240
107
|
|
|
241
108
|
// Only search plugins, not categories
|
|
242
|
-
const pluginItems = allItems.filter((item) => item.
|
|
109
|
+
const pluginItems = allItems.filter((item) => item.kind === "plugin");
|
|
243
110
|
const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
|
|
244
111
|
|
|
245
|
-
// Build a set of matched plugin item ids for O(1) lookup
|
|
246
112
|
const matchedPluginIds = new Set<string>();
|
|
247
113
|
for (const result of fuzzyResults) {
|
|
248
114
|
matchedPluginIds.add(result.item.id);
|
|
249
115
|
}
|
|
250
116
|
|
|
251
|
-
// Walk allItems sequentially:
|
|
252
|
-
// For each category, include it only if any plugin under it matched.
|
|
253
|
-
// We build a map from category item id -> whether any plugin below matched.
|
|
117
|
+
// Walk allItems sequentially: include a category only if any plugin below matched
|
|
254
118
|
const categoryHasMatch = new Map<string, boolean>();
|
|
255
119
|
let currentCategoryId: string | null = null;
|
|
256
120
|
for (const item of allItems) {
|
|
257
|
-
if (item.
|
|
121
|
+
if (item.kind === "category") {
|
|
258
122
|
currentCategoryId = item.id;
|
|
259
123
|
if (!categoryHasMatch.has(item.id)) {
|
|
260
124
|
categoryHasMatch.set(item.id, false);
|
|
261
125
|
}
|
|
262
|
-
} else if (item.
|
|
126
|
+
} else if (item.kind === "plugin" && currentCategoryId) {
|
|
263
127
|
if (matchedPluginIds.has(item.id)) {
|
|
264
128
|
categoryHasMatch.set(currentCategoryId, true);
|
|
265
129
|
}
|
|
266
130
|
}
|
|
267
131
|
}
|
|
268
132
|
|
|
269
|
-
const result:
|
|
133
|
+
const result: PluginBrowserItem[] = [];
|
|
270
134
|
let currentCatIncluded = false;
|
|
271
135
|
currentCategoryId = null;
|
|
272
136
|
|
|
273
137
|
for (const item of allItems) {
|
|
274
|
-
if (item.
|
|
138
|
+
if (item.kind === "category") {
|
|
275
139
|
currentCategoryId = item.id;
|
|
276
140
|
currentCatIncluded = categoryHasMatch.get(item.id) === true;
|
|
277
|
-
if (currentCatIncluded)
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
} else if (item.type === "plugin" && currentCatIncluded) {
|
|
141
|
+
if (currentCatIncluded) result.push(item);
|
|
142
|
+
} else if (item.kind === "plugin" && currentCatIncluded) {
|
|
281
143
|
if (matchedPluginIds.has(item.id)) {
|
|
282
144
|
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
283
|
-
result.push({ ...item,
|
|
284
|
-
_matches?: number[];
|
|
285
|
-
});
|
|
145
|
+
result.push({ ...item, matches: matched?.matches });
|
|
286
146
|
}
|
|
287
147
|
}
|
|
288
148
|
}
|
|
@@ -290,37 +150,28 @@ export function PluginsScreen() {
|
|
|
290
150
|
return result;
|
|
291
151
|
}, [allItems, pluginsState.searchQuery]);
|
|
292
152
|
|
|
293
|
-
|
|
294
|
-
const selectableItems = useMemo(() => {
|
|
295
|
-
return filteredItems.filter(
|
|
296
|
-
(item) => item.type === "plugin" || item.type === "category",
|
|
297
|
-
);
|
|
298
|
-
}, [filteredItems]);
|
|
153
|
+
const selectableItems = useMemo(() => filteredItems, [filteredItems]);
|
|
299
154
|
|
|
300
|
-
// Keyboard handling
|
|
155
|
+
// Keyboard handling
|
|
301
156
|
useKeyboard((event) => {
|
|
302
157
|
if (state.modal) return;
|
|
303
158
|
|
|
304
159
|
const hasQuery = pluginsState.searchQuery.length > 0;
|
|
305
160
|
|
|
306
|
-
// Escape: always clear search state fully
|
|
307
161
|
if (event.name === "escape") {
|
|
308
162
|
if (hasQuery || isSearchActive) {
|
|
309
163
|
dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
|
|
310
164
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
311
165
|
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
312
166
|
}
|
|
313
|
-
// Don't return — let GlobalKeyHandler handle Escape too (for quit)
|
|
314
167
|
return;
|
|
315
168
|
}
|
|
316
169
|
|
|
317
|
-
// Backspace: remove last char from search query
|
|
318
170
|
if (event.name === "backspace" || event.name === "delete") {
|
|
319
171
|
if (hasQuery) {
|
|
320
172
|
const newQuery = pluginsState.searchQuery.slice(0, -1);
|
|
321
173
|
dispatch({ type: "PLUGINS_SET_SEARCH", query: newQuery });
|
|
322
174
|
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
323
|
-
// If query becomes empty, exit search mode
|
|
324
175
|
if (newQuery.length === 0) {
|
|
325
176
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
326
177
|
}
|
|
@@ -328,54 +179,43 @@ export function PluginsScreen() {
|
|
|
328
179
|
return;
|
|
329
180
|
}
|
|
330
181
|
|
|
331
|
-
// Navigation — always works; exits search mode on navigate
|
|
332
182
|
if (event.name === "up" || event.name === "k") {
|
|
333
183
|
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
334
|
-
|
|
335
|
-
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
184
|
+
dispatch({ type: "PLUGINS_SELECT", index: Math.max(0, pluginsState.selectedIndex - 1) });
|
|
336
185
|
return;
|
|
337
186
|
}
|
|
338
187
|
if (event.name === "down" || event.name === "j") {
|
|
339
188
|
if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
pluginsState.selectedIndex + 1,
|
|
343
|
-
);
|
|
344
|
-
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
189
|
+
dispatch({
|
|
190
|
+
type: "PLUGINS_SELECT",
|
|
191
|
+
index: Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1),
|
|
192
|
+
});
|
|
345
193
|
return;
|
|
346
194
|
}
|
|
347
195
|
|
|
348
|
-
|
|
349
|
-
if (event.name === "enter") {
|
|
196
|
+
if (event.name === "enter" || event.name === "return") {
|
|
350
197
|
if (isSearchActive) {
|
|
351
198
|
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
352
|
-
// Keep the query — filter stays active, shortcuts resume
|
|
353
199
|
return;
|
|
354
200
|
}
|
|
355
201
|
handleSelect();
|
|
356
202
|
return;
|
|
357
203
|
}
|
|
358
204
|
|
|
359
|
-
// Collapse/expand marketplace — always works
|
|
360
205
|
if (
|
|
361
206
|
(event.name === "left" ||
|
|
362
207
|
event.name === "right" ||
|
|
363
208
|
event.name === "<" ||
|
|
364
209
|
event.name === ">") &&
|
|
365
|
-
selectableItems[pluginsState.selectedIndex]?.
|
|
210
|
+
selectableItems[pluginsState.selectedIndex]?.kind === "category"
|
|
366
211
|
) {
|
|
367
212
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
368
|
-
if (item?.
|
|
369
|
-
dispatch({
|
|
370
|
-
type: "PLUGINS_TOGGLE_MARKETPLACE",
|
|
371
|
-
name: item.marketplace.name,
|
|
372
|
-
});
|
|
213
|
+
if (item?.kind === "category") {
|
|
214
|
+
dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: item.marketplace.name });
|
|
373
215
|
}
|
|
374
216
|
return;
|
|
375
217
|
}
|
|
376
218
|
|
|
377
|
-
// When actively typing in search, letters go to the query
|
|
378
|
-
// After Enter (isSearchActive=false, hasQuery=true), shortcuts resume
|
|
379
219
|
if (isSearchActive) {
|
|
380
220
|
if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
|
|
381
221
|
dispatch({
|
|
@@ -387,15 +227,11 @@ export function PluginsScreen() {
|
|
|
387
227
|
return;
|
|
388
228
|
}
|
|
389
229
|
|
|
390
|
-
// Action shortcuts work when not actively typing (even with filter visible)
|
|
391
|
-
|
|
392
|
-
// Start explicit search mode with /
|
|
393
230
|
if (event.name === "/") {
|
|
394
231
|
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
395
232
|
return;
|
|
396
233
|
}
|
|
397
234
|
|
|
398
|
-
// Action shortcuts (only when query is empty)
|
|
399
235
|
if (event.name === "r") handleRefresh();
|
|
400
236
|
else if (event.name === "n") handleShowAddMarketplaceInstructions();
|
|
401
237
|
else if (event.name === "t") handleShowTeamConfigHelp();
|
|
@@ -405,13 +241,10 @@ export function PluginsScreen() {
|
|
|
405
241
|
else if (event.name === "U") handleUpdate();
|
|
406
242
|
else if (event.name === "a") handleUpdateAll();
|
|
407
243
|
else if (event.name === "s") handleSaveAsProfile();
|
|
408
|
-
// "/" to enter search mode
|
|
409
|
-
else if (event.name === "/") {
|
|
410
|
-
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
411
|
-
}
|
|
412
244
|
});
|
|
413
245
|
|
|
414
|
-
//
|
|
246
|
+
// ── Action handlers ────────────────────────────────────────────────────────
|
|
247
|
+
|
|
415
248
|
const handleRefresh = async () => {
|
|
416
249
|
progress.show("Refreshing cache...");
|
|
417
250
|
try {
|
|
@@ -421,7 +254,6 @@ export function PluginsScreen() {
|
|
|
421
254
|
clearMarketplaceCache();
|
|
422
255
|
progress.hide();
|
|
423
256
|
|
|
424
|
-
// Build message
|
|
425
257
|
let message =
|
|
426
258
|
"Cache refreshed.\n\n" +
|
|
427
259
|
"To update marketplaces from GitHub, run in Claude Code:\n" +
|
|
@@ -487,34 +319,26 @@ export function PluginsScreen() {
|
|
|
487
319
|
};
|
|
488
320
|
|
|
489
321
|
/**
|
|
490
|
-
* Collect environment variables required by a plugin's MCP servers
|
|
491
|
-
* Prompts user for missing values and saves to local settings
|
|
322
|
+
* Collect environment variables required by a plugin's MCP servers.
|
|
492
323
|
*/
|
|
493
324
|
const collectPluginEnvVars = async (
|
|
494
325
|
pluginName: string,
|
|
495
326
|
marketplace: string,
|
|
496
327
|
): Promise<boolean> => {
|
|
497
328
|
try {
|
|
498
|
-
// Get plugin source path from marketplace manifest
|
|
499
329
|
const pluginSource = await getPluginSourcePath(marketplace, pluginName);
|
|
500
|
-
if (!pluginSource) return true;
|
|
330
|
+
if (!pluginSource) return true;
|
|
501
331
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
marketplace,
|
|
505
|
-
pluginSource,
|
|
506
|
-
);
|
|
507
|
-
if (requirements.length === 0) return true; // No env vars needed
|
|
332
|
+
const requirements = await getPluginEnvRequirements(marketplace, pluginSource);
|
|
333
|
+
if (requirements.length === 0) return true;
|
|
508
334
|
|
|
509
|
-
// Get existing env vars
|
|
510
335
|
const existingEnvVars = await getMcpEnvVars(state.projectPath);
|
|
511
336
|
const missingVars = requirements.filter(
|
|
512
337
|
(req) => !existingEnvVars[req.name] && !process.env[req.name],
|
|
513
338
|
);
|
|
514
339
|
|
|
515
|
-
if (missingVars.length === 0) return true;
|
|
340
|
+
if (missingVars.length === 0) return true;
|
|
516
341
|
|
|
517
|
-
// Ask user if they want to configure MCP server env vars now
|
|
518
342
|
const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
|
|
519
343
|
const wantToConfigure = await modal.confirm(
|
|
520
344
|
"Configure MCP Servers?",
|
|
@@ -527,12 +351,10 @@ export function PluginsScreen() {
|
|
|
527
351
|
"You can configure these variables later in the Environment Variables screen (press 4).",
|
|
528
352
|
"info",
|
|
529
353
|
);
|
|
530
|
-
return true;
|
|
354
|
+
return true;
|
|
531
355
|
}
|
|
532
356
|
|
|
533
|
-
// Collect each missing env var
|
|
534
357
|
for (const req of missingVars) {
|
|
535
|
-
// Check if value exists in process.env
|
|
536
358
|
const existingProcessEnv = process.env[req.name];
|
|
537
359
|
if (existingProcessEnv) {
|
|
538
360
|
const useExisting = await modal.confirm(
|
|
@@ -540,13 +362,11 @@ export function PluginsScreen() {
|
|
|
540
362
|
`${req.name} is already set in your environment.\n\nUse the existing value?`,
|
|
541
363
|
);
|
|
542
364
|
if (useExisting) {
|
|
543
|
-
// Store reference to env var instead of literal value
|
|
544
365
|
await setMcpEnvVar(req.name, `\${${req.name}}`, state.projectPath);
|
|
545
366
|
continue;
|
|
546
367
|
}
|
|
547
368
|
}
|
|
548
369
|
|
|
549
|
-
// Prompt for value
|
|
550
370
|
const value = await modal.input(
|
|
551
371
|
`Configure ${req.serverName}`,
|
|
552
372
|
`${req.label} (required):`,
|
|
@@ -554,13 +374,12 @@ export function PluginsScreen() {
|
|
|
554
374
|
);
|
|
555
375
|
|
|
556
376
|
if (value === null) {
|
|
557
|
-
// User cancelled
|
|
558
377
|
await modal.message(
|
|
559
378
|
"Configuration Incomplete",
|
|
560
379
|
`Skipped remaining configuration.\nYou can configure these later in Environment Variables (press 4).`,
|
|
561
380
|
"info",
|
|
562
381
|
);
|
|
563
|
-
return true;
|
|
382
|
+
return true;
|
|
564
383
|
}
|
|
565
384
|
|
|
566
385
|
if (value) {
|
|
@@ -571,13 +390,12 @@ export function PluginsScreen() {
|
|
|
571
390
|
return true;
|
|
572
391
|
} catch (error) {
|
|
573
392
|
console.error("Error collecting plugin env vars:", error);
|
|
574
|
-
return true;
|
|
393
|
+
return true;
|
|
575
394
|
}
|
|
576
395
|
};
|
|
577
396
|
|
|
578
397
|
/**
|
|
579
|
-
* Install system dependencies required by a plugin's MCP servers
|
|
580
|
-
* Auto-detects available package managers (uv/pip, brew, npm, cargo)
|
|
398
|
+
* Install system dependencies required by a plugin's MCP servers.
|
|
581
399
|
*/
|
|
582
400
|
const installPluginSystemDeps = async (
|
|
583
401
|
pluginName: string,
|
|
@@ -597,13 +415,11 @@ export function PluginsScreen() {
|
|
|
597
415
|
|
|
598
416
|
if (!hasMissing) return;
|
|
599
417
|
|
|
600
|
-
// Build description of what will be installed
|
|
601
418
|
const parts: string[] = [];
|
|
602
419
|
if (missing.pip?.length) parts.push(`pip: ${missing.pip.join(", ")}`);
|
|
603
420
|
if (missing.brew?.length) parts.push(`brew: ${missing.brew.join(", ")}`);
|
|
604
421
|
if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
|
|
605
|
-
if (missing.cargo?.length)
|
|
606
|
-
parts.push(`cargo: ${missing.cargo.join(", ")}`);
|
|
422
|
+
if (missing.cargo?.length) parts.push(`cargo: ${missing.cargo.join(", ")}`);
|
|
607
423
|
|
|
608
424
|
const wantInstall = await modal.confirm(
|
|
609
425
|
"Install Dependencies?",
|
|
@@ -617,9 +433,7 @@ export function PluginsScreen() {
|
|
|
617
433
|
modal.hideModal();
|
|
618
434
|
|
|
619
435
|
if (result.failed.length > 0) {
|
|
620
|
-
const failMsg = result.failed
|
|
621
|
-
.map((f) => `${f.pkg}: ${f.error}`)
|
|
622
|
-
.join("\n");
|
|
436
|
+
const failMsg = result.failed.map((f) => `${f.pkg}: ${f.error}`).join("\n");
|
|
623
437
|
await modal.message(
|
|
624
438
|
"Partial Install",
|
|
625
439
|
`Installed: ${result.installed.length}\nFailed:\n${failMsg}`,
|
|
@@ -638,9 +452,7 @@ export function PluginsScreen() {
|
|
|
638
452
|
};
|
|
639
453
|
|
|
640
454
|
/**
|
|
641
|
-
* Save
|
|
642
|
-
* Claude CLI doesn't update installedPluginVersions in settings.json,
|
|
643
|
-
* so we do it ourselves to keep the TUI version display accurate.
|
|
455
|
+
* Save installed plugin version to settings after CLI install/update.
|
|
644
456
|
*/
|
|
645
457
|
const saveVersionAfterInstall = async (
|
|
646
458
|
pluginId: string,
|
|
@@ -651,11 +463,7 @@ export function PluginsScreen() {
|
|
|
651
463
|
if (scope === "user") {
|
|
652
464
|
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
653
465
|
} else if (scope === "local") {
|
|
654
|
-
await saveLocalInstalledPluginVersion(
|
|
655
|
-
pluginId,
|
|
656
|
-
version,
|
|
657
|
-
state.projectPath,
|
|
658
|
-
);
|
|
466
|
+
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
659
467
|
} else {
|
|
660
468
|
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
661
469
|
}
|
|
@@ -668,20 +476,16 @@ export function PluginsScreen() {
|
|
|
668
476
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
669
477
|
if (!item) return;
|
|
670
478
|
|
|
671
|
-
if (item.
|
|
479
|
+
if (item.kind === "category") {
|
|
672
480
|
const mp = item.marketplace;
|
|
673
481
|
|
|
674
482
|
if (item.marketplaceEnabled) {
|
|
675
483
|
const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
|
|
676
|
-
|
|
677
|
-
// If collapsed, expand first (even if no plugins - they might load)
|
|
678
484
|
if (isCollapsed) {
|
|
679
485
|
dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
|
|
680
|
-
} else if (item.pluginCount
|
|
681
|
-
// If expanded with plugins, collapse
|
|
486
|
+
} else if (item.pluginCount > 0) {
|
|
682
487
|
dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
|
|
683
488
|
} else {
|
|
684
|
-
// If expanded with no plugins, show removal instructions
|
|
685
489
|
await modal.message(
|
|
686
490
|
`Remove ${mp.displayName}?`,
|
|
687
491
|
`To remove this marketplace, run in Claude Code:\n\n` +
|
|
@@ -691,7 +495,6 @@ export function PluginsScreen() {
|
|
|
691
495
|
);
|
|
692
496
|
}
|
|
693
497
|
} else {
|
|
694
|
-
// Show add marketplace instructions
|
|
695
498
|
await modal.message(
|
|
696
499
|
`Add ${mp.displayName}?`,
|
|
697
500
|
`To add this marketplace, run in your terminal:\n\n` +
|
|
@@ -701,11 +504,10 @@ export function PluginsScreen() {
|
|
|
701
504
|
"info",
|
|
702
505
|
);
|
|
703
506
|
}
|
|
704
|
-
} else if (item.
|
|
507
|
+
} else if (item.kind === "plugin") {
|
|
705
508
|
const plugin = item.plugin;
|
|
706
509
|
const latestVersion = plugin.version || "0.0.0";
|
|
707
510
|
|
|
708
|
-
// Build scope options with status info
|
|
709
511
|
const buildScopeLabel = (
|
|
710
512
|
name: string,
|
|
711
513
|
scope: { enabled?: boolean; version?: string } | undefined,
|
|
@@ -713,12 +515,7 @@ export function PluginsScreen() {
|
|
|
713
515
|
) => {
|
|
714
516
|
const installed = scope?.enabled;
|
|
715
517
|
const ver = scope?.version;
|
|
716
|
-
const hasUpdate =
|
|
717
|
-
ver &&
|
|
718
|
-
latestVersion &&
|
|
719
|
-
ver !== latestVersion &&
|
|
720
|
-
latestVersion !== "0.0.0";
|
|
721
|
-
|
|
518
|
+
const hasUpdate = ver && latestVersion && ver !== latestVersion && latestVersion !== "0.0.0";
|
|
722
519
|
let label = installed ? `● ${name}` : `○ ${name}`;
|
|
723
520
|
label += ` (${desc})`;
|
|
724
521
|
if (ver) label += ` v${ver}`;
|
|
@@ -727,29 +524,14 @@ export function PluginsScreen() {
|
|
|
727
524
|
};
|
|
728
525
|
|
|
729
526
|
const scopeOptions = [
|
|
730
|
-
{
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
label: buildScopeLabel("Project", plugin.projectScope, "team"),
|
|
736
|
-
value: "project",
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
label: buildScopeLabel("Local", plugin.localScope, "private"),
|
|
740
|
-
value: "local",
|
|
741
|
-
},
|
|
527
|
+
{ label: buildScopeLabel("User", plugin.userScope, "global"), value: "user" },
|
|
528
|
+
{ label: buildScopeLabel("Project", plugin.projectScope, "team"), value: "project" },
|
|
529
|
+
{ label: buildScopeLabel("Local", plugin.localScope, "private"), value: "local" },
|
|
742
530
|
];
|
|
743
531
|
|
|
744
|
-
const scopeValue = await modal.select(
|
|
745
|
-
|
|
746
|
-
`Select scope to toggle:`,
|
|
747
|
-
scopeOptions,
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
if (scopeValue === null) return; // Cancelled
|
|
532
|
+
const scopeValue = await modal.select(plugin.name, `Select scope to toggle:`, scopeOptions);
|
|
533
|
+
if (scopeValue === null) return;
|
|
751
534
|
|
|
752
|
-
// Determine action based on selected scope's current state
|
|
753
535
|
const selectedScope =
|
|
754
536
|
scopeValue === "user"
|
|
755
537
|
? plugin.userScope
|
|
@@ -759,20 +541,14 @@ export function PluginsScreen() {
|
|
|
759
541
|
const isInstalledInScope = selectedScope?.enabled;
|
|
760
542
|
const installedVersion = selectedScope?.version;
|
|
761
543
|
const scopeLabel =
|
|
762
|
-
scopeValue === "user"
|
|
763
|
-
? "User"
|
|
764
|
-
: scopeValue === "project"
|
|
765
|
-
? "Project"
|
|
766
|
-
: "Local";
|
|
544
|
+
scopeValue === "user" ? "User" : scopeValue === "project" ? "Project" : "Local";
|
|
767
545
|
|
|
768
|
-
// Check if this scope has an update available
|
|
769
546
|
const hasUpdateInScope =
|
|
770
547
|
isInstalledInScope &&
|
|
771
548
|
installedVersion &&
|
|
772
549
|
latestVersion !== "0.0.0" &&
|
|
773
550
|
installedVersion !== latestVersion;
|
|
774
551
|
|
|
775
|
-
// Determine action: update if available, otherwise toggle
|
|
776
552
|
let action: "update" | "install" | "uninstall";
|
|
777
553
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
778
554
|
action = "update";
|
|
@@ -800,8 +576,6 @@ export function PluginsScreen() {
|
|
|
800
576
|
} else {
|
|
801
577
|
await cliInstallPlugin(plugin.id, scope);
|
|
802
578
|
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
803
|
-
|
|
804
|
-
// On fresh install, configure env vars and install system deps
|
|
805
579
|
modal.hideModal();
|
|
806
580
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
807
581
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -819,20 +593,15 @@ export function PluginsScreen() {
|
|
|
819
593
|
|
|
820
594
|
const handleUpdate = async () => {
|
|
821
595
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
822
|
-
if (!item || item.
|
|
596
|
+
if (!item || item.kind !== "plugin" || !item.plugin.hasUpdate) return;
|
|
823
597
|
|
|
824
598
|
const plugin = item.plugin;
|
|
825
|
-
const scope: PluginScope =
|
|
826
|
-
pluginsState.scope === "global" ? "user" : "project";
|
|
599
|
+
const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
|
|
827
600
|
|
|
828
601
|
modal.loading(`Updating ${plugin.name}...`);
|
|
829
602
|
try {
|
|
830
603
|
await cliUpdatePlugin(plugin.id, scope);
|
|
831
|
-
await saveVersionAfterInstall(
|
|
832
|
-
plugin.id,
|
|
833
|
-
plugin.version || "0.0.0",
|
|
834
|
-
scope,
|
|
835
|
-
);
|
|
604
|
+
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
836
605
|
modal.hideModal();
|
|
837
606
|
fetchData();
|
|
838
607
|
} catch (error) {
|
|
@@ -847,18 +616,13 @@ export function PluginsScreen() {
|
|
|
847
616
|
const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
|
|
848
617
|
if (updatable.length === 0) return;
|
|
849
618
|
|
|
850
|
-
const scope: PluginScope =
|
|
851
|
-
pluginsState.scope === "global" ? "user" : "project";
|
|
619
|
+
const scope: PluginScope = pluginsState.scope === "global" ? "user" : "project";
|
|
852
620
|
modal.loading(`Updating ${updatable.length} plugin(s)...`);
|
|
853
621
|
|
|
854
622
|
try {
|
|
855
623
|
for (const plugin of updatable) {
|
|
856
624
|
await cliUpdatePlugin(plugin.id, scope);
|
|
857
|
-
await saveVersionAfterInstall(
|
|
858
|
-
plugin.id,
|
|
859
|
-
plugin.version || "0.0.0",
|
|
860
|
-
scope,
|
|
861
|
-
);
|
|
625
|
+
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
862
626
|
}
|
|
863
627
|
modal.hideModal();
|
|
864
628
|
fetchData();
|
|
@@ -868,17 +632,15 @@ export function PluginsScreen() {
|
|
|
868
632
|
}
|
|
869
633
|
};
|
|
870
634
|
|
|
871
|
-
// Scope-specific toggle (install if not installed, uninstall if installed)
|
|
872
635
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
873
636
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
874
|
-
if (!item || item.
|
|
637
|
+
if (!item || item.kind !== "plugin") return;
|
|
875
638
|
|
|
876
639
|
const plugin = item.plugin;
|
|
877
640
|
const latestVersion = plugin.version || "0.0.0";
|
|
878
641
|
const scopeLabel =
|
|
879
642
|
scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
|
|
880
643
|
|
|
881
|
-
// Check if installed in this specific scope
|
|
882
644
|
const scopeData =
|
|
883
645
|
scope === "user"
|
|
884
646
|
? plugin.userScope
|
|
@@ -888,20 +650,12 @@ export function PluginsScreen() {
|
|
|
888
650
|
const isInstalledInScope = scopeData?.enabled;
|
|
889
651
|
const installedVersion = scopeData?.version;
|
|
890
652
|
|
|
891
|
-
// Also check if installed in ANY scope (for the toggle behavior)
|
|
892
|
-
const isInstalledAnywhere = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
893
|
-
|
|
894
|
-
// Check if this scope has an update available
|
|
895
653
|
const hasUpdateInScope =
|
|
896
654
|
isInstalledInScope &&
|
|
897
655
|
installedVersion &&
|
|
898
656
|
latestVersion !== "0.0.0" &&
|
|
899
657
|
installedVersion !== latestVersion;
|
|
900
658
|
|
|
901
|
-
// Determine action for THIS scope:
|
|
902
|
-
// - installed in this scope + has update → update
|
|
903
|
-
// - installed in this scope → uninstall from this scope
|
|
904
|
-
// - not installed in this scope → install to this scope
|
|
905
659
|
let action: "update" | "install" | "uninstall";
|
|
906
660
|
if (isInstalledInScope && hasUpdateInScope) {
|
|
907
661
|
action = "update";
|
|
@@ -928,8 +682,6 @@ export function PluginsScreen() {
|
|
|
928
682
|
} else {
|
|
929
683
|
await cliInstallPlugin(plugin.id, scope);
|
|
930
684
|
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
931
|
-
|
|
932
|
-
// On fresh install, configure env vars and install system deps
|
|
933
685
|
modal.hideModal();
|
|
934
686
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
935
687
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -945,7 +697,6 @@ export function PluginsScreen() {
|
|
|
945
697
|
};
|
|
946
698
|
|
|
947
699
|
const handleSaveAsProfile = async () => {
|
|
948
|
-
// Read current enabledPlugins from project settings
|
|
949
700
|
const settings = await readSettings(state.projectPath);
|
|
950
701
|
const enabledPlugins = settings.enabledPlugins ?? {};
|
|
951
702
|
|
|
@@ -956,14 +707,8 @@ export function PluginsScreen() {
|
|
|
956
707
|
"Save to scope",
|
|
957
708
|
"Where should this profile be saved?",
|
|
958
709
|
[
|
|
959
|
-
{
|
|
960
|
-
|
|
961
|
-
value: "user",
|
|
962
|
-
},
|
|
963
|
-
{
|
|
964
|
-
label: "Project — .claude/profiles.json (shared with team via git)",
|
|
965
|
-
value: "project",
|
|
966
|
-
},
|
|
710
|
+
{ label: "User — ~/.claude/profiles.json (available everywhere)", value: "user" },
|
|
711
|
+
{ label: "Project — .claude/profiles.json (shared with team via git)", value: "project" },
|
|
967
712
|
],
|
|
968
713
|
);
|
|
969
714
|
if (scopeChoice === null) return;
|
|
@@ -985,67 +730,8 @@ export function PluginsScreen() {
|
|
|
985
730
|
}
|
|
986
731
|
};
|
|
987
732
|
|
|
988
|
-
|
|
989
|
-
const item = selectableItems[pluginsState.selectedIndex];
|
|
990
|
-
if (!item || item.type !== "plugin" || !item.plugin) return;
|
|
991
|
-
|
|
992
|
-
const plugin = item.plugin;
|
|
993
|
-
|
|
994
|
-
// Build list of scopes where plugin is installed
|
|
995
|
-
const installedScopes: { label: string; value: string }[] = [];
|
|
996
|
-
if (plugin.userScope?.enabled) {
|
|
997
|
-
const ver = plugin.userScope.version
|
|
998
|
-
? ` v${plugin.userScope.version}`
|
|
999
|
-
: "";
|
|
1000
|
-
installedScopes.push({ label: `User (global)${ver}`, value: "user" });
|
|
1001
|
-
}
|
|
1002
|
-
if (plugin.projectScope?.enabled) {
|
|
1003
|
-
const ver = plugin.projectScope.version
|
|
1004
|
-
? ` v${plugin.projectScope.version}`
|
|
1005
|
-
: "";
|
|
1006
|
-
installedScopes.push({ label: `Project${ver}`, value: "project" });
|
|
1007
|
-
}
|
|
1008
|
-
if (plugin.localScope?.enabled) {
|
|
1009
|
-
const ver = plugin.localScope.version
|
|
1010
|
-
? ` v${plugin.localScope.version}`
|
|
1011
|
-
: "";
|
|
1012
|
-
installedScopes.push({ label: `Local${ver}`, value: "local" });
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (installedScopes.length === 0) {
|
|
1016
|
-
await modal.message(
|
|
1017
|
-
"Not Installed",
|
|
1018
|
-
`${plugin.name} is not installed in any scope.`,
|
|
1019
|
-
"info",
|
|
1020
|
-
);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
const scopeValue = await modal.select(
|
|
1025
|
-
`Uninstall ${plugin.name}`,
|
|
1026
|
-
`Installed in ${installedScopes.length} scope(s):`,
|
|
1027
|
-
installedScopes,
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
if (scopeValue === null) return; // Cancelled
|
|
1031
|
-
|
|
1032
|
-
modal.loading(`Uninstalling ${plugin.name}...`);
|
|
1033
|
-
|
|
1034
|
-
try {
|
|
1035
|
-
await cliUninstallPlugin(
|
|
1036
|
-
plugin.id,
|
|
1037
|
-
scopeValue as PluginScope,
|
|
1038
|
-
state.projectPath,
|
|
1039
|
-
);
|
|
1040
|
-
modal.hideModal();
|
|
1041
|
-
fetchData();
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
modal.hideModal();
|
|
1044
|
-
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
733
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
1047
734
|
|
|
1048
|
-
// Render loading state
|
|
1049
735
|
if (
|
|
1050
736
|
pluginsState.marketplaces.status === "loading" ||
|
|
1051
737
|
pluginsState.plugins.status === "loading"
|
|
@@ -1060,7 +746,6 @@ export function PluginsScreen() {
|
|
|
1060
746
|
);
|
|
1061
747
|
}
|
|
1062
748
|
|
|
1063
|
-
// Render error state
|
|
1064
749
|
if (
|
|
1065
750
|
pluginsState.marketplaces.status === "error" ||
|
|
1066
751
|
pluginsState.plugins.status === "error"
|
|
@@ -1075,391 +760,18 @@ export function PluginsScreen() {
|
|
|
1075
760
|
);
|
|
1076
761
|
}
|
|
1077
762
|
|
|
1078
|
-
// Get selected item for detail panel
|
|
1079
763
|
const selectedItem = selectableItems[pluginsState.selectedIndex];
|
|
1080
764
|
|
|
1081
|
-
// Render item with fuzzy highlight support
|
|
1082
|
-
const renderListItem = (
|
|
1083
|
-
item: ListItem,
|
|
1084
|
-
_idx: number,
|
|
1085
|
-
isSelected: boolean,
|
|
1086
|
-
) => {
|
|
1087
|
-
if (item.type === "category" && item.marketplace) {
|
|
1088
|
-
const mp = item.marketplace;
|
|
1089
|
-
// Differentiate marketplace types with appropriate badges
|
|
1090
|
-
let statusText = "";
|
|
1091
|
-
let statusColor = "green";
|
|
1092
|
-
if (item.marketplaceEnabled) {
|
|
1093
|
-
if (item.isCommunitySection) {
|
|
1094
|
-
statusText = "3rd Party";
|
|
1095
|
-
statusColor = "gray";
|
|
1096
|
-
} else if (mp.name === "claude-plugins-official") {
|
|
1097
|
-
statusText = "★ Official";
|
|
1098
|
-
statusColor = "yellow";
|
|
1099
|
-
} else if (mp.name === "claude-code-plugins") {
|
|
1100
|
-
statusText = "⚠ Deprecated";
|
|
1101
|
-
statusColor = "gray";
|
|
1102
|
-
} else if (mp.official) {
|
|
1103
|
-
statusText = "★ Official";
|
|
1104
|
-
statusColor = "yellow";
|
|
1105
|
-
} else {
|
|
1106
|
-
statusText = "✓ Added";
|
|
1107
|
-
statusColor = "green";
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
if (isSelected) {
|
|
1112
|
-
const arrow = item.isExpanded ? "▼" : "▶";
|
|
1113
|
-
const count =
|
|
1114
|
-
item.pluginCount !== undefined && item.pluginCount > 0
|
|
1115
|
-
? ` (${item.pluginCount})`
|
|
1116
|
-
: "";
|
|
1117
|
-
return (
|
|
1118
|
-
<text bg="magenta" fg="white">
|
|
1119
|
-
<strong>
|
|
1120
|
-
{" "}
|
|
1121
|
-
{arrow} {mp.displayName}
|
|
1122
|
-
{count}{" "}
|
|
1123
|
-
</strong>
|
|
1124
|
-
</text>
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
return (
|
|
1129
|
-
<CategoryHeader
|
|
1130
|
-
title={mp.displayName}
|
|
1131
|
-
expanded={item.isExpanded}
|
|
1132
|
-
count={item.pluginCount}
|
|
1133
|
-
status={statusText}
|
|
1134
|
-
statusColor={statusColor}
|
|
1135
|
-
/>
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
if (item.type === "plugin" && item.plugin) {
|
|
1140
|
-
const plugin = item.plugin;
|
|
1141
|
-
const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
1142
|
-
|
|
1143
|
-
// Build scope parts for colored rendering
|
|
1144
|
-
const hasUser = plugin.userScope?.enabled;
|
|
1145
|
-
const hasProject = plugin.projectScope?.enabled;
|
|
1146
|
-
const hasLocal = plugin.localScope?.enabled;
|
|
1147
|
-
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
1148
|
-
|
|
1149
|
-
// Build version string
|
|
1150
|
-
let versionStr = "";
|
|
1151
|
-
if (plugin.isOrphaned) {
|
|
1152
|
-
versionStr = " deprecated";
|
|
1153
|
-
} else if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
1154
|
-
versionStr = ` v${plugin.installedVersion}`;
|
|
1155
|
-
if (plugin.hasUpdate && plugin.version) {
|
|
1156
|
-
versionStr += ` → v${plugin.version}`;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Get fuzzy match highlights if available
|
|
1161
|
-
const matches = (item as ListItem & { _matches?: number[] })._matches;
|
|
1162
|
-
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
1163
|
-
|
|
1164
|
-
if (isSelected) {
|
|
1165
|
-
return (
|
|
1166
|
-
<text bg="magenta" fg="white">
|
|
1167
|
-
{" "}
|
|
1168
|
-
<span>{hasUser ? "■" : "□"}</span>
|
|
1169
|
-
<span>{hasProject ? "■" : "□"}</span>
|
|
1170
|
-
<span>{hasLocal ? "■" : "□"}</span>
|
|
1171
|
-
{" "}{plugin.name}{versionStr}{" "}
|
|
1172
|
-
</text>
|
|
1173
|
-
);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const displayName = segments
|
|
1177
|
-
? segments.map((seg) => seg.text).join("")
|
|
1178
|
-
: plugin.name;
|
|
1179
|
-
|
|
1180
|
-
if (plugin.isOrphaned) {
|
|
1181
|
-
const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
|
|
1182
|
-
? ` v${plugin.installedVersion}` : "";
|
|
1183
|
-
return (
|
|
1184
|
-
<text>
|
|
1185
|
-
<span fg="red"> ■■■ </span>
|
|
1186
|
-
<span fg="gray">{displayName}</span>
|
|
1187
|
-
{ver && <span fg="yellow">{ver}</span>}
|
|
1188
|
-
<span fg="red"> deprecated</span>
|
|
1189
|
-
</text>
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
return (
|
|
1194
|
-
<text>
|
|
1195
|
-
<span> </span>
|
|
1196
|
-
<span fg={hasUser ? "cyan" : "#333333"}>■</span>
|
|
1197
|
-
<span fg={hasProject ? "green" : "#333333"}>■</span>
|
|
1198
|
-
<span fg={hasLocal ? "yellow" : "#333333"}>■</span>
|
|
1199
|
-
<span> </span>
|
|
1200
|
-
<span fg={hasAnyScope ? "white" : "gray"}>{displayName}</span>
|
|
1201
|
-
<span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
|
|
1202
|
-
</text>
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
return <text fg="gray">{item.label}</text>;
|
|
1207
|
-
};
|
|
1208
|
-
|
|
1209
|
-
// Render detail content - compact to fit in available space
|
|
1210
|
-
const renderDetail = () => {
|
|
1211
|
-
if (!selectedItem) {
|
|
1212
|
-
return <text fg="gray">Select an item</text>;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (selectedItem.type === "category" && selectedItem.marketplace) {
|
|
1216
|
-
const mp = selectedItem.marketplace;
|
|
1217
|
-
const isEnabled = selectedItem.marketplaceEnabled;
|
|
1218
|
-
|
|
1219
|
-
// Get appropriate badge for marketplace type
|
|
1220
|
-
const getBadge = () => {
|
|
1221
|
-
if (selectedItem.isCommunitySection) return " 3rd Party";
|
|
1222
|
-
if (mp.name === "claude-plugins-official") return " ★";
|
|
1223
|
-
if (mp.name === "claude-code-plugins") return " ⚠";
|
|
1224
|
-
if (mp.official) return " ★";
|
|
1225
|
-
return "";
|
|
1226
|
-
};
|
|
1227
|
-
|
|
1228
|
-
// Determine action hint based on state
|
|
1229
|
-
const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
|
|
1230
|
-
const hasPlugins = (selectedItem.pluginCount || 0) > 0;
|
|
1231
|
-
let actionHint = "Add";
|
|
1232
|
-
if (isEnabled) {
|
|
1233
|
-
if (isCollapsed) {
|
|
1234
|
-
actionHint = "Expand";
|
|
1235
|
-
} else if (hasPlugins) {
|
|
1236
|
-
actionHint = "Collapse";
|
|
1237
|
-
} else {
|
|
1238
|
-
actionHint = "Remove";
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
return (
|
|
1243
|
-
<box flexDirection="column">
|
|
1244
|
-
<text fg="cyan">
|
|
1245
|
-
<strong>
|
|
1246
|
-
{mp.displayName}
|
|
1247
|
-
{getBadge()}
|
|
1248
|
-
</strong>
|
|
1249
|
-
</text>
|
|
1250
|
-
<text fg="gray">{mp.description || "No description"}</text>
|
|
1251
|
-
<text fg={isEnabled ? "green" : "gray"}>
|
|
1252
|
-
{isEnabled ? "● Added" : "○ Not added"}
|
|
1253
|
-
</text>
|
|
1254
|
-
<text fg="#5c9aff">github.com/{mp.source.repo}</text>
|
|
1255
|
-
<text>Plugins: {selectedItem.pluginCount || 0}</text>
|
|
1256
|
-
<box marginTop={1}>
|
|
1257
|
-
<text bg={isEnabled ? "cyan" : "green"} fg="black">
|
|
1258
|
-
{" "}
|
|
1259
|
-
Enter{" "}
|
|
1260
|
-
</text>
|
|
1261
|
-
<text fg="gray"> {actionHint}</text>
|
|
1262
|
-
</box>
|
|
1263
|
-
{isEnabled && (
|
|
1264
|
-
<box>
|
|
1265
|
-
<text fg="gray">← → to expand/collapse</text>
|
|
1266
|
-
</box>
|
|
1267
|
-
)}
|
|
1268
|
-
</box>
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (selectedItem.type === "plugin" && selectedItem.plugin) {
|
|
1273
|
-
const plugin = selectedItem.plugin;
|
|
1274
|
-
const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
|
|
1275
|
-
|
|
1276
|
-
// Orphaned/deprecated plugin
|
|
1277
|
-
if (plugin.isOrphaned) {
|
|
1278
|
-
return (
|
|
1279
|
-
<box flexDirection="column">
|
|
1280
|
-
<box justifyContent="center">
|
|
1281
|
-
<text bg="yellow" fg="black"><strong> {plugin.name} — DEPRECATED </strong></text>
|
|
1282
|
-
</box>
|
|
1283
|
-
<box marginTop={1}>
|
|
1284
|
-
<text fg="yellow">This plugin is no longer in the marketplace.</text>
|
|
1285
|
-
</box>
|
|
1286
|
-
<box marginTop={1}>
|
|
1287
|
-
<text fg="gray">It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up.</text>
|
|
1288
|
-
</box>
|
|
1289
|
-
{isInstalled && (
|
|
1290
|
-
<box flexDirection="column" marginTop={2}>
|
|
1291
|
-
<box>
|
|
1292
|
-
<text bg="red" fg="white"> d </text>
|
|
1293
|
-
<text> Uninstall (recommended)</text>
|
|
1294
|
-
</box>
|
|
1295
|
-
</box>
|
|
1296
|
-
)}
|
|
1297
|
-
</box>
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// Build component counts
|
|
1302
|
-
const components: string[] = [];
|
|
1303
|
-
if (plugin.agents?.length)
|
|
1304
|
-
components.push(`${plugin.agents.length} agents`);
|
|
1305
|
-
if (plugin.commands?.length)
|
|
1306
|
-
components.push(`${plugin.commands.length} commands`);
|
|
1307
|
-
if (plugin.skills?.length)
|
|
1308
|
-
components.push(`${plugin.skills.length} skills`);
|
|
1309
|
-
if (plugin.mcpServers?.length)
|
|
1310
|
-
components.push(`${plugin.mcpServers.length} MCP`);
|
|
1311
|
-
if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
|
|
1312
|
-
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// Show version only if valid (not null, not 0.0.0)
|
|
1316
|
-
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
1317
|
-
const showInstalledVersion =
|
|
1318
|
-
plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
1319
|
-
|
|
1320
|
-
return (
|
|
1321
|
-
<box flexDirection="column">
|
|
1322
|
-
{/* Plugin name header - centered */}
|
|
1323
|
-
<box justifyContent="center">
|
|
1324
|
-
<text bg="magenta" fg="white">
|
|
1325
|
-
<strong>
|
|
1326
|
-
{" "}
|
|
1327
|
-
{plugin.name}
|
|
1328
|
-
{plugin.hasUpdate ? " ⬆" : ""}{" "}
|
|
1329
|
-
</strong>
|
|
1330
|
-
</text>
|
|
1331
|
-
</box>
|
|
1332
|
-
|
|
1333
|
-
{/* Status line */}
|
|
1334
|
-
<box marginTop={1}>
|
|
1335
|
-
{isInstalled ? (
|
|
1336
|
-
<text fg="green">● Installed</text>
|
|
1337
|
-
) : (
|
|
1338
|
-
<text fg="gray">○ Not installed</text>
|
|
1339
|
-
)}
|
|
1340
|
-
</box>
|
|
1341
|
-
|
|
1342
|
-
{/* Description */}
|
|
1343
|
-
<box marginTop={1} marginBottom={1}>
|
|
1344
|
-
<text fg="white">{plugin.description}</text>
|
|
1345
|
-
</box>
|
|
1346
|
-
|
|
1347
|
-
{/* Metadata */}
|
|
1348
|
-
{showVersion && (
|
|
1349
|
-
<text>
|
|
1350
|
-
<span>Version </span>
|
|
1351
|
-
<span fg="#5c9aff">v{plugin.version}</span>
|
|
1352
|
-
{showInstalledVersion &&
|
|
1353
|
-
plugin.installedVersion !== plugin.version && (
|
|
1354
|
-
<span> (v{plugin.installedVersion} installed)</span>
|
|
1355
|
-
)}
|
|
1356
|
-
</text>
|
|
1357
|
-
)}
|
|
1358
|
-
{plugin.category && (
|
|
1359
|
-
<text>
|
|
1360
|
-
<span>Category </span>
|
|
1361
|
-
<span fg="magenta">{plugin.category}</span>
|
|
1362
|
-
</text>
|
|
1363
|
-
)}
|
|
1364
|
-
{plugin.author && (
|
|
1365
|
-
<text>
|
|
1366
|
-
<span>Author </span>
|
|
1367
|
-
<span>{plugin.author.name}</span>
|
|
1368
|
-
</text>
|
|
1369
|
-
)}
|
|
1370
|
-
{components.length > 0 && (
|
|
1371
|
-
<text>
|
|
1372
|
-
<span>Contains </span>
|
|
1373
|
-
<span fg="yellow">{components.join(" · ")}</span>
|
|
1374
|
-
</text>
|
|
1375
|
-
)}
|
|
1376
|
-
|
|
1377
|
-
{/* Scope Status with shortcuts - each scope has its own color */}
|
|
1378
|
-
<box flexDirection="column" marginTop={1}>
|
|
1379
|
-
<text>────────────────────────</text>
|
|
1380
|
-
<text>
|
|
1381
|
-
<strong>Scopes:</strong>
|
|
1382
|
-
</text>
|
|
1383
|
-
<box marginTop={1} flexDirection="column">
|
|
1384
|
-
<text>
|
|
1385
|
-
<span bg="cyan" fg="black">
|
|
1386
|
-
{" "}
|
|
1387
|
-
u{" "}
|
|
1388
|
-
</span>
|
|
1389
|
-
<span fg={plugin.userScope?.enabled ? "cyan" : "gray"}>
|
|
1390
|
-
{plugin.userScope?.enabled ? " ● " : " ○ "}
|
|
1391
|
-
</span>
|
|
1392
|
-
<span fg="cyan">User</span>
|
|
1393
|
-
<span> global</span>
|
|
1394
|
-
{plugin.userScope?.version && (
|
|
1395
|
-
<span fg="cyan"> v{plugin.userScope.version}</span>
|
|
1396
|
-
)}
|
|
1397
|
-
</text>
|
|
1398
|
-
<text>
|
|
1399
|
-
<span bg="green" fg="black">
|
|
1400
|
-
{" "}
|
|
1401
|
-
p{" "}
|
|
1402
|
-
</span>
|
|
1403
|
-
<span fg={plugin.projectScope?.enabled ? "green" : "gray"}>
|
|
1404
|
-
{plugin.projectScope?.enabled ? " ● " : " ○ "}
|
|
1405
|
-
</span>
|
|
1406
|
-
<span fg="green">Project</span>
|
|
1407
|
-
<span> team</span>
|
|
1408
|
-
{plugin.projectScope?.version && (
|
|
1409
|
-
<span fg="green"> v{plugin.projectScope.version}</span>
|
|
1410
|
-
)}
|
|
1411
|
-
</text>
|
|
1412
|
-
<text>
|
|
1413
|
-
<span bg="yellow" fg="black">
|
|
1414
|
-
{" "}
|
|
1415
|
-
l{" "}
|
|
1416
|
-
</span>
|
|
1417
|
-
<span fg={plugin.localScope?.enabled ? "yellow" : "gray"}>
|
|
1418
|
-
{plugin.localScope?.enabled ? " ● " : " ○ "}
|
|
1419
|
-
</span>
|
|
1420
|
-
<span fg="yellow">Local</span>
|
|
1421
|
-
<span> private</span>
|
|
1422
|
-
{plugin.localScope?.version && (
|
|
1423
|
-
<span fg="yellow"> v{plugin.localScope.version}</span>
|
|
1424
|
-
)}
|
|
1425
|
-
</text>
|
|
1426
|
-
</box>
|
|
1427
|
-
</box>
|
|
1428
|
-
|
|
1429
|
-
{/* Additional actions */}
|
|
1430
|
-
{isInstalled && (
|
|
1431
|
-
<box flexDirection="column" marginTop={1}>
|
|
1432
|
-
{plugin.hasUpdate && (
|
|
1433
|
-
<box>
|
|
1434
|
-
<text bg="magenta" fg="white">
|
|
1435
|
-
{" "}
|
|
1436
|
-
U{" "}
|
|
1437
|
-
</text>
|
|
1438
|
-
<text> Update to v{plugin.version}</text>
|
|
1439
|
-
</box>
|
|
1440
|
-
)}
|
|
1441
|
-
</box>
|
|
1442
|
-
)}
|
|
1443
|
-
</box>
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
return null;
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
765
|
const footerHints = isSearchActive
|
|
1451
|
-
|
|
1452
|
-
|
|
766
|
+
? "type to filter │ Enter:done │ Esc:clear"
|
|
767
|
+
: "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
|
|
1453
768
|
|
|
1454
|
-
// Calculate status for subtitle
|
|
1455
769
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
1456
|
-
const plugins =
|
|
770
|
+
const plugins: PluginInfo[] =
|
|
1457
771
|
pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
1458
772
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
1459
773
|
const updateCount = plugins.filter((p) => p.hasUpdate).length;
|
|
1460
774
|
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
|
|
1461
|
-
|
|
1462
|
-
// Search placeholder shows status when not searching
|
|
1463
775
|
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
|
|
1464
776
|
|
|
1465
777
|
return (
|
|
@@ -1478,7 +790,7 @@ export function PluginsScreen() {
|
|
|
1478
790
|
<ScrollableList
|
|
1479
791
|
items={selectableItems}
|
|
1480
792
|
selectedIndex={pluginsState.selectedIndex}
|
|
1481
|
-
renderItem={
|
|
793
|
+
renderItem={renderPluginRow}
|
|
1482
794
|
maxHeight={dimensions.listPanelHeight}
|
|
1483
795
|
/>
|
|
1484
796
|
{pluginsState.searchQuery && selectableItems.length === 0 && (
|
|
@@ -1486,7 +798,7 @@ export function PluginsScreen() {
|
|
|
1486
798
|
)}
|
|
1487
799
|
</box>
|
|
1488
800
|
}
|
|
1489
|
-
detailPanel={
|
|
801
|
+
detailPanel={renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces)}
|
|
1490
802
|
/>
|
|
1491
803
|
);
|
|
1492
804
|
}
|