claudeup 4.19.0 → 4.23.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 (182) 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__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-badge.test.ts +91 -0
  18. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  19. package/src/__tests__/plugin-requires.test.ts +133 -0
  20. package/src/__tests__/plugin-setup.test.ts +7 -0
  21. package/src/__tests__/profile-command.test.ts +116 -0
  22. package/src/__tests__/profile-materializer.test.ts +82 -0
  23. package/src/__tests__/profile-sync.test.ts +136 -0
  24. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  25. package/src/__tests__/resolve-executable.test.ts +42 -0
  26. package/src/__tests__/resolver.test.ts +218 -0
  27. package/src/__tests__/symlink-manager.test.ts +99 -0
  28. package/src/__tests__/toolchain.test.ts +56 -0
  29. package/src/cli/claude.ts +21 -0
  30. package/src/cli/doctor.ts +132 -0
  31. package/src/cli/install.ts +360 -0
  32. package/src/cli/profile.ts +166 -0
  33. package/src/cli/router.ts +107 -0
  34. package/src/cli/update.ts +85 -0
  35. package/src/data/cli-tools.ts +7 -7
  36. package/src/data/gitignore-defaults.ts +4 -0
  37. package/src/data/marketplaces.ts +12 -0
  38. package/src/data/predefined-profiles.ts +3 -4
  39. package/src/main.tsx +11 -154
  40. package/src/prerunner/index.ts +62 -27
  41. package/src/services/claude-cli.ts +19 -22
  42. package/src/services/claude-settings.ts +99 -26
  43. package/src/services/conventions-manager.ts +865 -0
  44. package/src/services/doctor-bins.ts +49 -0
  45. package/src/services/doctor.ts +509 -0
  46. package/src/services/install-plan.ts +107 -0
  47. package/src/services/manifest.ts +166 -0
  48. package/src/services/marketplace-refresh.ts +173 -0
  49. package/src/services/plugin-manager.ts +7 -1
  50. package/src/services/plugin-requires.ts +215 -0
  51. package/src/services/plugin-version-check.ts +9 -4
  52. package/src/services/profile-materializer.ts +61 -0
  53. package/src/services/profile-sync.ts +150 -0
  54. package/src/services/resolver.ts +293 -0
  55. package/src/services/symlink-manager.ts +146 -0
  56. package/src/services/toolchain.ts +97 -0
  57. package/src/services/version-check.ts +10 -11
  58. package/src/types/index.ts +138 -31
  59. package/src/ui/App.tsx +0 -4
  60. package/src/ui/adapters/pluginsAdapter.ts +13 -0
  61. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  62. package/src/ui/screens/index.ts +0 -1
  63. package/src/ui/state/reducer.ts +0 -101
  64. package/src/ui/state/types.ts +0 -35
  65. package/src/utils/command-utils.ts +23 -0
  66. package/src/utils/file-locking.ts +144 -0
  67. package/src/data/alias-flags.js +0 -205
  68. package/src/data/cli-tools.js +0 -123
  69. package/src/data/gitignore-defaults.js +0 -24
  70. package/src/data/gitignore-reasons.js +0 -97
  71. package/src/data/gitignore-templates.js +0 -21
  72. package/src/data/marketplaces.js +0 -138
  73. package/src/data/mcp-servers.js +0 -509
  74. package/src/data/predefined-profiles.js +0 -248
  75. package/src/data/settings-catalog.js +0 -625
  76. package/src/data/skill-repos.js +0 -160
  77. package/src/data/statuslines.js +0 -159
  78. package/src/data/statuslines.ts +0 -188
  79. package/src/index.js +0 -4
  80. package/src/index.ts +0 -5
  81. package/src/main.js +0 -170
  82. package/src/prerunner/index.js +0 -252
  83. package/src/services/alias-settings.js +0 -51
  84. package/src/services/alias-shell-writer.js +0 -1018
  85. package/src/services/alias-store.js +0 -129
  86. package/src/services/claude-cli.js +0 -189
  87. package/src/services/claude-runner.js +0 -28
  88. package/src/services/claude-settings.js +0 -1223
  89. package/src/services/gitignore-detector.js +0 -155
  90. package/src/services/gitignore-fixer.js +0 -231
  91. package/src/services/gitignore-prerun.js +0 -46
  92. package/src/services/gitignore-resolver.js +0 -143
  93. package/src/services/gitignore-service.js +0 -117
  94. package/src/services/local-marketplace.js +0 -339
  95. package/src/services/marketplace-fetcher.js +0 -96
  96. package/src/services/marketplace-sync.js +0 -94
  97. package/src/services/mcp-registry.js +0 -87
  98. package/src/services/plugin-manager.js +0 -473
  99. package/src/services/plugin-mcp-config.js +0 -177
  100. package/src/services/plugin-setup.js +0 -499
  101. package/src/services/plugin-version-check.js +0 -262
  102. package/src/services/profiles.js +0 -161
  103. package/src/services/settings-manager.js +0 -243
  104. package/src/services/skills-manager.js +0 -393
  105. package/src/services/skillsmp-client.js +0 -87
  106. package/src/services/update-cache.js +0 -52
  107. package/src/services/version-check.js +0 -99
  108. package/src/types/gitignore.js +0 -6
  109. package/src/types/index.js +0 -1
  110. package/src/ui/App.js +0 -355
  111. package/src/ui/adapters/pluginsAdapter.js +0 -139
  112. package/src/ui/adapters/settingsAdapter.js +0 -111
  113. package/src/ui/adapters/skillsAdapter.js +0 -154
  114. package/src/ui/components/CategoryHeader.js +0 -9
  115. package/src/ui/components/EmptyFilterState.js +0 -4
  116. package/src/ui/components/FlagDetailEditor.js +0 -0
  117. package/src/ui/components/ScopeIndicator.js +0 -30
  118. package/src/ui/components/ScrollableList.js +0 -36
  119. package/src/ui/components/SearchInput.js +0 -19
  120. package/src/ui/components/StyledText.js +0 -39
  121. package/src/ui/components/TabBar.js +0 -19
  122. package/src/ui/components/layout/FooterHints.js +0 -29
  123. package/src/ui/components/layout/Panel.js +0 -6
  124. package/src/ui/components/layout/ProgressBar.js +0 -14
  125. package/src/ui/components/layout/ScopeTabs.js +0 -6
  126. package/src/ui/components/layout/ScreenLayout.js +0 -16
  127. package/src/ui/components/layout/index.js +0 -5
  128. package/src/ui/components/modals/ConfirmModal.js +0 -14
  129. package/src/ui/components/modals/InputModal.js +0 -5
  130. package/src/ui/components/modals/LoadingModal.js +0 -17
  131. package/src/ui/components/modals/MessageModal.js +0 -16
  132. package/src/ui/components/modals/ModalContainer.js +0 -137
  133. package/src/ui/components/modals/SelectModal.js +0 -18
  134. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  135. package/src/ui/components/modals/index.js +0 -6
  136. package/src/ui/components/primitives/ActionHints.js +0 -13
  137. package/src/ui/components/primitives/DetailSection.js +0 -7
  138. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  139. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  140. package/src/ui/components/primitives/MetaText.js +0 -8
  141. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  142. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  143. package/src/ui/components/primitives/SelectableRow.js +0 -5
  144. package/src/ui/components/primitives/index.js +0 -8
  145. package/src/ui/hooks/index.js +0 -4
  146. package/src/ui/hooks/useAsyncData.js +0 -77
  147. package/src/ui/hooks/useGitignoreModal.js +0 -74
  148. package/src/ui/hooks/useKeyboard.js +0 -13
  149. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  150. package/src/ui/hooks/useMismatchModal.js +0 -77
  151. package/src/ui/registry.js +0 -1
  152. package/src/ui/renderers/cliToolRenderers.js +0 -54
  153. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  154. package/src/ui/renderers/mcpRenderers.js +0 -26
  155. package/src/ui/renderers/pluginRenderers.js +0 -124
  156. package/src/ui/renderers/profileRenderers.js +0 -177
  157. package/src/ui/renderers/settingsRenderers.js +0 -73
  158. package/src/ui/renderers/skillRenderers.js +0 -138
  159. package/src/ui/screens/AliasScreen.js +0 -1111
  160. package/src/ui/screens/CliToolsScreen.js +0 -338
  161. package/src/ui/screens/EnvVarsScreen.js +0 -152
  162. package/src/ui/screens/GitignoreScreen.js +0 -328
  163. package/src/ui/screens/McpRegistryScreen.js +0 -238
  164. package/src/ui/screens/McpScreen.js +0 -176
  165. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  166. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  167. package/src/ui/screens/PluginsScreen.js +0 -769
  168. package/src/ui/screens/ProfilesScreen.js +0 -298
  169. package/src/ui/screens/SkillsScreen.js +0 -549
  170. package/src/ui/screens/StatusLineScreen.js +0 -206
  171. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  172. package/src/ui/screens/index.js +0 -10
  173. package/src/ui/state/AnimationContext.js +0 -34
  174. package/src/ui/state/AppContext.js +0 -162
  175. package/src/ui/state/DimensionsContext.js +0 -71
  176. package/src/ui/state/reducer.js +0 -547
  177. package/src/ui/state/types.js +0 -1
  178. package/src/ui/theme.js +0 -47
  179. package/src/utils/clipboard.js +0 -56
  180. package/src/utils/command-utils.js +0 -19
  181. package/src/utils/fuzzy-search.js +0 -101
  182. package/src/utils/string-utils.js +0 -62
