claudeup 1.7.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.
Files changed (302) hide show
  1. package/bin/claudeup.js +20 -2
  2. package/package.json +10 -19
  3. package/src/data/cli-tools.js +123 -0
  4. package/src/data/cli-tools.ts +140 -0
  5. package/{dist → src}/data/marketplaces.js +23 -24
  6. package/src/data/marketplaces.ts +95 -0
  7. package/src/data/mcp-servers.js +509 -0
  8. package/src/data/mcp-servers.ts +526 -0
  9. package/src/data/statuslines.js +159 -0
  10. package/src/data/statuslines.ts +188 -0
  11. package/src/index.js +4 -0
  12. package/src/index.ts +5 -0
  13. package/{dist → src}/main.js +46 -47
  14. package/src/main.tsx +145 -0
  15. package/src/opentui.d.ts +191 -0
  16. package/src/prerunner/index.js +87 -0
  17. package/src/prerunner/index.ts +124 -0
  18. package/{dist → src}/services/claude-runner.js +9 -10
  19. package/src/services/claude-runner.ts +31 -0
  20. package/{dist → src}/services/claude-settings.js +72 -33
  21. package/src/services/claude-settings.ts +934 -0
  22. package/src/services/local-marketplace.js +339 -0
  23. package/src/services/local-marketplace.ts +489 -0
  24. package/{dist → src}/services/mcp-registry.js +13 -14
  25. package/src/services/mcp-registry.ts +105 -0
  26. package/{dist → src}/services/plugin-manager.js +61 -13
  27. package/src/services/plugin-manager.ts +693 -0
  28. package/{dist → src}/services/plugin-mcp-config.js +19 -18
  29. package/src/services/plugin-mcp-config.ts +242 -0
  30. package/{dist → src}/services/update-cache.js +7 -8
  31. package/src/services/update-cache.ts +78 -0
  32. package/{dist → src}/services/version-check.js +15 -14
  33. package/src/services/version-check.ts +122 -0
  34. package/src/types/index.js +1 -0
  35. package/src/types/index.ts +141 -0
  36. package/src/ui/App.js +213 -0
  37. package/src/ui/App.tsx +359 -0
  38. package/src/ui/components/CategoryHeader.js +9 -0
  39. package/src/ui/components/CategoryHeader.tsx +41 -0
  40. package/{dist → src}/ui/components/ScrollableList.js +19 -6
  41. package/src/ui/components/ScrollableList.tsx +98 -0
  42. package/src/ui/components/SearchInput.js +19 -0
  43. package/src/ui/components/SearchInput.tsx +56 -0
  44. package/src/ui/components/StyledText.js +39 -0
  45. package/src/ui/components/StyledText.tsx +70 -0
  46. package/src/ui/components/TabBar.js +38 -0
  47. package/src/ui/components/TabBar.tsx +88 -0
  48. package/src/ui/components/layout/Panel.js +6 -0
  49. package/src/ui/components/layout/Panel.tsx +62 -0
  50. package/src/ui/components/layout/ProgressBar.js +14 -0
  51. package/src/ui/components/layout/ProgressBar.tsx +47 -0
  52. package/src/ui/components/layout/ScopeTabs.js +6 -0
  53. package/src/ui/components/layout/ScopeTabs.tsx +53 -0
  54. package/src/ui/components/layout/ScreenLayout.js +21 -0
  55. package/src/ui/components/layout/ScreenLayout.tsx +147 -0
  56. package/src/ui/components/layout/index.js +4 -0
  57. package/src/ui/components/layout/index.ts +4 -0
  58. package/src/ui/components/modals/ConfirmModal.js +14 -0
  59. package/src/ui/components/modals/ConfirmModal.tsx +59 -0
  60. package/src/ui/components/modals/InputModal.js +16 -0
  61. package/src/ui/components/modals/InputModal.tsx +68 -0
  62. package/src/ui/components/modals/LoadingModal.js +14 -0
  63. package/src/ui/components/modals/LoadingModal.tsx +40 -0
  64. package/src/ui/components/modals/MessageModal.js +16 -0
  65. package/src/ui/components/modals/MessageModal.tsx +64 -0
  66. package/src/ui/components/modals/ModalContainer.js +56 -0
  67. package/src/ui/components/modals/ModalContainer.tsx +104 -0
  68. package/src/ui/components/modals/SelectModal.js +26 -0
  69. package/src/ui/components/modals/SelectModal.tsx +82 -0
  70. package/src/ui/components/modals/index.js +6 -0
  71. package/src/ui/components/modals/index.ts +6 -0
  72. package/src/ui/hooks/index.js +3 -0
  73. package/src/ui/hooks/index.ts +3 -0
  74. package/{dist → src}/ui/hooks/useAsyncData.js +21 -22
  75. package/src/ui/hooks/useAsyncData.ts +127 -0
  76. package/src/ui/hooks/useKeyboard.js +13 -0
  77. package/src/ui/hooks/useKeyboard.ts +26 -0
  78. package/src/ui/hooks/useKeyboardHandler.js +39 -0
  79. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  80. package/{dist → src}/ui/screens/CliToolsScreen.js +60 -54
  81. package/src/ui/screens/CliToolsScreen.tsx +468 -0
  82. package/src/ui/screens/EnvVarsScreen.js +154 -0
  83. package/src/ui/screens/EnvVarsScreen.tsx +269 -0
  84. package/{dist → src}/ui/screens/McpRegistryScreen.js +56 -55
  85. package/src/ui/screens/McpRegistryScreen.tsx +331 -0
  86. package/{dist → src}/ui/screens/McpScreen.js +46 -47
  87. package/src/ui/screens/McpScreen.tsx +392 -0
  88. package/src/ui/screens/ModelSelectorScreen.js +292 -0
  89. package/src/ui/screens/ModelSelectorScreen.tsx +441 -0
  90. package/{dist → src}/ui/screens/PluginsScreen.js +305 -293
  91. package/src/ui/screens/PluginsScreen.tsx +1231 -0
  92. package/src/ui/screens/StatusLineScreen.js +200 -0
  93. package/src/ui/screens/StatusLineScreen.tsx +411 -0
  94. package/src/ui/screens/index.js +7 -0
  95. package/src/ui/screens/index.ts +7 -0
  96. package/src/ui/state/AnimationContext.js +34 -0
  97. package/src/ui/state/AnimationContext.tsx +76 -0
  98. package/{dist → src}/ui/state/AppContext.js +31 -32
  99. package/src/ui/state/AppContext.tsx +235 -0
  100. package/{dist → src}/ui/state/DimensionsContext.js +16 -17
  101. package/src/ui/state/DimensionsContext.tsx +144 -0
  102. package/{dist → src}/ui/state/reducer.js +89 -90
  103. package/src/ui/state/reducer.ts +467 -0
  104. package/src/ui/state/types.js +1 -0
  105. package/src/ui/state/types.ts +273 -0
  106. package/{dist → src}/utils/command-utils.js +3 -4
  107. package/src/utils/command-utils.ts +20 -0
  108. package/{dist → src}/utils/fuzzy-search.js +2 -3
  109. package/src/utils/fuzzy-search.ts +138 -0
  110. package/{dist → src}/utils/string-utils.js +6 -6
  111. package/src/utils/string-utils.ts +88 -0
  112. package/dist/data/cli-tools.d.ts +0 -13
  113. package/dist/data/cli-tools.d.ts.map +0 -1
  114. package/dist/data/cli-tools.js +0 -124
  115. package/dist/data/cli-tools.js.map +0 -1
  116. package/dist/data/marketplaces.d.ts +0 -6
  117. package/dist/data/marketplaces.d.ts.map +0 -1
  118. package/dist/data/marketplaces.js.map +0 -1
  119. package/dist/data/mcp-servers.d.ts +0 -8
  120. package/dist/data/mcp-servers.d.ts.map +0 -1
  121. package/dist/data/mcp-servers.js +0 -503
  122. package/dist/data/mcp-servers.js.map +0 -1
  123. package/dist/data/statuslines.d.ts +0 -10
  124. package/dist/data/statuslines.d.ts.map +0 -1
  125. package/dist/data/statuslines.js +0 -160
  126. package/dist/data/statuslines.js.map +0 -1
  127. package/dist/index.d.ts +0 -3
  128. package/dist/index.d.ts.map +0 -1
  129. package/dist/index.js +0 -90
  130. package/dist/index.js.map +0 -1
  131. package/dist/main.d.ts +0 -3
  132. package/dist/main.d.ts.map +0 -1
  133. package/dist/main.js.map +0 -1
  134. package/dist/prerunner/index.d.ts +0 -7
  135. package/dist/prerunner/index.d.ts.map +0 -1
  136. package/dist/prerunner/index.js +0 -64
  137. package/dist/prerunner/index.js.map +0 -1
  138. package/dist/services/claude-runner.d.ts +0 -7
  139. package/dist/services/claude-runner.d.ts.map +0 -1
  140. package/dist/services/claude-runner.js.map +0 -1
  141. package/dist/services/claude-settings.d.ts +0 -73
  142. package/dist/services/claude-settings.d.ts.map +0 -1
  143. package/dist/services/claude-settings.js.map +0 -1
  144. package/dist/services/local-marketplace.d.ts +0 -111
  145. package/dist/services/local-marketplace.d.ts.map +0 -1
  146. package/dist/services/local-marketplace.js +0 -599
  147. package/dist/services/local-marketplace.js.map +0 -1
  148. package/dist/services/mcp-registry.d.ts +0 -10
  149. package/dist/services/mcp-registry.d.ts.map +0 -1
  150. package/dist/services/mcp-registry.js.map +0 -1
  151. package/dist/services/plugin-manager.d.ts +0 -65
  152. package/dist/services/plugin-manager.d.ts.map +0 -1
  153. package/dist/services/plugin-manager.js.map +0 -1
  154. package/dist/services/plugin-mcp-config.d.ts +0 -52
  155. package/dist/services/plugin-mcp-config.d.ts.map +0 -1
  156. package/dist/services/plugin-mcp-config.js.map +0 -1
  157. package/dist/services/update-cache.d.ts +0 -16
  158. package/dist/services/update-cache.d.ts.map +0 -1
  159. package/dist/services/update-cache.js.map +0 -1
  160. package/dist/services/version-check.d.ts +0 -20
  161. package/dist/services/version-check.d.ts.map +0 -1
  162. package/dist/services/version-check.js.map +0 -1
  163. package/dist/types/index.d.ts +0 -105
  164. package/dist/types/index.d.ts.map +0 -1
  165. package/dist/types/index.js +0 -2
  166. package/dist/types/index.js.map +0 -1
  167. package/dist/ui/InkApp.d.ts +0 -5
  168. package/dist/ui/InkApp.d.ts.map +0 -1
  169. package/dist/ui/InkApp.js +0 -188
  170. package/dist/ui/InkApp.js.map +0 -1
  171. package/dist/ui/components/CategoryHeader.d.ts +0 -16
  172. package/dist/ui/components/CategoryHeader.d.ts.map +0 -1
  173. package/dist/ui/components/CategoryHeader.js +0 -11
  174. package/dist/ui/components/CategoryHeader.js.map +0 -1
  175. package/dist/ui/components/ScrollableList.d.ts +0 -16
  176. package/dist/ui/components/ScrollableList.d.ts.map +0 -1
  177. package/dist/ui/components/ScrollableList.js.map +0 -1
  178. package/dist/ui/components/SearchInput.d.ts +0 -18
  179. package/dist/ui/components/SearchInput.d.ts.map +0 -1
  180. package/dist/ui/components/SearchInput.js +0 -30
  181. package/dist/ui/components/SearchInput.js.map +0 -1
  182. package/dist/ui/components/TabBar.d.ts +0 -8
  183. package/dist/ui/components/TabBar.d.ts.map +0 -1
  184. package/dist/ui/components/TabBar.js +0 -18
  185. package/dist/ui/components/TabBar.js.map +0 -1
  186. package/dist/ui/components/layout/Footer.d.ts +0 -14
  187. package/dist/ui/components/layout/Footer.d.ts.map +0 -1
  188. package/dist/ui/components/layout/Footer.js +0 -23
  189. package/dist/ui/components/layout/Footer.js.map +0 -1
  190. package/dist/ui/components/layout/Header.d.ts +0 -4
  191. package/dist/ui/components/layout/Header.d.ts.map +0 -1
  192. package/dist/ui/components/layout/Header.js +0 -25
  193. package/dist/ui/components/layout/Header.js.map +0 -1
  194. package/dist/ui/components/layout/Panel.d.ts +0 -22
  195. package/dist/ui/components/layout/Panel.d.ts.map +0 -1
  196. package/dist/ui/components/layout/Panel.js +0 -8
  197. package/dist/ui/components/layout/Panel.js.map +0 -1
  198. package/dist/ui/components/layout/ProgressBar.d.ts +0 -12
  199. package/dist/ui/components/layout/ProgressBar.d.ts.map +0 -1
  200. package/dist/ui/components/layout/ProgressBar.js +0 -16
  201. package/dist/ui/components/layout/ProgressBar.js.map +0 -1
  202. package/dist/ui/components/layout/ScopeTabs.d.ts +0 -12
  203. package/dist/ui/components/layout/ScopeTabs.d.ts.map +0 -1
  204. package/dist/ui/components/layout/ScopeTabs.js +0 -8
  205. package/dist/ui/components/layout/ScopeTabs.js.map +0 -1
  206. package/dist/ui/components/layout/ScreenLayout.d.ts +0 -30
  207. package/dist/ui/components/layout/ScreenLayout.d.ts.map +0 -1
  208. package/dist/ui/components/layout/ScreenLayout.js +0 -23
  209. package/dist/ui/components/layout/ScreenLayout.js.map +0 -1
  210. package/dist/ui/components/layout/index.d.ts +0 -7
  211. package/dist/ui/components/layout/index.d.ts.map +0 -1
  212. package/dist/ui/components/layout/index.js +0 -7
  213. package/dist/ui/components/layout/index.js.map +0 -1
  214. package/dist/ui/components/modals/ConfirmModal.d.ts +0 -14
  215. package/dist/ui/components/modals/ConfirmModal.d.ts.map +0 -1
  216. package/dist/ui/components/modals/ConfirmModal.js +0 -15
  217. package/dist/ui/components/modals/ConfirmModal.js.map +0 -1
  218. package/dist/ui/components/modals/InputModal.d.ts +0 -16
  219. package/dist/ui/components/modals/InputModal.d.ts.map +0 -1
  220. package/dist/ui/components/modals/InputModal.js +0 -23
  221. package/dist/ui/components/modals/InputModal.js.map +0 -1
  222. package/dist/ui/components/modals/LoadingModal.d.ts +0 -8
  223. package/dist/ui/components/modals/LoadingModal.d.ts.map +0 -1
  224. package/dist/ui/components/modals/LoadingModal.js +0 -8
  225. package/dist/ui/components/modals/LoadingModal.js.map +0 -1
  226. package/dist/ui/components/modals/MessageModal.d.ts +0 -14
  227. package/dist/ui/components/modals/MessageModal.d.ts.map +0 -1
  228. package/dist/ui/components/modals/MessageModal.js +0 -17
  229. package/dist/ui/components/modals/MessageModal.js.map +0 -1
  230. package/dist/ui/components/modals/ModalContainer.d.ts +0 -7
  231. package/dist/ui/components/modals/ModalContainer.d.ts.map +0 -1
  232. package/dist/ui/components/modals/ModalContainer.js +0 -38
  233. package/dist/ui/components/modals/ModalContainer.js.map +0 -1
  234. package/dist/ui/components/modals/SelectModal.d.ts +0 -17
  235. package/dist/ui/components/modals/SelectModal.d.ts.map +0 -1
  236. package/dist/ui/components/modals/SelectModal.js +0 -33
  237. package/dist/ui/components/modals/SelectModal.js.map +0 -1
  238. package/dist/ui/components/modals/index.d.ts +0 -7
  239. package/dist/ui/components/modals/index.d.ts.map +0 -1
  240. package/dist/ui/components/modals/index.js +0 -7
  241. package/dist/ui/components/modals/index.js.map +0 -1
  242. package/dist/ui/hooks/index.d.ts +0 -3
  243. package/dist/ui/hooks/index.d.ts.map +0 -1
  244. package/dist/ui/hooks/index.js +0 -3
  245. package/dist/ui/hooks/index.js.map +0 -1
  246. package/dist/ui/hooks/useAsyncData.d.ts +0 -40
  247. package/dist/ui/hooks/useAsyncData.d.ts.map +0 -1
  248. package/dist/ui/hooks/useAsyncData.js.map +0 -1
  249. package/dist/ui/hooks/useKeyboardNavigation.d.ts +0 -27
  250. package/dist/ui/hooks/useKeyboardNavigation.d.ts.map +0 -1
  251. package/dist/ui/hooks/useKeyboardNavigation.js +0 -82
  252. package/dist/ui/hooks/useKeyboardNavigation.js.map +0 -1
  253. package/dist/ui/screens/CliToolsScreen.d.ts +0 -4
  254. package/dist/ui/screens/CliToolsScreen.d.ts.map +0 -1
  255. package/dist/ui/screens/CliToolsScreen.js.map +0 -1
  256. package/dist/ui/screens/EnvVarsScreen.d.ts +0 -4
  257. package/dist/ui/screens/EnvVarsScreen.d.ts.map +0 -1
  258. package/dist/ui/screens/EnvVarsScreen.js +0 -145
  259. package/dist/ui/screens/EnvVarsScreen.js.map +0 -1
  260. package/dist/ui/screens/McpRegistryScreen.d.ts +0 -4
  261. package/dist/ui/screens/McpRegistryScreen.d.ts.map +0 -1
  262. package/dist/ui/screens/McpRegistryScreen.js.map +0 -1
  263. package/dist/ui/screens/McpScreen.d.ts +0 -4
  264. package/dist/ui/screens/McpScreen.d.ts.map +0 -1
  265. package/dist/ui/screens/McpScreen.js.map +0 -1
  266. package/dist/ui/screens/ModelSelectorScreen.d.ts +0 -4
  267. package/dist/ui/screens/ModelSelectorScreen.d.ts.map +0 -1
  268. package/dist/ui/screens/ModelSelectorScreen.js +0 -143
  269. package/dist/ui/screens/ModelSelectorScreen.js.map +0 -1
  270. package/dist/ui/screens/PluginsScreen.d.ts +0 -4
  271. package/dist/ui/screens/PluginsScreen.d.ts.map +0 -1
  272. package/dist/ui/screens/PluginsScreen.js.map +0 -1
  273. package/dist/ui/screens/StatusLineScreen.d.ts +0 -4
  274. package/dist/ui/screens/StatusLineScreen.d.ts.map +0 -1
  275. package/dist/ui/screens/StatusLineScreen.js +0 -197
  276. package/dist/ui/screens/StatusLineScreen.js.map +0 -1
  277. package/dist/ui/screens/index.d.ts +0 -8
  278. package/dist/ui/screens/index.d.ts.map +0 -1
  279. package/dist/ui/screens/index.js +0 -8
  280. package/dist/ui/screens/index.js.map +0 -1
  281. package/dist/ui/state/AppContext.d.ts +0 -40
  282. package/dist/ui/state/AppContext.d.ts.map +0 -1
  283. package/dist/ui/state/AppContext.js.map +0 -1
  284. package/dist/ui/state/DimensionsContext.d.ts +0 -27
  285. package/dist/ui/state/DimensionsContext.d.ts.map +0 -1
  286. package/dist/ui/state/DimensionsContext.js.map +0 -1
  287. package/dist/ui/state/reducer.d.ts +0 -4
  288. package/dist/ui/state/reducer.d.ts.map +0 -1
  289. package/dist/ui/state/reducer.js.map +0 -1
  290. package/dist/ui/state/types.d.ts +0 -266
  291. package/dist/ui/state/types.d.ts.map +0 -1
  292. package/dist/ui/state/types.js +0 -2
  293. package/dist/ui/state/types.js.map +0 -1
  294. package/dist/utils/command-utils.d.ts +0 -8
  295. package/dist/utils/command-utils.d.ts.map +0 -1
  296. package/dist/utils/command-utils.js.map +0 -1
  297. package/dist/utils/fuzzy-search.d.ts +0 -33
  298. package/dist/utils/fuzzy-search.d.ts.map +0 -1
  299. package/dist/utils/fuzzy-search.js.map +0 -1
  300. package/dist/utils/string-utils.d.ts +0 -24
  301. package/dist/utils/string-utils.d.ts.map +0 -1
  302. 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;