claudeup 1.8.0 → 3.3.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/bin/claudeup.js +20 -2
- package/package.json +10 -19
- package/src/data/cli-tools.js +123 -0
- package/src/data/cli-tools.ts +140 -0
- package/{dist → src}/data/marketplaces.js +23 -24
- package/src/data/marketplaces.ts +95 -0
- package/src/data/mcp-servers.js +509 -0
- package/src/data/mcp-servers.ts +526 -0
- package/src/data/statuslines.js +159 -0
- package/src/data/statuslines.ts +188 -0
- package/src/index.js +4 -0
- package/src/index.ts +5 -0
- package/{dist → src}/main.js +46 -47
- package/src/main.tsx +145 -0
- package/src/opentui.d.ts +191 -0
- package/{dist → src}/prerunner/index.js +31 -41
- package/src/prerunner/index.ts +124 -0
- package/{dist → src}/services/claude-runner.js +9 -10
- package/src/services/claude-runner.ts +31 -0
- package/{dist → src}/services/claude-settings.js +72 -33
- package/src/services/claude-settings.ts +934 -0
- package/src/services/local-marketplace.js +339 -0
- package/src/services/local-marketplace.ts +489 -0
- package/{dist → src}/services/mcp-registry.js +13 -14
- package/src/services/mcp-registry.ts +105 -0
- package/{dist → src}/services/plugin-manager.js +61 -13
- package/src/services/plugin-manager.ts +693 -0
- package/{dist → src}/services/plugin-mcp-config.js +19 -18
- package/src/services/plugin-mcp-config.ts +242 -0
- package/{dist → src}/services/update-cache.js +0 -1
- package/src/services/update-cache.ts +78 -0
- package/{dist → src}/services/version-check.js +15 -14
- package/src/services/version-check.ts +122 -0
- package/src/types/index.js +1 -0
- package/src/types/index.ts +141 -0
- package/src/ui/App.js +213 -0
- package/src/ui/App.tsx +359 -0
- package/src/ui/components/CategoryHeader.js +9 -0
- package/src/ui/components/CategoryHeader.tsx +41 -0
- package/{dist → src}/ui/components/ScrollableList.js +19 -6
- package/src/ui/components/ScrollableList.tsx +98 -0
- package/src/ui/components/SearchInput.js +19 -0
- package/src/ui/components/SearchInput.tsx +56 -0
- package/src/ui/components/StyledText.js +39 -0
- package/src/ui/components/StyledText.tsx +70 -0
- package/src/ui/components/TabBar.js +38 -0
- package/src/ui/components/TabBar.tsx +88 -0
- package/src/ui/components/layout/Panel.js +6 -0
- package/src/ui/components/layout/Panel.tsx +62 -0
- package/src/ui/components/layout/ProgressBar.js +14 -0
- package/src/ui/components/layout/ProgressBar.tsx +47 -0
- package/src/ui/components/layout/ScopeTabs.js +6 -0
- package/src/ui/components/layout/ScopeTabs.tsx +53 -0
- package/src/ui/components/layout/ScreenLayout.js +21 -0
- package/src/ui/components/layout/ScreenLayout.tsx +147 -0
- package/src/ui/components/layout/index.js +4 -0
- package/src/ui/components/layout/index.ts +4 -0
- package/src/ui/components/modals/ConfirmModal.js +14 -0
- package/src/ui/components/modals/ConfirmModal.tsx +59 -0
- package/src/ui/components/modals/InputModal.js +16 -0
- package/src/ui/components/modals/InputModal.tsx +68 -0
- package/src/ui/components/modals/LoadingModal.js +14 -0
- package/src/ui/components/modals/LoadingModal.tsx +40 -0
- package/src/ui/components/modals/MessageModal.js +16 -0
- package/src/ui/components/modals/MessageModal.tsx +64 -0
- package/src/ui/components/modals/ModalContainer.js +56 -0
- package/src/ui/components/modals/ModalContainer.tsx +104 -0
- package/src/ui/components/modals/SelectModal.js +26 -0
- package/src/ui/components/modals/SelectModal.tsx +82 -0
- package/src/ui/components/modals/index.js +6 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/hooks/index.js +3 -0
- package/src/ui/hooks/index.ts +3 -0
- package/{dist → src}/ui/hooks/useAsyncData.js +21 -22
- package/src/ui/hooks/useAsyncData.ts +127 -0
- package/src/ui/hooks/useKeyboard.js +13 -0
- package/src/ui/hooks/useKeyboard.ts +26 -0
- package/src/ui/hooks/useKeyboardHandler.js +39 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/{dist → src}/ui/screens/CliToolsScreen.js +60 -54
- package/src/ui/screens/CliToolsScreen.tsx +468 -0
- package/src/ui/screens/EnvVarsScreen.js +154 -0
- package/src/ui/screens/EnvVarsScreen.tsx +269 -0
- package/{dist → src}/ui/screens/McpRegistryScreen.js +56 -55
- package/src/ui/screens/McpRegistryScreen.tsx +331 -0
- package/{dist → src}/ui/screens/McpScreen.js +46 -47
- package/src/ui/screens/McpScreen.tsx +392 -0
- package/src/ui/screens/ModelSelectorScreen.js +292 -0
- package/src/ui/screens/ModelSelectorScreen.tsx +441 -0
- package/{dist → src}/ui/screens/PluginsScreen.js +305 -293
- package/src/ui/screens/PluginsScreen.tsx +1231 -0
- package/src/ui/screens/StatusLineScreen.js +200 -0
- package/src/ui/screens/StatusLineScreen.tsx +411 -0
- package/src/ui/screens/index.js +7 -0
- package/src/ui/screens/index.ts +7 -0
- package/src/ui/state/AnimationContext.js +34 -0
- package/src/ui/state/AnimationContext.tsx +76 -0
- package/{dist → src}/ui/state/AppContext.js +31 -32
- package/src/ui/state/AppContext.tsx +235 -0
- package/{dist → src}/ui/state/DimensionsContext.js +16 -17
- package/src/ui/state/DimensionsContext.tsx +144 -0
- package/{dist → src}/ui/state/reducer.js +89 -90
- package/src/ui/state/reducer.ts +467 -0
- package/src/ui/state/types.js +1 -0
- package/src/ui/state/types.ts +273 -0
- package/{dist → src}/utils/command-utils.js +3 -4
- package/src/utils/command-utils.ts +20 -0
- package/{dist → src}/utils/fuzzy-search.js +2 -3
- package/src/utils/fuzzy-search.ts +138 -0
- package/{dist → src}/utils/string-utils.js +6 -6
- package/src/utils/string-utils.ts +88 -0
- package/dist/data/cli-tools.d.ts +0 -13
- package/dist/data/cli-tools.d.ts.map +0 -1
- package/dist/data/cli-tools.js +0 -124
- package/dist/data/cli-tools.js.map +0 -1
- package/dist/data/marketplaces.d.ts +0 -6
- package/dist/data/marketplaces.d.ts.map +0 -1
- package/dist/data/marketplaces.js.map +0 -1
- package/dist/data/mcp-servers.d.ts +0 -8
- package/dist/data/mcp-servers.d.ts.map +0 -1
- package/dist/data/mcp-servers.js +0 -503
- package/dist/data/mcp-servers.js.map +0 -1
- package/dist/data/statuslines.d.ts +0 -10
- package/dist/data/statuslines.d.ts.map +0 -1
- package/dist/data/statuslines.js +0 -160
- package/dist/data/statuslines.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -90
- package/dist/index.js.map +0 -1
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/prerunner/index.d.ts +0 -7
- package/dist/prerunner/index.d.ts.map +0 -1
- package/dist/prerunner/index.js.map +0 -1
- package/dist/services/claude-runner.d.ts +0 -7
- package/dist/services/claude-runner.d.ts.map +0 -1
- package/dist/services/claude-runner.js.map +0 -1
- package/dist/services/claude-settings.d.ts +0 -73
- package/dist/services/claude-settings.d.ts.map +0 -1
- package/dist/services/claude-settings.js.map +0 -1
- package/dist/services/local-marketplace.d.ts +0 -111
- package/dist/services/local-marketplace.d.ts.map +0 -1
- package/dist/services/local-marketplace.js +0 -599
- package/dist/services/local-marketplace.js.map +0 -1
- package/dist/services/mcp-registry.d.ts +0 -10
- package/dist/services/mcp-registry.d.ts.map +0 -1
- package/dist/services/mcp-registry.js.map +0 -1
- package/dist/services/plugin-manager.d.ts +0 -65
- package/dist/services/plugin-manager.d.ts.map +0 -1
- package/dist/services/plugin-manager.js.map +0 -1
- package/dist/services/plugin-mcp-config.d.ts +0 -52
- package/dist/services/plugin-mcp-config.d.ts.map +0 -1
- package/dist/services/plugin-mcp-config.js.map +0 -1
- package/dist/services/update-cache.d.ts +0 -21
- package/dist/services/update-cache.d.ts.map +0 -1
- package/dist/services/update-cache.js.map +0 -1
- package/dist/services/version-check.d.ts +0 -20
- package/dist/services/version-check.d.ts.map +0 -1
- package/dist/services/version-check.js.map +0 -1
- package/dist/types/index.d.ts +0 -105
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -2
- package/dist/types/index.js.map +0 -1
- package/dist/ui/InkApp.d.ts +0 -5
- package/dist/ui/InkApp.d.ts.map +0 -1
- package/dist/ui/InkApp.js +0 -188
- package/dist/ui/InkApp.js.map +0 -1
- package/dist/ui/components/CategoryHeader.d.ts +0 -16
- package/dist/ui/components/CategoryHeader.d.ts.map +0 -1
- package/dist/ui/components/CategoryHeader.js +0 -11
- package/dist/ui/components/CategoryHeader.js.map +0 -1
- package/dist/ui/components/ScrollableList.d.ts +0 -16
- package/dist/ui/components/ScrollableList.d.ts.map +0 -1
- package/dist/ui/components/ScrollableList.js.map +0 -1
- package/dist/ui/components/SearchInput.d.ts +0 -18
- package/dist/ui/components/SearchInput.d.ts.map +0 -1
- package/dist/ui/components/SearchInput.js +0 -30
- package/dist/ui/components/SearchInput.js.map +0 -1
- package/dist/ui/components/TabBar.d.ts +0 -8
- package/dist/ui/components/TabBar.d.ts.map +0 -1
- package/dist/ui/components/TabBar.js +0 -18
- package/dist/ui/components/TabBar.js.map +0 -1
- package/dist/ui/components/layout/Footer.d.ts +0 -14
- package/dist/ui/components/layout/Footer.d.ts.map +0 -1
- package/dist/ui/components/layout/Footer.js +0 -23
- package/dist/ui/components/layout/Footer.js.map +0 -1
- package/dist/ui/components/layout/Header.d.ts +0 -4
- package/dist/ui/components/layout/Header.d.ts.map +0 -1
- package/dist/ui/components/layout/Header.js +0 -25
- package/dist/ui/components/layout/Header.js.map +0 -1
- package/dist/ui/components/layout/Panel.d.ts +0 -22
- package/dist/ui/components/layout/Panel.d.ts.map +0 -1
- package/dist/ui/components/layout/Panel.js +0 -8
- package/dist/ui/components/layout/Panel.js.map +0 -1
- package/dist/ui/components/layout/ProgressBar.d.ts +0 -12
- package/dist/ui/components/layout/ProgressBar.d.ts.map +0 -1
- package/dist/ui/components/layout/ProgressBar.js +0 -16
- package/dist/ui/components/layout/ProgressBar.js.map +0 -1
- package/dist/ui/components/layout/ScopeTabs.d.ts +0 -12
- package/dist/ui/components/layout/ScopeTabs.d.ts.map +0 -1
- package/dist/ui/components/layout/ScopeTabs.js +0 -8
- package/dist/ui/components/layout/ScopeTabs.js.map +0 -1
- package/dist/ui/components/layout/ScreenLayout.d.ts +0 -30
- package/dist/ui/components/layout/ScreenLayout.d.ts.map +0 -1
- package/dist/ui/components/layout/ScreenLayout.js +0 -23
- package/dist/ui/components/layout/ScreenLayout.js.map +0 -1
- package/dist/ui/components/layout/index.d.ts +0 -7
- package/dist/ui/components/layout/index.d.ts.map +0 -1
- package/dist/ui/components/layout/index.js +0 -7
- package/dist/ui/components/layout/index.js.map +0 -1
- package/dist/ui/components/modals/ConfirmModal.d.ts +0 -14
- package/dist/ui/components/modals/ConfirmModal.d.ts.map +0 -1
- package/dist/ui/components/modals/ConfirmModal.js +0 -15
- package/dist/ui/components/modals/ConfirmModal.js.map +0 -1
- package/dist/ui/components/modals/InputModal.d.ts +0 -16
- package/dist/ui/components/modals/InputModal.d.ts.map +0 -1
- package/dist/ui/components/modals/InputModal.js +0 -23
- package/dist/ui/components/modals/InputModal.js.map +0 -1
- package/dist/ui/components/modals/LoadingModal.d.ts +0 -8
- package/dist/ui/components/modals/LoadingModal.d.ts.map +0 -1
- package/dist/ui/components/modals/LoadingModal.js +0 -8
- package/dist/ui/components/modals/LoadingModal.js.map +0 -1
- package/dist/ui/components/modals/MessageModal.d.ts +0 -14
- package/dist/ui/components/modals/MessageModal.d.ts.map +0 -1
- package/dist/ui/components/modals/MessageModal.js +0 -17
- package/dist/ui/components/modals/MessageModal.js.map +0 -1
- package/dist/ui/components/modals/ModalContainer.d.ts +0 -7
- package/dist/ui/components/modals/ModalContainer.d.ts.map +0 -1
- package/dist/ui/components/modals/ModalContainer.js +0 -38
- package/dist/ui/components/modals/ModalContainer.js.map +0 -1
- package/dist/ui/components/modals/SelectModal.d.ts +0 -17
- package/dist/ui/components/modals/SelectModal.d.ts.map +0 -1
- package/dist/ui/components/modals/SelectModal.js +0 -33
- package/dist/ui/components/modals/SelectModal.js.map +0 -1
- package/dist/ui/components/modals/index.d.ts +0 -7
- package/dist/ui/components/modals/index.d.ts.map +0 -1
- package/dist/ui/components/modals/index.js +0 -7
- package/dist/ui/components/modals/index.js.map +0 -1
- package/dist/ui/hooks/index.d.ts +0 -3
- package/dist/ui/hooks/index.d.ts.map +0 -1
- package/dist/ui/hooks/index.js +0 -3
- package/dist/ui/hooks/index.js.map +0 -1
- package/dist/ui/hooks/useAsyncData.d.ts +0 -40
- package/dist/ui/hooks/useAsyncData.d.ts.map +0 -1
- package/dist/ui/hooks/useAsyncData.js.map +0 -1
- package/dist/ui/hooks/useKeyboardNavigation.d.ts +0 -27
- package/dist/ui/hooks/useKeyboardNavigation.d.ts.map +0 -1
- package/dist/ui/hooks/useKeyboardNavigation.js +0 -82
- package/dist/ui/hooks/useKeyboardNavigation.js.map +0 -1
- package/dist/ui/screens/CliToolsScreen.d.ts +0 -4
- package/dist/ui/screens/CliToolsScreen.d.ts.map +0 -1
- package/dist/ui/screens/CliToolsScreen.js.map +0 -1
- package/dist/ui/screens/EnvVarsScreen.d.ts +0 -4
- package/dist/ui/screens/EnvVarsScreen.d.ts.map +0 -1
- package/dist/ui/screens/EnvVarsScreen.js +0 -145
- package/dist/ui/screens/EnvVarsScreen.js.map +0 -1
- package/dist/ui/screens/McpRegistryScreen.d.ts +0 -4
- package/dist/ui/screens/McpRegistryScreen.d.ts.map +0 -1
- package/dist/ui/screens/McpRegistryScreen.js.map +0 -1
- package/dist/ui/screens/McpScreen.d.ts +0 -4
- package/dist/ui/screens/McpScreen.d.ts.map +0 -1
- package/dist/ui/screens/McpScreen.js.map +0 -1
- package/dist/ui/screens/ModelSelectorScreen.d.ts +0 -4
- package/dist/ui/screens/ModelSelectorScreen.d.ts.map +0 -1
- package/dist/ui/screens/ModelSelectorScreen.js +0 -143
- package/dist/ui/screens/ModelSelectorScreen.js.map +0 -1
- package/dist/ui/screens/PluginsScreen.d.ts +0 -4
- package/dist/ui/screens/PluginsScreen.d.ts.map +0 -1
- package/dist/ui/screens/PluginsScreen.js.map +0 -1
- package/dist/ui/screens/StatusLineScreen.d.ts +0 -4
- package/dist/ui/screens/StatusLineScreen.d.ts.map +0 -1
- package/dist/ui/screens/StatusLineScreen.js +0 -197
- package/dist/ui/screens/StatusLineScreen.js.map +0 -1
- package/dist/ui/screens/index.d.ts +0 -8
- package/dist/ui/screens/index.d.ts.map +0 -1
- package/dist/ui/screens/index.js +0 -8
- package/dist/ui/screens/index.js.map +0 -1
- package/dist/ui/state/AppContext.d.ts +0 -40
- package/dist/ui/state/AppContext.d.ts.map +0 -1
- package/dist/ui/state/AppContext.js.map +0 -1
- package/dist/ui/state/DimensionsContext.d.ts +0 -27
- package/dist/ui/state/DimensionsContext.d.ts.map +0 -1
- package/dist/ui/state/DimensionsContext.js.map +0 -1
- package/dist/ui/state/reducer.d.ts +0 -4
- package/dist/ui/state/reducer.d.ts.map +0 -1
- package/dist/ui/state/reducer.js.map +0 -1
- package/dist/ui/state/types.d.ts +0 -266
- package/dist/ui/state/types.d.ts.map +0 -1
- package/dist/ui/state/types.js +0 -2
- package/dist/ui/state/types.js.map +0 -1
- package/dist/utils/command-utils.d.ts +0 -8
- package/dist/utils/command-utils.d.ts.map +0 -1
- package/dist/utils/command-utils.js.map +0 -1
- package/dist/utils/fuzzy-search.d.ts +0 -33
- package/dist/utils/fuzzy-search.d.ts.map +0 -1
- package/dist/utils/fuzzy-search.js.map +0 -1
- package/dist/utils/string-utils.d.ts +0 -24
- package/dist/utils/string-utils.d.ts.map +0 -1
- package/dist/utils/string-utils.js.map +0 -1
|
@@ -0,0 +1,1231 @@
|
|
|
1
|
+
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
3
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
+
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
7
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
|
+
import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
|
|
9
|
+
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
10
|
+
import {
|
|
11
|
+
getAvailablePlugins,
|
|
12
|
+
refreshAllMarketplaces,
|
|
13
|
+
clearMarketplaceCache,
|
|
14
|
+
saveInstalledPluginVersion,
|
|
15
|
+
removeInstalledPluginVersion,
|
|
16
|
+
getLocalMarketplacesInfo,
|
|
17
|
+
type PluginInfo,
|
|
18
|
+
} from "../../services/plugin-manager.js";
|
|
19
|
+
import {
|
|
20
|
+
enablePlugin,
|
|
21
|
+
enableGlobalPlugin,
|
|
22
|
+
enableLocalPlugin,
|
|
23
|
+
saveGlobalInstalledPluginVersion,
|
|
24
|
+
removeGlobalInstalledPluginVersion,
|
|
25
|
+
saveLocalInstalledPluginVersion,
|
|
26
|
+
removeLocalInstalledPluginVersion,
|
|
27
|
+
setMcpEnvVar,
|
|
28
|
+
getMcpEnvVars,
|
|
29
|
+
} from "../../services/claude-settings.js";
|
|
30
|
+
import {
|
|
31
|
+
getPluginEnvRequirements,
|
|
32
|
+
getPluginSourcePath,
|
|
33
|
+
} from "../../services/plugin-mcp-config.js";
|
|
34
|
+
import type { Marketplace } from "../../types/index.js";
|
|
35
|
+
|
|
36
|
+
interface ListItem {
|
|
37
|
+
id: string;
|
|
38
|
+
type: "category" | "plugin";
|
|
39
|
+
label: string;
|
|
40
|
+
marketplace?: Marketplace;
|
|
41
|
+
marketplaceEnabled?: boolean;
|
|
42
|
+
plugin?: PluginInfo;
|
|
43
|
+
pluginCount?: number;
|
|
44
|
+
isExpanded?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function PluginsScreen() {
|
|
48
|
+
const { state, dispatch } = useApp();
|
|
49
|
+
const { plugins: pluginsState } = state;
|
|
50
|
+
const modal = useModal();
|
|
51
|
+
const progress = useProgress();
|
|
52
|
+
const dimensions = useDimensions();
|
|
53
|
+
|
|
54
|
+
const isSearchActive =
|
|
55
|
+
state.isSearching &&
|
|
56
|
+
state.currentRoute.screen === "plugins" &&
|
|
57
|
+
!state.modal;
|
|
58
|
+
|
|
59
|
+
// Fetch data (always fetches all scopes)
|
|
60
|
+
const fetchData = useCallback(async () => {
|
|
61
|
+
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
62
|
+
try {
|
|
63
|
+
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
64
|
+
const allMarketplaces = getAllMarketplaces(localMarketplaces);
|
|
65
|
+
|
|
66
|
+
// Always use getAvailablePlugins which fetches all scope data
|
|
67
|
+
const pluginData = await getAvailablePlugins(state.projectPath);
|
|
68
|
+
|
|
69
|
+
dispatch({
|
|
70
|
+
type: "PLUGINS_DATA_SUCCESS",
|
|
71
|
+
marketplaces: allMarketplaces,
|
|
72
|
+
plugins: pluginData,
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
dispatch({
|
|
76
|
+
type: "PLUGINS_DATA_ERROR",
|
|
77
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}, [dispatch, state.projectPath]);
|
|
81
|
+
|
|
82
|
+
// Load data on mount or when dataRefreshVersion changes
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
fetchData();
|
|
85
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
86
|
+
|
|
87
|
+
// Build list items (categories + plugins)
|
|
88
|
+
const allItems = useMemo((): ListItem[] => {
|
|
89
|
+
if (
|
|
90
|
+
pluginsState.marketplaces.status !== "success" ||
|
|
91
|
+
pluginsState.plugins.status !== "success"
|
|
92
|
+
) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const marketplaces = pluginsState.marketplaces.data;
|
|
97
|
+
const plugins = pluginsState.plugins.data;
|
|
98
|
+
const collapsed = pluginsState.collapsedMarketplaces;
|
|
99
|
+
|
|
100
|
+
const pluginsByMarketplace = new Map<string, PluginInfo[]>();
|
|
101
|
+
for (const plugin of plugins) {
|
|
102
|
+
const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
|
|
103
|
+
existing.push(plugin);
|
|
104
|
+
pluginsByMarketplace.set(plugin.marketplace, existing);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort marketplaces: deprecated ones go to the bottom
|
|
108
|
+
const sortedMarketplaces = [...marketplaces].sort((a, b) => {
|
|
109
|
+
const aDeprecated = a.name === "claude-code-plugins" ? 1 : 0;
|
|
110
|
+
const bDeprecated = b.name === "claude-code-plugins" ? 1 : 0;
|
|
111
|
+
return aDeprecated - bDeprecated;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const items: ListItem[] = [];
|
|
115
|
+
|
|
116
|
+
for (const marketplace of sortedMarketplaces) {
|
|
117
|
+
const marketplacePlugins =
|
|
118
|
+
pluginsByMarketplace.get(marketplace.name) || [];
|
|
119
|
+
const isCollapsed = collapsed.has(marketplace.name);
|
|
120
|
+
const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
|
|
121
|
+
const hasPlugins = marketplacePlugins.length > 0;
|
|
122
|
+
|
|
123
|
+
// Category header (marketplace)
|
|
124
|
+
items.push({
|
|
125
|
+
id: `mp:${marketplace.name}`,
|
|
126
|
+
type: "category",
|
|
127
|
+
label: marketplace.displayName,
|
|
128
|
+
marketplace,
|
|
129
|
+
marketplaceEnabled: isEnabled,
|
|
130
|
+
pluginCount: marketplacePlugins.length,
|
|
131
|
+
isExpanded: !isCollapsed && hasPlugins,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Plugins under this marketplace (if expanded)
|
|
135
|
+
if (isEnabled && hasPlugins && !isCollapsed) {
|
|
136
|
+
for (const plugin of marketplacePlugins) {
|
|
137
|
+
items.push({
|
|
138
|
+
id: `pl:${plugin.id}`,
|
|
139
|
+
type: "plugin",
|
|
140
|
+
label: plugin.name,
|
|
141
|
+
plugin,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return items;
|
|
148
|
+
}, [
|
|
149
|
+
pluginsState.marketplaces,
|
|
150
|
+
pluginsState.plugins,
|
|
151
|
+
pluginsState.collapsedMarketplaces,
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// Filter items by search query
|
|
155
|
+
const filteredItems = useMemo(() => {
|
|
156
|
+
const query = pluginsState.searchQuery.trim();
|
|
157
|
+
if (!query) return allItems;
|
|
158
|
+
|
|
159
|
+
// Only search plugins, not categories
|
|
160
|
+
const pluginItems = allItems.filter((item) => item.type === "plugin");
|
|
161
|
+
const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
|
|
162
|
+
|
|
163
|
+
// Include parent categories for matched plugins
|
|
164
|
+
const matchedMarketplaces = new Set<string>();
|
|
165
|
+
for (const result of fuzzyResults) {
|
|
166
|
+
if (result.item.plugin) {
|
|
167
|
+
matchedMarketplaces.add(result.item.plugin.marketplace);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result: ListItem[] = [];
|
|
172
|
+
let currentMarketplace: string | null = null;
|
|
173
|
+
|
|
174
|
+
for (const item of allItems) {
|
|
175
|
+
if (item.type === "category" && item.marketplace) {
|
|
176
|
+
if (matchedMarketplaces.has(item.marketplace.name)) {
|
|
177
|
+
result.push(item);
|
|
178
|
+
currentMarketplace = item.marketplace.name;
|
|
179
|
+
} else {
|
|
180
|
+
currentMarketplace = null;
|
|
181
|
+
}
|
|
182
|
+
} else if (item.type === "plugin" && item.plugin) {
|
|
183
|
+
if (currentMarketplace === item.plugin.marketplace) {
|
|
184
|
+
// Check if this plugin matched
|
|
185
|
+
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
186
|
+
if (matched) {
|
|
187
|
+
result.push({ ...item, _matches: matched.matches } as ListItem & {
|
|
188
|
+
_matches?: number[];
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}, [allItems, pluginsState.searchQuery]);
|
|
197
|
+
|
|
198
|
+
// Only selectable items (plugins, not categories)
|
|
199
|
+
const selectableItems = useMemo(() => {
|
|
200
|
+
return filteredItems.filter(
|
|
201
|
+
(item) => item.type === "plugin" || item.type === "category",
|
|
202
|
+
);
|
|
203
|
+
}, [filteredItems]);
|
|
204
|
+
|
|
205
|
+
// Keyboard handling
|
|
206
|
+
useKeyboard((event) => {
|
|
207
|
+
// Handle search mode
|
|
208
|
+
if (isSearchActive) {
|
|
209
|
+
if (event.name === "escape") {
|
|
210
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
211
|
+
dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
|
|
212
|
+
} else if (event.name === "enter") {
|
|
213
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
214
|
+
// Keep the search query, just exit search mode
|
|
215
|
+
} else if (event.name === "backspace" || event.name === "delete") {
|
|
216
|
+
dispatch({
|
|
217
|
+
type: "PLUGINS_SET_SEARCH",
|
|
218
|
+
query: pluginsState.searchQuery.slice(0, -1),
|
|
219
|
+
});
|
|
220
|
+
} else if (event.name.length === 1 && !event.ctrl && !event.meta) {
|
|
221
|
+
dispatch({
|
|
222
|
+
type: "PLUGINS_SET_SEARCH",
|
|
223
|
+
query: pluginsState.searchQuery + event.name,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (state.modal) return;
|
|
230
|
+
|
|
231
|
+
// Start search with /
|
|
232
|
+
if (event.name === "/") {
|
|
233
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Navigation
|
|
238
|
+
if (event.name === "up" || event.name === "k") {
|
|
239
|
+
const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
|
|
240
|
+
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
241
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
242
|
+
const newIndex = Math.min(
|
|
243
|
+
selectableItems.length - 1,
|
|
244
|
+
pluginsState.selectedIndex + 1,
|
|
245
|
+
);
|
|
246
|
+
dispatch({ type: "PLUGINS_SELECT", index: newIndex });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Collapse/expand marketplace
|
|
250
|
+
else if (
|
|
251
|
+
(event.name === "left" || event.name === "right" || event.name === "<" || event.name === ">") &&
|
|
252
|
+
selectableItems[pluginsState.selectedIndex]?.marketplace
|
|
253
|
+
) {
|
|
254
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
255
|
+
if (item?.marketplace) {
|
|
256
|
+
dispatch({
|
|
257
|
+
type: "PLUGINS_TOGGLE_MARKETPLACE",
|
|
258
|
+
name: item.marketplace.name,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Refresh
|
|
264
|
+
else if (event.name === "r") {
|
|
265
|
+
handleRefresh();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// New marketplace (show instructions)
|
|
269
|
+
else if (event.name === "n") {
|
|
270
|
+
handleShowAddMarketplaceInstructions();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Team config help
|
|
274
|
+
else if (event.name === "t") {
|
|
275
|
+
handleShowTeamConfigHelp();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Scope-specific toggle shortcuts (u/p/l)
|
|
279
|
+
else if (event.name === "u") {
|
|
280
|
+
handleScopeToggle("user");
|
|
281
|
+
} else if (event.name === "p") {
|
|
282
|
+
handleScopeToggle("project");
|
|
283
|
+
} else if (event.name === "l") {
|
|
284
|
+
handleScopeToggle("local");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Update plugin (Shift+U)
|
|
288
|
+
else if (event.name === "U") {
|
|
289
|
+
handleUpdate();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Update all
|
|
293
|
+
else if (event.name === "a") {
|
|
294
|
+
handleUpdateAll();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Delete/uninstall
|
|
298
|
+
else if (event.name === "d") {
|
|
299
|
+
handleUninstall();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Enter for selection
|
|
303
|
+
else if (event.name === "enter") {
|
|
304
|
+
handleSelect();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Handle actions
|
|
309
|
+
const handleRefresh = async () => {
|
|
310
|
+
progress.show("Refreshing cache...");
|
|
311
|
+
try {
|
|
312
|
+
const result = await refreshAllMarketplaces((p) => {
|
|
313
|
+
progress.show(`${p.name}`, p.current, p.total);
|
|
314
|
+
});
|
|
315
|
+
clearMarketplaceCache();
|
|
316
|
+
progress.hide();
|
|
317
|
+
|
|
318
|
+
// Build message
|
|
319
|
+
let message =
|
|
320
|
+
"Cache refreshed.\n\n" +
|
|
321
|
+
"To update marketplaces from GitHub, run in Claude Code:\n" +
|
|
322
|
+
" /plugin marketplace update\n\n" +
|
|
323
|
+
"Then refresh claudeup again with 'r'.";
|
|
324
|
+
|
|
325
|
+
if (result.repair.length > 0) {
|
|
326
|
+
const totalRepaired = result.repair.reduce(
|
|
327
|
+
(sum, r) => sum + r.repaired.length,
|
|
328
|
+
0,
|
|
329
|
+
);
|
|
330
|
+
message += `\n\nAuto-repaired ${totalRepaired} plugin(s) with missing agents/commands.`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await modal.message("Refreshed", message, "success");
|
|
334
|
+
fetchData();
|
|
335
|
+
} catch (error) {
|
|
336
|
+
progress.hide();
|
|
337
|
+
await modal.message("Error", `Refresh failed: ${error}`, "error");
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const handleShowAddMarketplaceInstructions = async () => {
|
|
342
|
+
await modal.message(
|
|
343
|
+
"Add Marketplace",
|
|
344
|
+
"To add a marketplace, run this command in your terminal:\n\n" +
|
|
345
|
+
" claude marketplace add owner/repo\n\n" +
|
|
346
|
+
"Examples:\n" +
|
|
347
|
+
" claude marketplace add MadAppGang/claude-code\n" +
|
|
348
|
+
" claude marketplace add anthropics/claude-plugins-official\n\n" +
|
|
349
|
+
"Auto-update is enabled by default for new marketplaces.\n\n" +
|
|
350
|
+
"After adding, refresh claudeup with 'r' to see the new marketplace.",
|
|
351
|
+
"info",
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const handleShowTeamConfigHelp = async () => {
|
|
356
|
+
const helpText =
|
|
357
|
+
"TEAM CONFIGURATION FOR MARKETPLACES\n\n" +
|
|
358
|
+
"To configure marketplaces for your entire team:\n\n" +
|
|
359
|
+
"1. Add to .claude/settings.json (committed to git):\n\n" +
|
|
360
|
+
" {\n" +
|
|
361
|
+
' "extraKnownMarketplaces": {\n' +
|
|
362
|
+
' "company-tools": {\n' +
|
|
363
|
+
' "source": {\n' +
|
|
364
|
+
' "source": "github",\n' +
|
|
365
|
+
' "repo": "your-org/claude-plugins"\n' +
|
|
366
|
+
" }\n" +
|
|
367
|
+
" }\n" +
|
|
368
|
+
" },\n" +
|
|
369
|
+
' "enabledPlugins": {\n' +
|
|
370
|
+
' "code-formatter@company-tools": true\n' +
|
|
371
|
+
" }\n" +
|
|
372
|
+
" }\n\n" +
|
|
373
|
+
"2. Team members get prompted to install when they trust the folder\n\n" +
|
|
374
|
+
"3. Marketplaces are GLOBAL ONLY (managed by Claude Code)\n" +
|
|
375
|
+
" - Not project-specific\n" +
|
|
376
|
+
" - All team members share the same marketplace cache\n\n" +
|
|
377
|
+
"NOTE: Individual plugin enablement is still project-specific.\n" +
|
|
378
|
+
'Use "Project" scope in claudeup to enable for the team.';
|
|
379
|
+
|
|
380
|
+
await modal.message("Team Configuration", helpText, "info");
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Collect environment variables required by a plugin's MCP servers
|
|
385
|
+
* Prompts user for missing values and saves to local settings
|
|
386
|
+
*/
|
|
387
|
+
const collectPluginEnvVars = async (
|
|
388
|
+
pluginName: string,
|
|
389
|
+
marketplace: string,
|
|
390
|
+
): Promise<boolean> => {
|
|
391
|
+
try {
|
|
392
|
+
// Get plugin source path from marketplace manifest
|
|
393
|
+
const pluginSource = await getPluginSourcePath(marketplace, pluginName);
|
|
394
|
+
if (!pluginSource) return true; // No source path, nothing to configure
|
|
395
|
+
|
|
396
|
+
// Get env var requirements from plugin's MCP config
|
|
397
|
+
const requirements = await getPluginEnvRequirements(
|
|
398
|
+
marketplace,
|
|
399
|
+
pluginSource,
|
|
400
|
+
);
|
|
401
|
+
if (requirements.length === 0) return true; // No env vars needed
|
|
402
|
+
|
|
403
|
+
// Get existing env vars
|
|
404
|
+
const existingEnvVars = await getMcpEnvVars(state.projectPath);
|
|
405
|
+
const missingVars = requirements.filter(
|
|
406
|
+
(req) => !existingEnvVars[req.name] && !process.env[req.name],
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (missingVars.length === 0) return true; // All vars already configured
|
|
410
|
+
|
|
411
|
+
// Ask user if they want to configure MCP server env vars now
|
|
412
|
+
const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
|
|
413
|
+
const wantToConfigure = await modal.confirm(
|
|
414
|
+
"Configure MCP Servers?",
|
|
415
|
+
`This plugin includes MCP servers (${serverNames.join(", ")}) that need ${missingVars.length} environment variable(s).\n\nConfigure now?`,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (!wantToConfigure) {
|
|
419
|
+
await modal.message(
|
|
420
|
+
"Skipped Configuration",
|
|
421
|
+
"You can configure these variables later in the Environment Variables screen (press 4).",
|
|
422
|
+
"info",
|
|
423
|
+
);
|
|
424
|
+
return true; // Still installed, just not configured
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Collect each missing env var
|
|
428
|
+
for (const req of missingVars) {
|
|
429
|
+
// Check if value exists in process.env
|
|
430
|
+
const existingProcessEnv = process.env[req.name];
|
|
431
|
+
if (existingProcessEnv) {
|
|
432
|
+
const useExisting = await modal.confirm(
|
|
433
|
+
`Use ${req.name}?`,
|
|
434
|
+
`${req.name} is already set in your environment.\n\nUse the existing value?`,
|
|
435
|
+
);
|
|
436
|
+
if (useExisting) {
|
|
437
|
+
// Store reference to env var instead of literal value
|
|
438
|
+
await setMcpEnvVar(req.name, `\${${req.name}}`, state.projectPath);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Prompt for value
|
|
444
|
+
const value = await modal.input(
|
|
445
|
+
`Configure ${req.serverName}`,
|
|
446
|
+
`${req.label} (required):`,
|
|
447
|
+
"",
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (value === null) {
|
|
451
|
+
// User cancelled
|
|
452
|
+
await modal.message(
|
|
453
|
+
"Configuration Incomplete",
|
|
454
|
+
`Skipped remaining configuration.\nYou can configure these later in Environment Variables (press 4).`,
|
|
455
|
+
"info",
|
|
456
|
+
);
|
|
457
|
+
return true; // Still installed
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (value) {
|
|
461
|
+
await setMcpEnvVar(req.name, value, state.projectPath);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return true;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error("Error collecting plugin env vars:", error);
|
|
468
|
+
return true; // Don't block installation on config errors
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const handleSelect = async () => {
|
|
473
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
474
|
+
if (!item) return;
|
|
475
|
+
|
|
476
|
+
if (item.type === "category" && item.marketplace) {
|
|
477
|
+
const mp = item.marketplace;
|
|
478
|
+
|
|
479
|
+
if (item.marketplaceEnabled) {
|
|
480
|
+
const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
|
|
481
|
+
|
|
482
|
+
// If collapsed, expand first (even if no plugins - they might load)
|
|
483
|
+
if (isCollapsed) {
|
|
484
|
+
dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
|
|
485
|
+
} else if (item.pluginCount && item.pluginCount > 0) {
|
|
486
|
+
// If expanded with plugins, collapse
|
|
487
|
+
dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
|
|
488
|
+
} else {
|
|
489
|
+
// If expanded with no plugins, show removal instructions
|
|
490
|
+
await modal.message(
|
|
491
|
+
`Remove ${mp.displayName}?`,
|
|
492
|
+
`To remove this marketplace, run in Claude Code:\n\n` +
|
|
493
|
+
` /plugin marketplace remove ${mp.name}\n\n` +
|
|
494
|
+
`After removing, refresh claudeup with 'r' to update the display.`,
|
|
495
|
+
"info",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
// Show add marketplace instructions
|
|
500
|
+
await modal.message(
|
|
501
|
+
`Add ${mp.displayName}?`,
|
|
502
|
+
`To add this marketplace, run in your terminal:\n\n` +
|
|
503
|
+
` claude marketplace add ${mp.source.repo || mp.name}\n\n` +
|
|
504
|
+
`Auto-update is enabled by default.\n\n` +
|
|
505
|
+
`After adding, refresh claudeup with 'r' to see it.`,
|
|
506
|
+
"info",
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
} else if (item.type === "plugin" && item.plugin) {
|
|
510
|
+
const plugin = item.plugin;
|
|
511
|
+
const latestVersion = plugin.version || "0.0.0";
|
|
512
|
+
|
|
513
|
+
// Build scope options with status info
|
|
514
|
+
const buildScopeLabel = (
|
|
515
|
+
name: string,
|
|
516
|
+
scope: { enabled?: boolean; version?: string } | undefined,
|
|
517
|
+
desc: string,
|
|
518
|
+
) => {
|
|
519
|
+
const installed = scope?.enabled;
|
|
520
|
+
const ver = scope?.version;
|
|
521
|
+
const hasUpdate =
|
|
522
|
+
ver &&
|
|
523
|
+
latestVersion &&
|
|
524
|
+
ver !== latestVersion &&
|
|
525
|
+
latestVersion !== "0.0.0";
|
|
526
|
+
|
|
527
|
+
let label = installed ? `● ${name}` : `○ ${name}`;
|
|
528
|
+
label += ` (${desc})`;
|
|
529
|
+
if (ver) label += ` v${ver}`;
|
|
530
|
+
if (hasUpdate) label += ` → v${latestVersion}`;
|
|
531
|
+
return label;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const scopeOptions = [
|
|
535
|
+
{
|
|
536
|
+
label: buildScopeLabel("User", plugin.userScope, "global"),
|
|
537
|
+
value: "user",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
label: buildScopeLabel("Project", plugin.projectScope, "team"),
|
|
541
|
+
value: "project",
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
label: buildScopeLabel("Local", plugin.localScope, "private"),
|
|
545
|
+
value: "local",
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const scopeValue = await modal.select(
|
|
550
|
+
plugin.name,
|
|
551
|
+
`Select scope to toggle:`,
|
|
552
|
+
scopeOptions,
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
if (scopeValue === null) return; // Cancelled
|
|
556
|
+
|
|
557
|
+
// Determine action based on selected scope's current state
|
|
558
|
+
const selectedScope =
|
|
559
|
+
scopeValue === "user"
|
|
560
|
+
? plugin.userScope
|
|
561
|
+
: scopeValue === "project"
|
|
562
|
+
? plugin.projectScope
|
|
563
|
+
: plugin.localScope;
|
|
564
|
+
const isInstalledInScope = selectedScope?.enabled;
|
|
565
|
+
const installedVersion = selectedScope?.version;
|
|
566
|
+
const scopeLabel =
|
|
567
|
+
scopeValue === "user"
|
|
568
|
+
? "User"
|
|
569
|
+
: scopeValue === "project"
|
|
570
|
+
? "Project"
|
|
571
|
+
: "Local";
|
|
572
|
+
|
|
573
|
+
// Check if this scope has an update available
|
|
574
|
+
const hasUpdateInScope =
|
|
575
|
+
isInstalledInScope &&
|
|
576
|
+
installedVersion &&
|
|
577
|
+
latestVersion !== "0.0.0" &&
|
|
578
|
+
installedVersion !== latestVersion;
|
|
579
|
+
|
|
580
|
+
// Determine action: update if available, otherwise toggle
|
|
581
|
+
let action: "update" | "install" | "uninstall";
|
|
582
|
+
if (isInstalledInScope && hasUpdateInScope) {
|
|
583
|
+
action = "update";
|
|
584
|
+
} else if (isInstalledInScope) {
|
|
585
|
+
action = "uninstall";
|
|
586
|
+
} else {
|
|
587
|
+
action = "install";
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const actionLabel =
|
|
591
|
+
action === "update"
|
|
592
|
+
? `Updating ${scopeLabel}`
|
|
593
|
+
: action === "install"
|
|
594
|
+
? `Installing to ${scopeLabel}`
|
|
595
|
+
: `Uninstalling from ${scopeLabel}`;
|
|
596
|
+
modal.loading(`${actionLabel}...`);
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
if (action === "uninstall") {
|
|
600
|
+
// Uninstall from this scope
|
|
601
|
+
if (scopeValue === "user") {
|
|
602
|
+
await enableGlobalPlugin(plugin.id, false);
|
|
603
|
+
await removeGlobalInstalledPluginVersion(plugin.id);
|
|
604
|
+
} else if (scopeValue === "project") {
|
|
605
|
+
await enablePlugin(plugin.id, false, state.projectPath);
|
|
606
|
+
await removeInstalledPluginVersion(plugin.id, state.projectPath);
|
|
607
|
+
} else {
|
|
608
|
+
await enableLocalPlugin(plugin.id, false, state.projectPath);
|
|
609
|
+
await removeLocalInstalledPluginVersion(
|
|
610
|
+
plugin.id,
|
|
611
|
+
state.projectPath,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
// Install or update (both save the latest version)
|
|
616
|
+
if (scopeValue === "user") {
|
|
617
|
+
await enableGlobalPlugin(plugin.id, true);
|
|
618
|
+
await saveGlobalInstalledPluginVersion(plugin.id, latestVersion);
|
|
619
|
+
} else if (scopeValue === "project") {
|
|
620
|
+
await enablePlugin(plugin.id, true, state.projectPath);
|
|
621
|
+
await saveInstalledPluginVersion(
|
|
622
|
+
plugin.id,
|
|
623
|
+
latestVersion,
|
|
624
|
+
state.projectPath,
|
|
625
|
+
);
|
|
626
|
+
} else {
|
|
627
|
+
await enableLocalPlugin(plugin.id, true, state.projectPath);
|
|
628
|
+
await saveLocalInstalledPluginVersion(
|
|
629
|
+
plugin.id,
|
|
630
|
+
latestVersion,
|
|
631
|
+
state.projectPath,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// On fresh install, prompt for MCP server env vars if needed
|
|
636
|
+
if (action === "install") {
|
|
637
|
+
modal.hideModal();
|
|
638
|
+
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (action !== "install") {
|
|
642
|
+
modal.hideModal();
|
|
643
|
+
}
|
|
644
|
+
fetchData();
|
|
645
|
+
} catch (error) {
|
|
646
|
+
modal.hideModal();
|
|
647
|
+
await modal.message("Error", `Failed: ${error}`, "error");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const handleUpdate = async () => {
|
|
653
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
654
|
+
if (!item || item.type !== "plugin" || !item.plugin?.hasUpdate) return;
|
|
655
|
+
|
|
656
|
+
const plugin = item.plugin;
|
|
657
|
+
const isGlobal = pluginsState.scope === "global";
|
|
658
|
+
|
|
659
|
+
modal.loading(`Updating ${plugin.name}...`);
|
|
660
|
+
try {
|
|
661
|
+
const versionToSave = plugin.version || "0.0.0";
|
|
662
|
+
if (isGlobal) {
|
|
663
|
+
await saveGlobalInstalledPluginVersion(plugin.id, versionToSave);
|
|
664
|
+
} else {
|
|
665
|
+
await saveInstalledPluginVersion(
|
|
666
|
+
plugin.id,
|
|
667
|
+
versionToSave,
|
|
668
|
+
state.projectPath,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
modal.hideModal();
|
|
672
|
+
fetchData();
|
|
673
|
+
} catch (error) {
|
|
674
|
+
modal.hideModal();
|
|
675
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const handleUpdateAll = async () => {
|
|
680
|
+
if (pluginsState.plugins.status !== "success") return;
|
|
681
|
+
|
|
682
|
+
const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
|
|
683
|
+
if (updatable.length === 0) return;
|
|
684
|
+
|
|
685
|
+
const isGlobal = pluginsState.scope === "global";
|
|
686
|
+
modal.loading(`Updating ${updatable.length} plugin(s)...`);
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
for (const plugin of updatable) {
|
|
690
|
+
const versionToSave = plugin.version || "0.0.0";
|
|
691
|
+
if (isGlobal) {
|
|
692
|
+
await saveGlobalInstalledPluginVersion(plugin.id, versionToSave);
|
|
693
|
+
} else {
|
|
694
|
+
await saveInstalledPluginVersion(
|
|
695
|
+
plugin.id,
|
|
696
|
+
versionToSave,
|
|
697
|
+
state.projectPath,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
modal.hideModal();
|
|
702
|
+
fetchData();
|
|
703
|
+
} catch (error) {
|
|
704
|
+
modal.hideModal();
|
|
705
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Scope-specific toggle (install if not installed, uninstall if installed)
|
|
710
|
+
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
711
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
712
|
+
if (!item || item.type !== "plugin" || !item.plugin) return;
|
|
713
|
+
|
|
714
|
+
const plugin = item.plugin;
|
|
715
|
+
const latestVersion = plugin.version || "0.0.0";
|
|
716
|
+
const scopeLabel =
|
|
717
|
+
scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
|
|
718
|
+
|
|
719
|
+
// Check if installed in this scope
|
|
720
|
+
const scopeData =
|
|
721
|
+
scope === "user"
|
|
722
|
+
? plugin.userScope
|
|
723
|
+
: scope === "project"
|
|
724
|
+
? plugin.projectScope
|
|
725
|
+
: plugin.localScope;
|
|
726
|
+
const isInstalledInScope = scopeData?.enabled;
|
|
727
|
+
const installedVersion = scopeData?.version;
|
|
728
|
+
|
|
729
|
+
// Check if this scope has an update available
|
|
730
|
+
const hasUpdateInScope =
|
|
731
|
+
isInstalledInScope &&
|
|
732
|
+
installedVersion &&
|
|
733
|
+
latestVersion !== "0.0.0" &&
|
|
734
|
+
installedVersion !== latestVersion;
|
|
735
|
+
|
|
736
|
+
// Determine action: update if available, otherwise toggle install/uninstall
|
|
737
|
+
let action: "update" | "install" | "uninstall";
|
|
738
|
+
if (isInstalledInScope && hasUpdateInScope) {
|
|
739
|
+
action = "update";
|
|
740
|
+
} else if (isInstalledInScope) {
|
|
741
|
+
action = "uninstall";
|
|
742
|
+
} else {
|
|
743
|
+
action = "install";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const actionLabel =
|
|
747
|
+
action === "update"
|
|
748
|
+
? `Updating ${scopeLabel}`
|
|
749
|
+
: action === "install"
|
|
750
|
+
? `Installing to ${scopeLabel}`
|
|
751
|
+
: `Uninstalling from ${scopeLabel}`;
|
|
752
|
+
modal.loading(`${actionLabel}...`);
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
if (action === "uninstall") {
|
|
756
|
+
// Uninstall from this scope
|
|
757
|
+
if (scope === "user") {
|
|
758
|
+
await enableGlobalPlugin(plugin.id, false);
|
|
759
|
+
await removeGlobalInstalledPluginVersion(plugin.id);
|
|
760
|
+
} else if (scope === "project") {
|
|
761
|
+
await enablePlugin(plugin.id, false, state.projectPath);
|
|
762
|
+
await removeInstalledPluginVersion(plugin.id, state.projectPath);
|
|
763
|
+
} else {
|
|
764
|
+
await enableLocalPlugin(plugin.id, false, state.projectPath);
|
|
765
|
+
await removeLocalInstalledPluginVersion(plugin.id, state.projectPath);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
// Install or update to this scope (both save the latest version)
|
|
769
|
+
if (scope === "user") {
|
|
770
|
+
await enableGlobalPlugin(plugin.id, true);
|
|
771
|
+
await saveGlobalInstalledPluginVersion(plugin.id, latestVersion);
|
|
772
|
+
} else if (scope === "project") {
|
|
773
|
+
await enablePlugin(plugin.id, true, state.projectPath);
|
|
774
|
+
await saveInstalledPluginVersion(
|
|
775
|
+
plugin.id,
|
|
776
|
+
latestVersion,
|
|
777
|
+
state.projectPath,
|
|
778
|
+
);
|
|
779
|
+
} else {
|
|
780
|
+
await enableLocalPlugin(plugin.id, true, state.projectPath);
|
|
781
|
+
await saveLocalInstalledPluginVersion(
|
|
782
|
+
plugin.id,
|
|
783
|
+
latestVersion,
|
|
784
|
+
state.projectPath,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// On fresh install, prompt for MCP server env vars if needed
|
|
789
|
+
if (action === "install") {
|
|
790
|
+
modal.hideModal();
|
|
791
|
+
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (action !== "install") {
|
|
795
|
+
modal.hideModal();
|
|
796
|
+
}
|
|
797
|
+
fetchData();
|
|
798
|
+
} catch (error) {
|
|
799
|
+
modal.hideModal();
|
|
800
|
+
await modal.message("Error", `Failed: ${error}`, "error");
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
const handleUninstall = async () => {
|
|
805
|
+
const item = selectableItems[pluginsState.selectedIndex];
|
|
806
|
+
if (!item || item.type !== "plugin" || !item.plugin) return;
|
|
807
|
+
|
|
808
|
+
const plugin = item.plugin;
|
|
809
|
+
|
|
810
|
+
// Build list of scopes where plugin is installed
|
|
811
|
+
const installedScopes: { label: string; value: string }[] = [];
|
|
812
|
+
if (plugin.userScope?.enabled) {
|
|
813
|
+
const ver = plugin.userScope.version
|
|
814
|
+
? ` v${plugin.userScope.version}`
|
|
815
|
+
: "";
|
|
816
|
+
installedScopes.push({ label: `User (global)${ver}`, value: "user" });
|
|
817
|
+
}
|
|
818
|
+
if (plugin.projectScope?.enabled) {
|
|
819
|
+
const ver = plugin.projectScope.version
|
|
820
|
+
? ` v${plugin.projectScope.version}`
|
|
821
|
+
: "";
|
|
822
|
+
installedScopes.push({ label: `Project${ver}`, value: "project" });
|
|
823
|
+
}
|
|
824
|
+
if (plugin.localScope?.enabled) {
|
|
825
|
+
const ver = plugin.localScope.version
|
|
826
|
+
? ` v${plugin.localScope.version}`
|
|
827
|
+
: "";
|
|
828
|
+
installedScopes.push({ label: `Local${ver}`, value: "local" });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (installedScopes.length === 0) {
|
|
832
|
+
await modal.message(
|
|
833
|
+
"Not Installed",
|
|
834
|
+
`${plugin.name} is not installed in any scope.`,
|
|
835
|
+
"info",
|
|
836
|
+
);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const scopeValue = await modal.select(
|
|
841
|
+
`Uninstall ${plugin.name}`,
|
|
842
|
+
`Installed in ${installedScopes.length} scope(s):`,
|
|
843
|
+
installedScopes,
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
if (scopeValue === null) return; // Cancelled
|
|
847
|
+
|
|
848
|
+
modal.loading(`Uninstalling ${plugin.name}...`);
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
if (scopeValue === "user") {
|
|
852
|
+
await enableGlobalPlugin(plugin.id, false);
|
|
853
|
+
await removeGlobalInstalledPluginVersion(plugin.id);
|
|
854
|
+
} else if (scopeValue === "project") {
|
|
855
|
+
await enablePlugin(plugin.id, false, state.projectPath);
|
|
856
|
+
await removeInstalledPluginVersion(plugin.id, state.projectPath);
|
|
857
|
+
} else {
|
|
858
|
+
// local scope
|
|
859
|
+
await enableLocalPlugin(plugin.id, false, state.projectPath);
|
|
860
|
+
await removeLocalInstalledPluginVersion(plugin.id, state.projectPath);
|
|
861
|
+
}
|
|
862
|
+
modal.hideModal();
|
|
863
|
+
fetchData();
|
|
864
|
+
} catch (error) {
|
|
865
|
+
modal.hideModal();
|
|
866
|
+
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// Render loading state
|
|
871
|
+
if (
|
|
872
|
+
pluginsState.marketplaces.status === "loading" ||
|
|
873
|
+
pluginsState.plugins.status === "loading"
|
|
874
|
+
) {
|
|
875
|
+
return (
|
|
876
|
+
<box flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
877
|
+
<text fg="#7e57c2"><strong>claudeup Plugins</strong></text>
|
|
878
|
+
<text fg="gray">Loading...</text>
|
|
879
|
+
</box>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Render error state
|
|
884
|
+
if (
|
|
885
|
+
pluginsState.marketplaces.status === "error" ||
|
|
886
|
+
pluginsState.plugins.status === "error"
|
|
887
|
+
) {
|
|
888
|
+
return (
|
|
889
|
+
<box flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
890
|
+
<text fg="#7e57c2"><strong>claudeup Plugins</strong></text>
|
|
891
|
+
<text fg="red">Error loading data</text>
|
|
892
|
+
</box>
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Get selected item for detail panel
|
|
897
|
+
const selectedItem = selectableItems[pluginsState.selectedIndex];
|
|
898
|
+
|
|
899
|
+
// Render item with fuzzy highlight support
|
|
900
|
+
const renderListItem = (
|
|
901
|
+
item: ListItem,
|
|
902
|
+
_idx: number,
|
|
903
|
+
isSelected: boolean,
|
|
904
|
+
) => {
|
|
905
|
+
if (item.type === "category" && item.marketplace) {
|
|
906
|
+
const mp = item.marketplace;
|
|
907
|
+
// Differentiate marketplace types with appropriate badges
|
|
908
|
+
let statusText = "";
|
|
909
|
+
let statusColor = "green";
|
|
910
|
+
if (item.marketplaceEnabled) {
|
|
911
|
+
if (mp.name === "claude-plugins-official") {
|
|
912
|
+
statusText = "★ Official";
|
|
913
|
+
statusColor = "yellow";
|
|
914
|
+
} else if (mp.name === "claude-code-plugins") {
|
|
915
|
+
statusText = "⚠ Deprecated";
|
|
916
|
+
statusColor = "gray";
|
|
917
|
+
} else if (mp.official) {
|
|
918
|
+
statusText = "★ Official";
|
|
919
|
+
statusColor = "yellow";
|
|
920
|
+
} else {
|
|
921
|
+
statusText = "✓ Added";
|
|
922
|
+
statusColor = "green";
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (isSelected) {
|
|
927
|
+
const arrow = item.isExpanded ? "▼" : "▶";
|
|
928
|
+
const count =
|
|
929
|
+
item.pluginCount !== undefined && item.pluginCount > 0
|
|
930
|
+
? ` (${item.pluginCount})`
|
|
931
|
+
: "";
|
|
932
|
+
return (
|
|
933
|
+
<text bg="magenta" fg="white"><strong> {arrow} {mp.displayName}{count} </strong></text>
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
<CategoryHeader
|
|
939
|
+
title={mp.displayName}
|
|
940
|
+
expanded={item.isExpanded}
|
|
941
|
+
count={item.pluginCount}
|
|
942
|
+
status={statusText}
|
|
943
|
+
statusColor={statusColor}
|
|
944
|
+
/>
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (item.type === "plugin" && item.plugin) {
|
|
949
|
+
const plugin = item.plugin;
|
|
950
|
+
let statusIcon = "○";
|
|
951
|
+
let statusColor = "gray";
|
|
952
|
+
|
|
953
|
+
if (plugin.enabled) {
|
|
954
|
+
statusIcon = "●";
|
|
955
|
+
statusColor = "green";
|
|
956
|
+
} else if (plugin.installedVersion) {
|
|
957
|
+
statusIcon = "●";
|
|
958
|
+
statusColor = "yellow";
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Build version string
|
|
962
|
+
let versionStr = "";
|
|
963
|
+
if (plugin.installedVersion && plugin.installedVersion !== "0.0.0") {
|
|
964
|
+
versionStr = ` v${plugin.installedVersion}`;
|
|
965
|
+
if (plugin.hasUpdate && plugin.version) {
|
|
966
|
+
versionStr += ` → v${plugin.version}`;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Get fuzzy match highlights if available
|
|
971
|
+
const matches = (item as ListItem & { _matches?: number[] })._matches;
|
|
972
|
+
const segments = matches ? highlightMatches(plugin.name, matches) : null;
|
|
973
|
+
|
|
974
|
+
if (isSelected) {
|
|
975
|
+
const displayText = ` ${statusIcon} ${plugin.name}${versionStr} `;
|
|
976
|
+
return (
|
|
977
|
+
<text bg="magenta" fg="white">
|
|
978
|
+
{displayText}
|
|
979
|
+
</text>
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// For non-selected, render with colors
|
|
984
|
+
const displayName = segments
|
|
985
|
+
? segments.map((seg) => seg.text).join("")
|
|
986
|
+
: plugin.name;
|
|
987
|
+
return (
|
|
988
|
+
<text>
|
|
989
|
+
<span fg={statusColor}>{" "}{statusIcon} </span>
|
|
990
|
+
<span>{displayName}</span>
|
|
991
|
+
<span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
|
|
992
|
+
</text>
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return <text fg="gray">{item.label}</text>;
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// Render detail content - compact to fit in available space
|
|
1000
|
+
const renderDetail = () => {
|
|
1001
|
+
if (!selectedItem) {
|
|
1002
|
+
return <text fg="gray">Select an item</text>;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (selectedItem.type === "category" && selectedItem.marketplace) {
|
|
1006
|
+
const mp = selectedItem.marketplace;
|
|
1007
|
+
const isEnabled = selectedItem.marketplaceEnabled;
|
|
1008
|
+
|
|
1009
|
+
// Get appropriate badge for marketplace type
|
|
1010
|
+
const getBadge = () => {
|
|
1011
|
+
if (mp.name === "claude-plugins-official") return " ★";
|
|
1012
|
+
if (mp.name === "claude-code-plugins") return " ⚠";
|
|
1013
|
+
if (mp.official) return " ★";
|
|
1014
|
+
return "";
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// Determine action hint based on state
|
|
1018
|
+
const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
|
|
1019
|
+
const hasPlugins = (selectedItem.pluginCount || 0) > 0;
|
|
1020
|
+
let actionHint = "Add";
|
|
1021
|
+
if (isEnabled) {
|
|
1022
|
+
if (isCollapsed) {
|
|
1023
|
+
actionHint = "Expand";
|
|
1024
|
+
} else if (hasPlugins) {
|
|
1025
|
+
actionHint = "Collapse";
|
|
1026
|
+
} else {
|
|
1027
|
+
actionHint = "Remove";
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return (
|
|
1032
|
+
<box flexDirection="column">
|
|
1033
|
+
<text fg="cyan"><strong>{mp.displayName}{getBadge()}</strong></text>
|
|
1034
|
+
<text fg="gray">{mp.description || "No description"}</text>
|
|
1035
|
+
<text fg={isEnabled ? "green" : "gray"}>
|
|
1036
|
+
{isEnabled ? "● Added" : "○ Not added"}
|
|
1037
|
+
</text>
|
|
1038
|
+
<text fg="blue">github.com/{mp.source.repo}</text>
|
|
1039
|
+
<text>Plugins: {selectedItem.pluginCount || 0}</text>
|
|
1040
|
+
<box marginTop={1}>
|
|
1041
|
+
<text bg={isEnabled ? "cyan" : "green"} fg="black"> Enter </text>
|
|
1042
|
+
<text fg="gray"> {actionHint}</text>
|
|
1043
|
+
</box>
|
|
1044
|
+
{isEnabled && (
|
|
1045
|
+
<box>
|
|
1046
|
+
<text fg="gray">← → to expand/collapse</text>
|
|
1047
|
+
</box>
|
|
1048
|
+
)}
|
|
1049
|
+
</box>
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (selectedItem.type === "plugin" && selectedItem.plugin) {
|
|
1054
|
+
const plugin = selectedItem.plugin;
|
|
1055
|
+
const isInstalled = plugin.enabled || plugin.installedVersion;
|
|
1056
|
+
|
|
1057
|
+
// Build component counts
|
|
1058
|
+
const components: string[] = [];
|
|
1059
|
+
if (plugin.agents?.length)
|
|
1060
|
+
components.push(`${plugin.agents.length} agents`);
|
|
1061
|
+
if (plugin.commands?.length)
|
|
1062
|
+
components.push(`${plugin.commands.length} commands`);
|
|
1063
|
+
if (plugin.skills?.length)
|
|
1064
|
+
components.push(`${plugin.skills.length} skills`);
|
|
1065
|
+
if (plugin.mcpServers?.length)
|
|
1066
|
+
components.push(`${plugin.mcpServers.length} MCP`);
|
|
1067
|
+
if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
|
|
1068
|
+
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Show version only if valid (not null, not 0.0.0)
|
|
1072
|
+
const showVersion = plugin.version && plugin.version !== "0.0.0";
|
|
1073
|
+
const showInstalledVersion =
|
|
1074
|
+
plugin.installedVersion && plugin.installedVersion !== "0.0.0";
|
|
1075
|
+
|
|
1076
|
+
return (
|
|
1077
|
+
<box flexDirection="column">
|
|
1078
|
+
{/* Plugin name header - centered */}
|
|
1079
|
+
<box justifyContent="center">
|
|
1080
|
+
<text bg="magenta" fg="white"><strong> {plugin.name}{plugin.hasUpdate ? " ⬆" : ""} </strong></text>
|
|
1081
|
+
</box>
|
|
1082
|
+
|
|
1083
|
+
{/* Status line */}
|
|
1084
|
+
<box marginTop={1}>
|
|
1085
|
+
{isInstalled ? (
|
|
1086
|
+
<text fg={plugin.enabled ? "green" : "yellow"}>
|
|
1087
|
+
{plugin.enabled ? "● Enabled" : "● Disabled"}
|
|
1088
|
+
</text>
|
|
1089
|
+
) : (
|
|
1090
|
+
<text fg="gray">○ Not installed</text>
|
|
1091
|
+
)}
|
|
1092
|
+
</box>
|
|
1093
|
+
|
|
1094
|
+
{/* Description */}
|
|
1095
|
+
<box marginTop={1} marginBottom={1}>
|
|
1096
|
+
<text fg="white">{plugin.description}</text>
|
|
1097
|
+
</box>
|
|
1098
|
+
|
|
1099
|
+
{/* Metadata */}
|
|
1100
|
+
{showVersion && (
|
|
1101
|
+
<text>
|
|
1102
|
+
<span>Version </span>
|
|
1103
|
+
<span fg="blue">v{plugin.version}</span>
|
|
1104
|
+
{showInstalledVersion &&
|
|
1105
|
+
plugin.installedVersion !== plugin.version && (
|
|
1106
|
+
<span> (v{plugin.installedVersion} installed)</span>
|
|
1107
|
+
)}
|
|
1108
|
+
</text>
|
|
1109
|
+
)}
|
|
1110
|
+
{plugin.category && (
|
|
1111
|
+
<text>
|
|
1112
|
+
<span>Category </span>
|
|
1113
|
+
<span fg="magenta">{plugin.category}</span>
|
|
1114
|
+
</text>
|
|
1115
|
+
)}
|
|
1116
|
+
{plugin.author && (
|
|
1117
|
+
<text>
|
|
1118
|
+
<span>Author </span>
|
|
1119
|
+
<span>{plugin.author.name}</span>
|
|
1120
|
+
</text>
|
|
1121
|
+
)}
|
|
1122
|
+
{components.length > 0 && (
|
|
1123
|
+
<text>
|
|
1124
|
+
<span>Contains </span>
|
|
1125
|
+
<span fg="yellow">{components.join(" · ")}</span>
|
|
1126
|
+
</text>
|
|
1127
|
+
)}
|
|
1128
|
+
|
|
1129
|
+
{/* Scope Status with shortcuts - each scope has its own color */}
|
|
1130
|
+
<box flexDirection="column" marginTop={1}>
|
|
1131
|
+
<text>────────────────────────</text>
|
|
1132
|
+
<text><strong>Scopes:</strong></text>
|
|
1133
|
+
<box marginTop={1} flexDirection="column">
|
|
1134
|
+
<text>
|
|
1135
|
+
<span bg="cyan" fg="black"> u </span>
|
|
1136
|
+
<span fg={plugin.userScope?.enabled ? "cyan" : "gray"}>
|
|
1137
|
+
{plugin.userScope?.enabled ? " ● " : " ○ "}
|
|
1138
|
+
</span>
|
|
1139
|
+
<span fg="cyan">User</span>
|
|
1140
|
+
<span> global</span>
|
|
1141
|
+
{plugin.userScope?.version && (
|
|
1142
|
+
<span fg="cyan"> v{plugin.userScope.version}</span>
|
|
1143
|
+
)}
|
|
1144
|
+
</text>
|
|
1145
|
+
<text>
|
|
1146
|
+
<span bg="green" fg="black"> p </span>
|
|
1147
|
+
<span fg={plugin.projectScope?.enabled ? "green" : "gray"}>
|
|
1148
|
+
{plugin.projectScope?.enabled ? " ● " : " ○ "}
|
|
1149
|
+
</span>
|
|
1150
|
+
<span fg="green">Project</span>
|
|
1151
|
+
<span> team</span>
|
|
1152
|
+
{plugin.projectScope?.version && (
|
|
1153
|
+
<span fg="green"> v{plugin.projectScope.version}</span>
|
|
1154
|
+
)}
|
|
1155
|
+
</text>
|
|
1156
|
+
<text>
|
|
1157
|
+
<span bg="yellow" fg="black"> l </span>
|
|
1158
|
+
<span fg={plugin.localScope?.enabled ? "yellow" : "gray"}>
|
|
1159
|
+
{plugin.localScope?.enabled ? " ● " : " ○ "}
|
|
1160
|
+
</span>
|
|
1161
|
+
<span fg="yellow">Local</span>
|
|
1162
|
+
<span> private</span>
|
|
1163
|
+
{plugin.localScope?.version && (
|
|
1164
|
+
<span fg="yellow"> v{plugin.localScope.version}</span>
|
|
1165
|
+
)}
|
|
1166
|
+
</text>
|
|
1167
|
+
</box>
|
|
1168
|
+
</box>
|
|
1169
|
+
|
|
1170
|
+
{/* Additional actions */}
|
|
1171
|
+
{isInstalled && (
|
|
1172
|
+
<box flexDirection="column" marginTop={1}>
|
|
1173
|
+
{plugin.hasUpdate && (
|
|
1174
|
+
<box>
|
|
1175
|
+
<text bg="magenta" fg="white"> U </text>
|
|
1176
|
+
<text> Update to v{plugin.version}</text>
|
|
1177
|
+
</box>
|
|
1178
|
+
)}
|
|
1179
|
+
<box>
|
|
1180
|
+
<text bg="red" fg="white"> d </text>
|
|
1181
|
+
<text> Uninstall</text>
|
|
1182
|
+
</box>
|
|
1183
|
+
</box>
|
|
1184
|
+
)}
|
|
1185
|
+
</box>
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return null;
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const footerHints = isSearchActive
|
|
1193
|
+
? "Type to search │ Enter Confirm │ Esc Cancel"
|
|
1194
|
+
: "u/p/l:scope │ U:update │ a:all │ d:remove │ n:add │ t:team │ /:search";
|
|
1195
|
+
|
|
1196
|
+
// Calculate status for subtitle
|
|
1197
|
+
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
1198
|
+
const plugins =
|
|
1199
|
+
pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
1200
|
+
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
1201
|
+
const updateCount = plugins.filter((p) => p.hasUpdate).length;
|
|
1202
|
+
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
|
|
1203
|
+
|
|
1204
|
+
// Search placeholder shows status when not searching
|
|
1205
|
+
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
|
|
1206
|
+
|
|
1207
|
+
return (
|
|
1208
|
+
<ScreenLayout
|
|
1209
|
+
title="claudeup Plugins"
|
|
1210
|
+
subtitle={subtitle}
|
|
1211
|
+
currentScreen="plugins"
|
|
1212
|
+
search={{
|
|
1213
|
+
isActive: isSearchActive,
|
|
1214
|
+
query: pluginsState.searchQuery,
|
|
1215
|
+
placeholder: searchPlaceholder,
|
|
1216
|
+
}}
|
|
1217
|
+
footerHints={footerHints}
|
|
1218
|
+
listPanel={
|
|
1219
|
+
<ScrollableList
|
|
1220
|
+
items={selectableItems}
|
|
1221
|
+
selectedIndex={pluginsState.selectedIndex}
|
|
1222
|
+
renderItem={renderListItem}
|
|
1223
|
+
maxHeight={dimensions.listPanelHeight}
|
|
1224
|
+
/>
|
|
1225
|
+
}
|
|
1226
|
+
detailPanel={renderDetail()}
|
|
1227
|
+
/>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export default PluginsScreen;
|