@@ -1,769 +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
- (missing.go?.length || 0) >
379
- 0;
380
- if (!hasMissing)
381
- return;
382
- const parts = [];
383
- if (missing.pip?.length)
384
- parts.push(`pip: ${missing.pip.join(", ")}`);
385
- if (missing.brew?.length)
386
- parts.push(`brew: ${missing.brew.join(", ")}`);
387
- if (missing.npm?.length)
388
- parts.push(`npm: ${missing.npm.join(", ")}`);
389
- if (missing.cargo?.length)
390
- parts.push(`cargo: ${missing.cargo.join(", ")}`);
391
- if (missing.go?.length)
392
- parts.push(`go: ${missing.go.join(", ")}`);
393
- const wantInstall = await modal.confirm("Install Dependencies?", `This plugin needs system dependencies:\n\n${parts.join("\n")}\n\nInstall now?`);
394
- if (!wantInstall)
395
- return;
396
- modal.loading("Installing dependencies...");
397
- const result = await installPluginDeps(missing);
398
- modal.hideModal();
399
- if (result.failed.length > 0) {
400
- const failMsg = result.failed
401
- .map((f) => `${f.pkg}: ${f.error}`)
402
- .join("\n");
403
- await modal.message("Partial Install", `Installed: ${result.installed.length}\nFailed:\n${failMsg}`, "error");
404
- }
405
- else if (result.installed.length > 0) {
406
- await modal.message("Dependencies Installed", `Installed ${result.installed.length} package(s):\n${result.installed.join(", ")}`, "success");
407
- }
408
- }
409
- catch (error) {
410
- console.error("Error installing plugin deps:", error);
411
- }
412
- };
413
- const handleSelect = async () => {
414
- const item = selectableItems[pluginsState.selectedIndex];
415
- if (!item)
416
- return;
417
- if (item.kind === "category") {
418
- const mp = item.marketplace;
419
- if (item.marketplaceEnabled) {
420
- const isCollapsed = pluginsState.collapsedMarketplaces.has(mp.name);
421
- if (isCollapsed) {
422
- dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
423
- }
424
- else if (item.pluginCount > 0) {
425
- dispatch({ type: "PLUGINS_TOGGLE_MARKETPLACE", name: mp.name });
426
- }
427
- else {
428
- await modal.message(`Remove ${mp.displayName}?`, `To remove this marketplace, run in Claude Code:\n\n` +
429
- ` /plugin marketplace remove ${mp.name}\n\n` +
430
- `After removing, refresh claudeup with 'r' to update the display.`, "info");
431
- }
432
- }
433
- else {
434
- await modal.message(`Add ${mp.displayName}?`, `To add this marketplace, run in your terminal:\n\n` +
435
- ` claude plugin marketplace add ${mp.source.repo || mp.name}\n\n` +
436
- `Auto-update is enabled by default.\n\n` +
437
- `After adding, refresh claudeup with 'r' to see it.`, "info");
438
- }
439
- }
440
- else if (item.kind === "plugin") {
441
- const plugin = item.plugin;
442
- const latestVersion = plugin.version || "0.0.0";
443
- const buildScopeLabel = (name, scope, desc) => {
444
- const installed = scope?.enabled;
445
- const ver = scope?.version;
446
- const hasUpdate = ver &&
447
- latestVersion &&
448
- ver !== latestVersion &&
449
- latestVersion !== "0.0.0";
450
- let label = installed ? `● ${name}` : `○ ${name}`;
451
- label += ` (${desc})`;
452
- if (ver)
453
- label += ` v${ver}`;
454
- if (hasUpdate)
455
- label += ` → v${latestVersion}`;
456
- return label;
457
- };
458
- const scopeOptions = [
459
- {
460
- label: buildScopeLabel("User", plugin.userScope, "global"),
461
- value: "user",
462
- },
463
- {
464
- label: buildScopeLabel("Project", plugin.projectScope, "team"),
465
- value: "project",
466
- },
467
- {
468
- label: buildScopeLabel("Local", plugin.localScope, "private"),
469
- value: "local",
470
- },
471
- ];
472
- const scopeValue = await modal.select(plugin.name, `Select scope to toggle:`, scopeOptions);
473
- if (scopeValue === null)
474
- return;
475
- const selectedScope = scopeValue === "user"
476
- ? plugin.userScope
477
- : scopeValue === "project"
478
- ? plugin.projectScope
479
- : plugin.localScope;
480
- const isInstalledInScope = selectedScope?.enabled;
481
- const installedVersion = selectedScope?.version;
482
- const scopeLabel = scopeValue === "user"
483
- ? "User"
484
- : scopeValue === "project"
485
- ? "Project"
486
- : "Local";
487
- const hasUpdateInScope = isInstalledInScope &&
488
- installedVersion &&
489
- latestVersion !== "0.0.0" &&
490
- installedVersion !== latestVersion;
491
- let action;
492
- if (isInstalledInScope && hasUpdateInScope) {
493
- action = "update";
494
- }
495
- else if (isInstalledInScope) {
496
- action = "uninstall";
497
- }
498
- else {
499
- action = "install";
500
- }
501
- try {
502
- const scope = scopeValue;
503
- if (action === "uninstall") {
504
- modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
505
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
506
- }
507
- else if (action === "update") {
508
- modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
509
- await cliUpdatePlugin(plugin.id, scope);
510
- await saveVersionForScope(plugin.id, latestVersion, scope);
511
- }
512
- else {
513
- modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
514
- await cliInstallPlugin(plugin.id, scope);
515
- await saveVersionForScope(plugin.id, latestVersion, scope);
516
- modal.hideModal();
517
- await collectPluginEnvVars(plugin.name, plugin.marketplace);
518
- await installPluginSystemDeps(plugin.name, plugin.marketplace);
519
- }
520
- if (action !== "install") {
521
- modal.hideModal();
522
- }
523
- if (action === "update") {
524
- await warnIfVersionMismatch(plugin.id);
525
- }
526
- fetchData();
527
- }
528
- catch (error) {
529
- modal.hideModal();
530
- await modal.message("Error", `Failed: ${error}`, "error");
531
- }
532
- }
533
- };
534
- const handleUpdate = async () => {
535
- const item = selectableItems[pluginsState.selectedIndex];
536
- if (!item || item.kind !== "plugin" || !item.plugin.hasUpdate)
537
- return;
538
- const plugin = item.plugin;
539
- const scope = pluginsState.scope === "global" ? "user" : "project";
540
- modal.loading(`Updating ${plugin.name}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
541
- try {
542
- await cliUpdatePlugin(plugin.id, scope);
543
- if (plugin.version) {
544
- await saveVersionForScope(plugin.id, plugin.version, scope);
545
- }
546
- modal.hideModal();
547
- await warnIfVersionMismatch(plugin.id);
548
- fetchData();
549
- }
550
- catch (error) {
551
- modal.hideModal();
552
- await modal.message("Error", `Failed to update: ${error}`, "error");
553
- }
554
- };
555
- const handleUpdateAll = async () => {
556
- if (pluginsState.plugins.status !== "success")
557
- return;
558
- const updatable = pluginsState.plugins.data.filter((p) => p.hasUpdate);
559
- if (updatable.length === 0)
560
- return;
561
- const scope = pluginsState.scope === "global" ? "user" : "project";
562
- const updatedPluginIds = [];
563
- try {
564
- for (let i = 0; i < updatable.length; i++) {
565
- const plugin = updatable[i];
566
- modal.loading(`Updating ${plugin.name} (${i + 1}/${updatable.length})…\nclaude plugin install ${plugin.id} --scope ${scope}`);
567
- await cliUpdatePlugin(plugin.id, scope);
568
- if (plugin.version) {
569
- await saveVersionForScope(plugin.id, plugin.version, scope);
570
- }
571
- updatedPluginIds.push(plugin.id);
572
- }
573
- modal.hideModal();
574
- // Batch mismatch checks into a single modal
575
- const projectPath = state.projectPath || process.cwd();
576
- const allMismatches = [];
577
- for (const pluginId of updatedPluginIds) {
578
- try {
579
- const m = await checkSinglePluginMismatch(pluginId, projectPath);
580
- if (m)
581
- allMismatches.push(m);
582
- }
583
- catch {
584
- // best-effort
585
- }
586
- }
587
- if (allMismatches.length > 0) {
588
- mismatchModal.show(allMismatches);
589
- }
590
- fetchData();
591
- }
592
- catch (error) {
593
- modal.hideModal();
594
- await modal.message("Error", `Failed to update: ${error}`, "error");
595
- }
596
- };
597
- /**
598
- * Manual on-demand check for plugin version mismatches across projects.
599
- * Bound to the `m` keybind. Shows a success message if everything is
600
- * aligned, or the interactive fix modal if any mismatches are found.
601
- */
602
- const handleCheckMismatches = async () => {
603
- try {
604
- const projectPath = state.projectPath || process.cwd();
605
- const mismatches = await checkPluginVersionMismatches(projectPath);
606
- if (mismatches.length === 0) {
607
- await modal.message("No Mismatches", "All enabled plugins in this project will load their expected versions. No action needed.", "success");
608
- return;
609
- }
610
- mismatchModal.show(mismatches);
611
- }
612
- catch (error) {
613
- await modal.message("Error", `Failed to check mismatches: ${error}`, "error");
614
- }
615
- };
616
- const handleScopeToggle = async (scope) => {
617
- const item = selectableItems[pluginsState.selectedIndex];
618
- if (!item || item.kind !== "plugin")
619
- return;
620
- const plugin = item.plugin;
621
- const latestVersion = plugin.version || "0.0.0";
622
- const scopeLabel = scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
623
- const scopeData = scope === "user"
624
- ? plugin.userScope
625
- : scope === "project"
626
- ? plugin.projectScope
627
- : plugin.localScope;
628
- const isInstalledInScope = scopeData?.enabled;
629
- const installedVersion = scopeData?.version;
630
- const hasUpdateInScope = isInstalledInScope &&
631
- installedVersion &&
632
- latestVersion !== "0.0.0" &&
633
- installedVersion !== latestVersion;
634
- let action;
635
- if (isInstalledInScope && hasUpdateInScope) {
636
- action = "update";
637
- }
638
- else if (isInstalledInScope) {
639
- action = "uninstall";
640
- }
641
- else {
642
- action = "install";
643
- }
644
- try {
645
- if (action === "uninstall") {
646
- modal.loading(`Uninstalling ${plugin.name} from ${scopeLabel}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
647
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
648
- }
649
- else if (action === "update") {
650
- modal.loading(`Updating ${plugin.name} in ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
651
- await cliUpdatePlugin(plugin.id, scope);
652
- await saveVersionForScope(plugin.id, latestVersion, scope);
653
- }
654
- else {
655
- modal.loading(`Installing ${plugin.name} to ${scopeLabel}…\nclaude plugin install ${plugin.id} --scope ${scope}`);
656
- await cliInstallPlugin(plugin.id, scope);
657
- await saveVersionForScope(plugin.id, latestVersion, scope);
658
- modal.hideModal();
659
- await collectPluginEnvVars(plugin.name, plugin.marketplace);
660
- await installPluginSystemDeps(plugin.name, plugin.marketplace);
661
- }
662
- if (action !== "install") {
663
- modal.hideModal();
664
- }
665
- if (action === "update") {
666
- await warnIfVersionMismatch(plugin.id);
667
- }
668
- fetchData();
669
- }
670
- catch (error) {
671
- modal.hideModal();
672
- await modal.message("Error", `Failed: ${error}`, "error");
673
- }
674
- };
675
- const handleRemoveDeprecated = async () => {
676
- const item = selectableItems[pluginsState.selectedIndex];
677
- if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned)
678
- return;
679
- const plugin = item.plugin;
680
- try {
681
- // Remove from all scopes — try all to clean up stale references
682
- const scopes = ["user", "project", "local"];
683
- for (const scope of scopes) {
684
- try {
685
- modal.loading(`Removing ${plugin.name} from ${scope}…\nclaude plugin uninstall ${plugin.id} --scope ${scope}`);
686
- await cliUninstallPlugin(plugin.id, scope, state.projectPath);
687
- }
688
- catch {
689
- // Ignore errors for scopes where it doesn't exist
690
- }
691
- }
692
- modal.hideModal();
693
- fetchData();
694
- }
695
- catch (error) {
696
- modal.hideModal();
697
- await modal.message("Error", `Failed to remove: ${error}`, "error");
698
- }
699
- };
700
- const handleSaveAsProfile = async () => {
701
- const settings = await readSettings(state.projectPath);
702
- const enabledPlugins = settings.enabledPlugins ?? {};
703
- const name = await modal.input("Save Profile", "Profile name:");
704
- if (name === null || !name.trim())
705
- return;
706
- const scopeChoice = await modal.select("Save to scope", "Where should this profile be saved?", [
707
- {
708
- label: "User — ~/.claude/profiles.json (available everywhere)",
709
- value: "user",
710
- },
711
- {
712
- label: "Project — .claude/profiles.json (shared with team via git)",
713
- value: "project",
714
- },
715
- ]);
716
- if (scopeChoice === null)
717
- return;
718
- const scope = scopeChoice;
719
- modal.loading("Saving profile...");
720
- try {
721
- await saveProfile(name.trim(), enabledPlugins, scope, state.projectPath);
722
- modal.hideModal();
723
- await modal.message("Saved", `Profile "${name.trim()}" saved.\nPress 6 to manage profiles.`, "success");
724
- }
725
- catch (error) {
726
- modal.hideModal();
727
- await modal.message("Error", `Failed to save profile: ${error}`, "error");
728
- }
729
- };
730
- // ── Render ─────────────────────────────────────────────────────────────────
731
- if (pluginsState.marketplaces.status === "loading" ||
732
- pluginsState.plugins.status === "loading") {
733
- 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..." })] }));
734
- }
735
- if (pluginsState.marketplaces.status === "error" ||
736
- pluginsState.plugins.status === "error") {
737
- 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" })] }));
738
- }
739
- const selectedItem = selectableItems[pluginsState.selectedIndex];
740
- const footerHints = isSearchActive
741
- ? [
742
- { keys: ["type"], label: "filter" },
743
- { keys: ["Enter"], label: "done" },
744
- { keys: ["Esc"], label: "clear" },
745
- ]
746
- : [
747
- { keys: ["u", "/", "p", "/", "l"], label: "toggle" },
748
- { keys: ["U"], label: "update" },
749
- { keys: ["a"], label: "all" },
750
- { keys: ["m"], label: "mismatches" },
751
- { keys: ["s"], label: "profile" },
752
- { keys: ["/"], label: "search" },
753
- ];
754
- const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
755
- const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
756
- const installedCount = plugins.filter((p) => p.enabled).length;
757
- const updateCount = plugins.filter((p) => p.hasUpdate &&
758
- (p.userScope?.enabled ||
759
- p.projectScope?.enabled ||
760
- p.localScope?.enabled)).length;
761
- const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
762
- const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
763
- return (_jsx(ScreenLayout, { title: "claudeup Plugins", subtitle: subtitle, currentScreen: "plugins", search: {
764
- isActive: isSearchActive,
765
- query: pluginsState.searchQuery,
766
- placeholder: searchPlaceholder,
767
- }, 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) }));
768
- }
769
- export default PluginsScreen;