claudeup 4.18.0 → 4.22.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 (191) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -1,766 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useMemo } from "react";
3
- import { useApp, useModal, useProgress } from "../state/AppContext.js";
4
- import { useDimensions } from "../state/DimensionsContext.js";
5
- import { useKeyboard } from "../hooks/useKeyboard.js";
6
- import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
- import { EmptyFilterState } from "../components/EmptyFilterState.js";
9
- import { fuzzyFilter } from "../../utils/fuzzy-search.js";
10
- import { getAllMarketplaces } from "../../data/marketplaces.js";
11
- import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, saveInstalledPluginVersion, } from "../../services/plugin-manager.js";
12
- import { setMcpEnvVar, getMcpEnvVars, readSettings, saveGlobalInstalledPluginVersion, saveLocalInstalledPluginVersion, } from "../../services/claude-settings.js";
13
- import { saveProfile } from "../../services/profiles.js";
14
- import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
15
- import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
16
- import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
17
- import { checkPluginVersionMismatches, checkSinglePluginMismatch, } from "../../services/plugin-version-check.js";
18
- import { useMismatchModal } from "../hooks/useMismatchModal.js";
19
- import { buildPluginBrowserItems, } from "../adapters/pluginsAdapter.js";
20
- import { renderPluginRow, renderPluginDetail, } from "../renderers/pluginRenderers.js";
21
- export function PluginsScreen() {
22
- const { state, dispatch } = useApp();
23
- const { plugins: pluginsState } = state;
24
- const modal = useModal();
25
- const progress = useProgress();
26
- const dimensions = useDimensions();
27
- const mismatchModal = useMismatchModal();
28
- const isSearchActive = state.isSearching &&
29
- state.currentRoute.screen === "plugins" &&
30
- !state.modal;
31
- // Fetch data (always fetches all scopes)
32
- const fetchData = useCallback(async () => {
33
- dispatch({ type: "PLUGINS_DATA_LOADING" });
34
- try {
35
- const localMarketplaces = await getLocalMarketplacesInfo();
36
- const allMarketplaces = getAllMarketplaces(localMarketplaces);
37
- const pluginData = await getAvailablePlugins(state.projectPath);
38
- dispatch({
39
- type: "PLUGINS_DATA_SUCCESS",
40
- marketplaces: allMarketplaces,
41
- plugins: pluginData,
42
- });
43
- }
44
- catch (error) {
45
- dispatch({
46
- type: "PLUGINS_DATA_ERROR",
47
- error: error instanceof Error ? error : new Error(String(error)),
48
- });
49
- }
50
- }, [dispatch, state.projectPath]);
51
- // Load data on mount or when dataRefreshVersion changes
52
- useEffect(() => {
53
- fetchData();
54
- }, [fetchData, state.dataRefreshVersion]);
55
- // Build list items (categories + plugins)
56
- const allItems = useMemo(() => {
57
- if (pluginsState.marketplaces.status !== "success" ||
58
- pluginsState.plugins.status !== "success") {
59
- return [];
60
- }
61
- return buildPluginBrowserItems({
62
- marketplaces: pluginsState.marketplaces.data,
63
- plugins: pluginsState.plugins.data,
64
- collapsedMarketplaces: pluginsState.collapsedMarketplaces,
65
- });
66
- }, [
67
- pluginsState.marketplaces,
68
- pluginsState.plugins,
69
- pluginsState.collapsedMarketplaces,
70
- ]);
71
- // Filter items by search query
72
- const filteredItems = useMemo(() => {
73
- const query = pluginsState.searchQuery.trim();
74
- if (!query)
75
- return allItems;
76
- // Only search plugins, not categories
77
- const pluginItems = allItems.filter((item) => item.kind === "plugin");
78
- const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
79
- const matchedPluginIds = new Set();
80
- for (const result of fuzzyResults) {
81
- matchedPluginIds.add(result.item.id);
82
- }
83
- // Walk allItems sequentially: include a category only if any plugin below matched
84
- const categoryHasMatch = new Map();
85
- let currentCategoryId = null;
86
- for (const item of allItems) {
87
- if (item.kind === "category") {
88
- currentCategoryId = item.id;
89
- if (!categoryHasMatch.has(item.id)) {
90
- categoryHasMatch.set(item.id, false);
91
- }
92
- }
93
- else if (item.kind === "plugin" && currentCategoryId) {
94
- if (matchedPluginIds.has(item.id)) {
95
- categoryHasMatch.set(currentCategoryId, true);
96
- }
97
- }
98
- }
99
- const result = [];
100
- let currentCatIncluded = false;
101
- currentCategoryId = null;
102
- for (const item of allItems) {
103
- if (item.kind === "category") {
104
- currentCategoryId = item.id;
105
- currentCatIncluded = categoryHasMatch.get(item.id) === true;
106
- if (currentCatIncluded)
107
- result.push(item);
108
- }
109
- else if (item.kind === "plugin" && currentCatIncluded) {
110
- if (matchedPluginIds.has(item.id)) {
111
- const matched = fuzzyResults.find((r) => r.item.id === item.id);
112
- result.push({ ...item, matches: matched?.matches });
113
- }
114
- }
115
- }
116
- return result;
117
- }, [allItems, pluginsState.searchQuery]);
118
- const selectableItems = useMemo(() => filteredItems, [filteredItems]);
119
- // Keyboard handling
120
- useKeyboard((event) => {
121
- if (state.modal)
122
- return;
123
- const hasQuery = pluginsState.searchQuery.length > 0;
124
- if (event.name === "escape") {
125
- if (hasQuery || isSearchActive) {
126
- dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
127
- dispatch({ type: "SET_SEARCHING", isSearching: false });
128
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
129
- }
130
- return;
131
- }
132
- if (event.name === "backspace" || event.name === "delete") {
133
- if (hasQuery) {
134
- const newQuery = pluginsState.searchQuery.slice(0, -1);
135
- dispatch({ type: "PLUGINS_SET_SEARCH", query: newQuery });
136
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
137
- if (newQuery.length === 0) {
138
- dispatch({ type: "SET_SEARCHING", isSearching: false });
139
- }
140
- }
141
- return;
142
- }
143
- if (event.name === "up" || event.name === "k") {
144
- if (isSearchActive)
145
- dispatch({ type: "SET_SEARCHING", isSearching: false });
146
- dispatch({
147
- type: "PLUGINS_SELECT",
148
- index: Math.max(0, pluginsState.selectedIndex - 1),
149
- });
150
- return;
151
- }
152
- if (event.name === "down" || event.name === "j") {
153
- if (isSearchActive)
154
- dispatch({ type: "SET_SEARCHING", isSearching: false });
155
- dispatch({
156
- type: "PLUGINS_SELECT",
157
- index: Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1),
158
- });
159
- return;
160
- }
161
- if (event.name === "enter" || event.name === "return") {
162
- if (isSearchActive) {
163
- dispatch({ type: "SET_SEARCHING", isSearching: false });
164
- return;
165
- }
166
- handleSelect();
167
- return;
168
- }
169
- if ((event.name === "left" ||
170
- event.name === "right" ||
171
- event.name === "<" ||
172
- event.name === ">") &&
173
- selectableItems[pluginsState.selectedIndex]?.kind === "category") {
174
- const item = selectableItems[pluginsState.selectedIndex];
175
- if (item?.kind === "category") {
176
- dispatch({
177
- type: "PLUGINS_TOGGLE_MARKETPLACE",
178
- name: item.marketplace.name,
179
- });
180
- }
181
- return;
182
- }
183
- if (isSearchActive) {
184
- if (event.name.length === 1 &&
185
- !event.ctrl &&
186
- !event.meta &&
187
- !/[0-9]/.test(event.name)) {
188
- dispatch({
189
- type: "PLUGINS_SET_SEARCH",
190
- query: pluginsState.searchQuery + event.name,
191
- });
192
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
193
- }
194
- return;
195
- }
196
- if (event.name === "/") {
197
- dispatch({ type: "SET_SEARCHING", isSearching: true });
198
- return;
199
- }
200
- if (event.name === "r")
201
- handleRefresh();
202
- else if (event.name === "n")
203
- handleShowAddMarketplaceInstructions();
204
- else if (event.name === "t")
205
- handleShowTeamConfigHelp();
206
- else if (event.name === "u")
207
- handleScopeToggle("user");
208
- else if (event.name === "p")
209
- handleScopeToggle("project");
210
- else if (event.name === "l")
211
- handleScopeToggle("local");
212
- else if (event.name === "d")
213
- handleRemoveDeprecated();
214
- else if (event.name === "U")
215
- handleUpdate();
216
- else if (event.name === "a")
217
- handleUpdateAll();
218
- else if (event.name === "m")
219
- handleCheckMismatches();
220
- else if (event.name === "s")
221
- handleSaveAsProfile();
222
- });
223
- // ── Helpers ───────────────────────────────────────────────────────────────
224
- /**
225
- * Save the installed plugin version to the correct settings file for the scope.
226
- * The Claude CLI's `plugin install` does NOT update installedPluginVersions,
227
- * so claudeup must do it after a successful CLI install/update.
228
- */
229
- const saveVersionForScope = async (pluginId, version, scope) => {
230
- if (scope === "user") {
231
- await saveGlobalInstalledPluginVersion(pluginId, version);
232
- }
233
- else if (scope === "local") {
234
- await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
235
- }
236
- else {
237
- await saveInstalledPluginVersion(pluginId, version, state.projectPath);
238
- }
239
- };
240
- /**
241
- * Check for version mismatch after a plugin update and show the
242
- * interactive mismatch modal with a "Fix all projects" button.
243
- *
244
- * The [0] index bug in Claude Code means the update may not actually
245
- * take effect if another project's entry is at index 0. The shared
246
- * useMismatchModal hook provides the same fix flow available at startup.
247
- */
248
- const warnIfVersionMismatch = async (pluginId) => {
249
- try {
250
- const projectPath = state.projectPath || process.cwd();
251
- const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
252
- if (mismatch) {
253
- mismatchModal.show([mismatch]);
254
- }
255
- }
256
- catch {
257
- // Non-fatal: mismatch check is best-effort
258
- }
259
- };
260
- // ── Action handlers ────────────────────────────────────────────────────────
261
- const handleRefresh = async () => {
262
- progress.show("Refreshing cache...");
263
- try {
264
- const result = await refreshAllMarketplaces((p) => {
265
- progress.show(`${p.name}`, p.current, p.total);
266
- });
267
- clearMarketplaceCache();
268
- progress.hide();
269
- let message = "Cache refreshed.\n\n" +
270
- "To update marketplaces from GitHub, run in Claude Code:\n" +
271
- " /plugin marketplace update\n\n" +
272
- "Then refresh claudeup again with 'r'.";
273
- if (result.repair.length > 0) {
274
- const totalRepaired = result.repair.reduce((sum, r) => sum + r.repaired.length, 0);
275
- message += `\n\nAuto-repaired ${totalRepaired} plugin(s) with missing agents/commands.`;
276
- }
277
- await modal.message("Refreshed", message, "success");
278
- fetchData();
279
- }
280
- catch (error) {
281
- progress.hide();
282
- await modal.message("Error", `Refresh failed: ${error}`, "error");
283
- }
284
- };
285
- const handleShowAddMarketplaceInstructions = async () => {
286
- await modal.message("Add Marketplace", "To add a marketplace, run this command in your terminal:\n\n" +
287
- " claude plugin marketplace add owner/repo\n\n" +
288
- "Examples:\n" +
289
- " claude plugin marketplace add MadAppGang/magus\n" +
290
- " claude plugin marketplace add anthropics/claude-plugins-official\n\n" +
291
- "Auto-update is enabled by default for new marketplaces.\n\n" +
292
- "After adding, refresh claudeup with 'r' to see the new marketplace.", "info");
293
- };
294
- const handleShowTeamConfigHelp = async () => {
295
- const helpText = "TEAM CONFIGURATION FOR MARKETPLACES\n\n" +
296
- "To configure marketplaces for your entire team:\n\n" +
297
- "1. Add to .claude/settings.json (committed to git):\n\n" +
298
- " {\n" +
299
- ' "extraKnownMarketplaces": {\n' +
300
- ' "company-tools": {\n' +
301
- ' "source": {\n' +
302
- ' "source": "github",\n' +
303
- ' "repo": "your-org/claude-plugins"\n' +
304
- " }\n" +
305
- " }\n" +
306
- " },\n" +
307
- ' "enabledPlugins": {\n' +
308
- ' "code-formatter@company-tools": true\n' +
309
- " }\n" +
310
- " }\n\n" +
311
- "2. Team members get prompted to install when they trust the folder\n\n" +
312
- "3. Marketplaces are GLOBAL ONLY (managed by Claude Code)\n" +
313
- " - Not project-specific\n" +
314
- " - All team members share the same marketplace cache\n\n" +
315
- "NOTE: Individual plugin enablement is still project-specific.\n" +
316
- 'Use "Project" scope in claudeup to enable for the team.';
317
- await modal.message("Team Configuration", helpText, "info");
318
- };
319
- /**
320
- * Collect environment variables required by a plugin's MCP servers.
321
- */
322
- const collectPluginEnvVars = async (pluginName, marketplace) => {
323
- try {
324
- const pluginSource = await getPluginSourcePath(marketplace, pluginName);
325
- if (!pluginSource)
326
- return true;
327
- const requirements = await getPluginEnvRequirements(marketplace, pluginSource);
328
- if (requirements.length === 0)
329
- return true;
330
- const existingEnvVars = await getMcpEnvVars(state.projectPath);
331
- const missingVars = requirements.filter((req) => !existingEnvVars[req.name] && !process.env[req.name]);
332
- if (missingVars.length === 0)
333
- return true;
334
- const serverNames = [...new Set(missingVars.map((v) => v.serverName))];
335
- const wantToConfigure = await modal.confirm("Configure MCP Servers?", `This plugin includes MCP servers (${serverNames.join(", ")}) that need ${missingVars.length} environment variable(s).\n\nConfigure now?`);
336
- if (!wantToConfigure) {
337
- await modal.message("Skipped Configuration", "You can configure these variables later in the Environment Variables screen (press 4).", "info");
338
- return true;
339
- }
340
- for (const req of missingVars) {
341
- const existingProcessEnv = process.env[req.name];
342
- if (existingProcessEnv) {
343
- const useExisting = await modal.confirm(`Use ${req.name}?`, `${req.name} is already set in your environment.\n\nUse the existing value?`);
344
- if (useExisting) {
345
- await setMcpEnvVar(req.name, `\${${req.name}}`, state.projectPath);
346
- continue;
347
- }
348
- }
349
- const value = await modal.input(`Configure ${req.serverName}`, `${req.label} (required):`, "");
350
- if (value === null) {
351
- await modal.message("Configuration Incomplete", `Skipped remaining configuration.\nYou can configure these later in Environment Variables (press 4).`, "info");
352
- return true;
353
- }
354
- if (value) {
355
- await setMcpEnvVar(req.name, value, state.projectPath);
356
- }
357
- }
358
- return true;
359
- }
360
- catch (error) {
361
- console.error("Error collecting plugin env vars:", error);
362
- return true;
363
- }
364
- };
365
- /**
366
- * Install system dependencies required by a plugin's MCP servers.
367
- */
368
- const installPluginSystemDeps = async (pluginName, marketplace) => {
369
- try {
370
- const setup = await getPluginSetupFromSource(marketplace, pluginName);
371
- if (!setup)
372
- return;
373
- const missing = await checkMissingDeps(setup);
374
- const hasMissing = (missing.pip?.length || 0) +
375
- (missing.brew?.length || 0) +
376
- (missing.npm?.length || 0) +
377
- (missing.cargo?.length || 0) >
378
- 0;
379
- if (!hasMissing)
380
- return;
381
- const parts = [];
382
- if (missing.pip?.length)
383
- parts.push(`pip: ${missing.pip.join(", ")}`);
384
- if (missing.brew?.length)
385
- parts.push(`brew: ${missing.brew.join(", ")}`);
386
- if (missing.npm?.length)
387
- parts.push(`npm: ${missing.npm.join(", ")}`);
388
- if (missing.cargo?.length)
389
- parts.push(`cargo: ${missing.cargo.join(", ")}`);
390
- const wantInstall = await modal.confirm("Install Dependencies?", `This plugin needs system dependencies:\n\n${parts.join("\n")}\n\nInstall now?`);
391
- if (!wantInstall)
392
- return;
393
- modal.loading("Installing dependencies...");
394
- const result = await installPluginDeps(missing);
395
- modal.hideModal();
396
- if (result.failed.length > 0) {
397
- const failMsg = result.failed
398
- .map((f) => `${f.pkg}: ${f.error}`)
399
- .join("\n");
400
- await modal.message("Partial Install", `Installed: ${result.installed.length}\nFailed:\n${failMsg}`, "error");
401
- }
402
- else if (result.installed.length > 0) {
403
- await modal.message("Dependencies Installed", `Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`, "success");
404
- }
405
- }
406
- catch (error) {
407
- console.error("Error installing plugin deps:", error);
408
- }
409
- };
410
- const handleSelect = async () => {
411
- const item = selectableItems[pluginsState.selectedIndex];
412
- if (!item)
413
- return;
414
- if (item.kind === "category") {
415
- const mp = item.marketplace;
416
- if (item.marketplaceEnabled) {
417
- const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
418
- if (isCollapsed) {
419
- dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
420
- }
421
- else if (item.pluginCount > 0) {
422
- dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
423
- }
424
- else {
425
- await modal.message(`Remove ${mp.displayName}?`, `To remove this marketplace, run in Claude Code:\n\n` +
426
- ` /plugin marketplace remove ${mp.name}\n\n` +
427
- `After removing, refresh claudeup with 'r' to update the display.`, "info");
428
- }
429
- }
430
- else {
431
- await modal.message(`Add ${mp.displayName}?`, `To add this marketplace, run in your terminal:\n\n` +
432
- ` claude plugin marketplace add ${mp.source.repo || mp.name}\n\n` +
433
- `Auto-update is enabled by default.\n\n` +
434
- `After adding, refresh claudeup with 'r' to see it.`, "info");
435
- }
436
- }
437
- else if (item.kind === "plugin") {
438
- const plugin = item.plugin;
439
- const latestVersion = plugin.version || "0.0.0";
440
- const buildScopeLabel = (name, scope, desc) => {
441
- const installed = scope?.enabled;
442
- const ver = scope?.version;
443
- const hasUpdate = ver &&
444
- latestVersion &&
445
- ver !== latestVersion &&
446
- latestVersion !== "0.0.0";
447
- let label = installed ? `● ${name}` : `○ ${name}`;
448
- label += ` (${desc})`;
449
- if (ver)
450
- label += ` v${ver}`;
451
- if (hasUpdate)
452
- label += ` → v${latestVersion}`;
453
- return label;
454
- };
455
- const scopeOptions = [
456
- {
457
- label: buildScopeLabel("User", plugin.userScope, "global"),
458
- value: "user",
459
- },
460
- {
461
- label: buildScopeLabel("Project", plugin.projectScope, "team"),
462
- value: "project",
463
- },
464
- {
465
- label: buildScopeLabel("Local", plugin.localScope, "private"),
466
- value: "local",
467
- },
468
- ];
469
- const scopeValue = await modal.select(plugin.name, `Select scope to toggle:`, scopeOptions);
470
- if (scopeValue === null)
471
- return;
472
- const selectedScope = scopeValue === "user"
473
- ? plugin.userScope
474
- : scopeValue === "project"
475
- ? plugin.projectScope
476
- : plugin.localScope;
477
- const isInstalledInScope = selectedScope?.enabled;
478
- const installedVersion = selectedScope?.version;
479
- const scopeLabel = scopeValue === "user"
480
- ? "User"
481
- : scopeValue === "project"
482
- ? "Project"
483
- : "Local";
484
- const hasUpdateInScope = isInstalledInScope &&
485
- installedVersion &&
486
- latestVersion !== "0.0.0" &&
487
- installedVersion !== latestVersion;
488
- let action;
489
- if (isInstalledInScope && hasUpdateInScope) {
490
- action = "update";
491
- }
492
- else if (isInstalledInScope) {
493
- action = "uninstall";
494
- }
495
- else {
496
- action = "install";
497
- }
498
- try {
499
- const scope = scopeValue;
500
- if (action === "uninstall") {
501
- modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
502
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
503
- }
504
- else if (action === "update") {
505
- modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
506
- await cliUpdatePlugin(plugin.id, scope);
507
- await saveVersionForScope(plugin.id, latestVersion, scope);
508
- }
509
- else {
510
- modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
511
- await cliInstallPlugin(plugin.id, scope);
512
- await saveVersionForScope(plugin.id, latestVersion, scope);
513
- modal.hideModal();
514
- await collectPluginEnvVars(plugin.name, plugin.marketplace);
515
- await installPluginSystemDeps(plugin.name, plugin.marketplace);
516
- }
517
- if (action !== "install") {
518
- modal.hideModal();
519
- }
520
- if (action === "update") {
521
- await warnIfVersionMismatch(plugin.id);
522
- }
523
- fetchData();
524
- }
525
- catch (error) {
526
- modal.hideModal();
527
- await modal.message("Error", `Failed: ${error}`, "error");
528
- }
529
- }
530
- };
531
- const handleUpdate = async () => {
532
- const item = selectableItems[pluginsState.selectedIndex];
533
- if (!item || item.kind !== "plugin" || !item.plugin.hasUpdate)
534
- return;
535
- const plugin = item.plugin;
536
- const scope = pluginsState.scope === "global" ? "user" : "project";
537
- modal.loading(`Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
538
- try {
539
- await cliUpdatePlugin(plugin.id, scope);
540
- if (plugin.version) {
541
- await saveVersionForScope(plugin.id, plugin.version, scope);
542
- }
543
- modal.hideModal();
544
- await warnIfVersionMismatch(plugin.id);
545
- fetchData();
546
- }
547
- catch (error) {
548
- modal.hideModal();
549
- await modal.message("Error", `Failed to update: ${error}`, "error");
550
- }
551
- };
552
- const handleUpdateAll = async () => {
553
- if (pluginsState.plugins.status !== "success")
554
- return;
555
- const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
556
- if (updatable.length === 0)
557
- return;
558
- const scope = pluginsState.scope === "global" ? "user" : "project";
559
- const updatedPluginIds = [];
560
- try {
561
- for (let i = 0; i < updatable.length; i++) {
562
- const plugin = updatable[i];
563
- modal.loading(`Updating ${plugin.name} (${i + 1}/${updatable.length})…\nclaude plugin install ${plugin.id} --scope ${scope}`);
564
- await cliUpdatePlugin(plugin.id, scope);
565
- if (plugin.version) {
566
- await saveVersionForScope(plugin.id, plugin.version, scope);
567
- }
568
- updatedPluginIds.push(plugin.id);
569
- }
570
- modal.hideModal();
571
- // Batch mismatch checks into a single modal
572
- const projectPath = state.projectPath || process.cwd();
573
- const allMismatches = [];
574
- for (const pluginId of updatedPluginIds) {
575
- try {
576
- const m = await checkSinglePluginMismatch(pluginId, projectPath);
577
- if (m)
578
- allMismatches.push(m);
579
- }
580
- catch {
581
- // best-effort
582
- }
583
- }
584
- if (allMismatches.length > 0) {
585
- mismatchModal.show(allMismatches);
586
- }
587
- fetchData();
588
- }
589
- catch (error) {
590
- modal.hideModal();
591
- await modal.message("Error", `Failed to update: ${error}`, "error");
592
- }
593
- };
594
- /**
595
- * Manual on-demand check for plugin version mismatches across projects.
596
- * Bound to the `m` keybind. Shows a success message if everything is
597
- * aligned, or the interactive fix modal if any mismatches are found.
598
- */
599
- const handleCheckMismatches = async () => {
600
- try {
601
- const projectPath = state.projectPath || process.cwd();
602
- const mismatches = await checkPluginVersionMismatches(projectPath);
603
- if (mismatches.length === 0) {
604
- await modal.message("No Mismatches", "All enabled plugins in this project will load their expected versions. No action needed.", "success");
605
- return;
606
- }
607
- mismatchModal.show(mismatches);
608
- }
609
- catch (error) {
610
- await modal.message("Error", `Failed to check mismatches: ${error}`, "error");
611
- }
612
- };
613
- const handleScopeToggle = async (scope) => {
614
- const item = selectableItems[pluginsState.selectedIndex];
615
- if (!item || item.kind !== "plugin")
616
- return;
617
- const plugin = item.plugin;
618
- const latestVersion = plugin.version || "0.0.0";
619
- const scopeLabel = scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
620
- const scopeData = scope === "user"
621
- ? plugin.userScope
622
- : scope === "project"
623
- ? plugin.projectScope
624
- : plugin.localScope;
625
- const isInstalledInScope = scopeData?.enabled;
626
- const installedVersion = scopeData?.version;
627
- const hasUpdateInScope = isInstalledInScope &&
628
- installedVersion &&
629
- latestVersion !== "0.0.0" &&
630
- installedVersion !== latestVersion;
631
- let action;
632
- if (isInstalledInScope && hasUpdateInScope) {
633
- action = "update";
634
- }
635
- else if (isInstalledInScope) {
636
- action = "uninstall";
637
- }
638
- else {
639
- action = "install";
640
- }
641
- try {
642
- if (action === "uninstall") {
643
- modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
644
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
645
- }
646
- else if (action === "update") {
647
- modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
648
- await cliUpdatePlugin(plugin.id, scope);
649
- await saveVersionForScope(plugin.id, latestVersion, scope);
650
- }
651
- else {
652
- modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
653
- await cliInstallPlugin(plugin.id, scope);
654
- await saveVersionForScope(plugin.id, latestVersion, scope);
655
- modal.hideModal();
656
- await collectPluginEnvVars(plugin.name, plugin.marketplace);
657
- await installPluginSystemDeps(plugin.name, plugin.marketplace);
658
- }
659
- if (action !== "install") {
660
- modal.hideModal();
661
- }
662
- if (action === "update") {
663
- await warnIfVersionMismatch(plugin.id);
664
- }
665
- fetchData();
666
- }
667
- catch (error) {
668
- modal.hideModal();
669
- await modal.message("Error", `Failed: ${error}`, "error");
670
- }
671
- };
672
- const handleRemoveDeprecated = async () => {
673
- const item = selectableItems[pluginsState.selectedIndex];
674
- if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned)
675
- return;
676
- const plugin = item.plugin;
677
- try {
678
- // Remove from all scopes — try all to clean up stale references
679
- const scopes = ["user", "project", "local"];
680
- for (const scope of scopes) {
681
- try {
682
- modal.loading(`Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
683
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
684
- }
685
- catch {
686
- // Ignore errors for scopes where it doesn't exist
687
- }
688
- }
689
- modal.hideModal();
690
- fetchData();
691
- }
692
- catch (error) {
693
- modal.hideModal();
694
- await modal.message("Error", `Failed to remove: ${error}`, "error");
695
- }
696
- };
697
- const handleSaveAsProfile = async () => {
698
- const settings = await readSettings(state.projectPath);
699
- const enabledPlugins = settings.enabledPlugins ?? {};
700
- const name = await modal.input("Save Profile", "Profile name:");
701
- if (name === null || !name.trim())
702
- return;
703
- const scopeChoice = await modal.select("Save to scope", "Where should this profile be saved?", [
704
- {
705
- label: "User — ~/.claude/profiles.json (available everywhere)",
706
- value: "user",
707
- },
708
- {
709
- label: "Project — .claude/profiles.json (shared with team via git)",
710
- value: "project",
711
- },
712
- ]);
713
- if (scopeChoice === null)
714
- return;
715
- const scope = scopeChoice;
716
- modal.loading("Saving profile...");
717
- try {
718
- await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
719
- modal.hideModal();
720
- await modal.message("Saved", `Profile "${name.trim()}" saved.\nPress 6 to manage profiles.`, "success");
721
- }
722
- catch (error) {
723
- modal.hideModal();
724
- await modal.message("Error", `Failed to save profile: ${error}`, "error");
725
- }
726
- };
727
- // ── Render ─────────────────────────────────────────────────────────────────
728
- if (pluginsState.marketplaces.status === "loading" ||
729
- pluginsState.plugins.status === "loading") {
730
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { fg: "#7e57c2", children: _jsx("strong", { children: "claudeup Plugins" }) }), _jsx("text", { fg: "gray", children: "Loading..." })] }));
731
- }
732
- if (pluginsState.marketplaces.status === "error" ||
733
- pluginsState.plugins.status === "error") {
734
- return (_jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx("text", { fg: "#7e57c2", children: _jsx("strong", { children: "claudeup Plugins" }) }), _jsx("text", { fg: "red", children: "Error loading data" })] }));
735
- }
736
- const selectedItem = selectableItems[pluginsState.selectedIndex];
737
- const footerHints = isSearchActive
738
- ? [
739
- { keys: ["type"], label: "filter" },
740
- { keys: ["Enter"], label: "done" },
741
- { keys: ["Esc"], label: "clear" },
742
- ]
743
- : [
744
- { keys: ["u", "/", "p", "/", "l"], label: "toggle" },
745
- { keys: ["U"], label: "update" },
746
- { keys: ["a"], label: "all" },
747
- { keys: ["m"], label: "mismatches" },
748
- { keys: ["s"], label: "profile" },
749
- { keys: ["/"], label: "search" },
750
- ];
751
- const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
752
- const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
753
- const installedCount = plugins.filter((p) => p.enabled).length;
754
- const updateCount = plugins.filter((p) => p.hasUpdate &&
755
- (p.userScope?.enabled ||
756
- p.projectScope?.enabled ||
757
- p.localScope?.enabled)).length;
758
- const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
759
- const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
760
- return (_jsx(ScreenLayout, { title: "claudeup Plugins", subtitle: subtitle, currentScreen: "plugins", search: {
761
- isActive: isSearchActive,
762
- query: pluginsState.searchQuery,
763
- placeholder: searchPlaceholder,
764
- }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderPluginRow, maxHeight: dimensions.listPanelHeight, getKey: (item) => item.id }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderPluginDetail(selectedItem, pluginsState.collapsedMarketplaces) }));
765
- }
766
- export default PluginsScreen;