claudeup 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -16,6 +16,10 @@ import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
16
16
  import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
17
17
  import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
18
18
  import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
19
+ // Virtual marketplace name for the community sub-section of claude-plugins-official
20
+ const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
21
+ // The marketplace that gets split into Anthropic Official + Community sections
22
+ const SPLIT_MARKETPLACE = "claude-plugins-official";
19
23
  export function PluginsScreen() {
20
24
  const { state, dispatch } = useApp();
21
25
  const { plugins: pluginsState } = state;
@@ -77,6 +81,64 @@ export function PluginsScreen() {
77
81
  const isCollapsed = collapsed.has(marketplace.name);
78
82
  const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
79
83
  const hasPlugins = marketplacePlugins.length > 0;
84
+ // Special handling: split claude-plugins-official into two sub-sections
85
+ if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
86
+ const anthropicPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() === "anthropic");
87
+ const communityPlugins = marketplacePlugins.filter((p) => p.author?.name?.toLowerCase() !== "anthropic");
88
+ // Sub-section 1: Anthropic Official (plugins by Anthropic)
89
+ const anthropicCollapsed = collapsed.has(marketplace.name);
90
+ const anthropicHasPlugins = anthropicPlugins.length > 0;
91
+ items.push({
92
+ id: `mp:${marketplace.name}`,
93
+ type: "category",
94
+ label: marketplace.displayName,
95
+ marketplace,
96
+ marketplaceEnabled: isEnabled,
97
+ pluginCount: anthropicPlugins.length,
98
+ isExpanded: !anthropicCollapsed && anthropicHasPlugins,
99
+ });
100
+ if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
101
+ for (const plugin of anthropicPlugins) {
102
+ items.push({
103
+ id: `pl:${plugin.id}`,
104
+ type: "plugin",
105
+ label: plugin.name,
106
+ plugin,
107
+ });
108
+ }
109
+ }
110
+ // Sub-section 2: Community (third-party plugins in same marketplace)
111
+ if (communityPlugins.length > 0) {
112
+ const communityVirtualMp = {
113
+ name: COMMUNITY_VIRTUAL_MARKETPLACE,
114
+ displayName: "Community",
115
+ source: marketplace.source,
116
+ description: "Third-party plugins from the community",
117
+ };
118
+ const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
119
+ items.push({
120
+ id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
121
+ type: "category",
122
+ label: "Community",
123
+ marketplace: communityVirtualMp,
124
+ marketplaceEnabled: true,
125
+ pluginCount: communityPlugins.length,
126
+ isExpanded: !communityCollapsed,
127
+ isCommunitySection: true,
128
+ });
129
+ if (!communityCollapsed) {
130
+ for (const plugin of communityPlugins) {
131
+ items.push({
132
+ id: `pl:${plugin.id}`,
133
+ type: "plugin",
134
+ label: plugin.name,
135
+ plugin,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ continue;
141
+ }
80
142
  // Category header (marketplace)
81
143
  items.push({
82
144
  id: `mp:${marketplace.name}`,
@@ -113,32 +175,44 @@ export function PluginsScreen() {
113
175
  // Only search plugins, not categories
114
176
  const pluginItems = allItems.filter((item) => item.type === "plugin");
115
177
  const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
116
- // Include parent categories for matched plugins
117
- const matchedMarketplaces = new Set();
178
+ // Build a set of matched plugin item ids for O(1) lookup
179
+ const matchedPluginIds = new Set();
118
180
  for (const result of fuzzyResults) {
119
- if (result.item.plugin) {
120
- matchedMarketplaces.add(result.item.plugin.marketplace);
181
+ matchedPluginIds.add(result.item.id);
182
+ }
183
+ // Walk allItems sequentially: track the current category section.
184
+ // For each category, include it only if any plugin under it matched.
185
+ // We build a map from category item id -> whether any plugin below matched.
186
+ const categoryHasMatch = new Map();
187
+ let currentCategoryId = null;
188
+ for (const item of allItems) {
189
+ if (item.type === "category") {
190
+ currentCategoryId = item.id;
191
+ if (!categoryHasMatch.has(item.id)) {
192
+ categoryHasMatch.set(item.id, false);
193
+ }
194
+ }
195
+ else if (item.type === "plugin" && currentCategoryId) {
196
+ if (matchedPluginIds.has(item.id)) {
197
+ categoryHasMatch.set(currentCategoryId, true);
198
+ }
121
199
  }
122
200
  }
123
201
  const result = [];
124
- let currentMarketplace = null;
202
+ let currentCatIncluded = false;
203
+ currentCategoryId = null;
125
204
  for (const item of allItems) {
126
- if (item.type === "category" && item.marketplace) {
127
- if (matchedMarketplaces.has(item.marketplace.name)) {
205
+ if (item.type === "category") {
206
+ currentCategoryId = item.id;
207
+ currentCatIncluded = categoryHasMatch.get(item.id) === true;
208
+ if (currentCatIncluded) {
128
209
  result.push(item);
129
- currentMarketplace = item.marketplace.name;
130
- }
131
- else {
132
- currentMarketplace = null;
133
210
  }
134
211
  }
135
- else if (item.type === "plugin" && item.plugin) {
136
- if (currentMarketplace === item.plugin.marketplace) {
137
- // Check if this plugin matched
212
+ else if (item.type === "plugin" && currentCatIncluded) {
213
+ if (matchedPluginIds.has(item.id)) {
138
214
  const matched = fuzzyResults.find((r) => r.item.id === item.id);
139
- if (matched) {
140
- result.push({ ...item, _matches: matched.matches });
141
- }
215
+ result.push({ ...item, _matches: matched?.matches });
142
216
  }
143
217
  }
144
218
  }
@@ -777,7 +851,11 @@ export function PluginsScreen() {
777
851
  let statusText = "";
778
852
  let statusColor = "green";
779
853
  if (item.marketplaceEnabled) {
780
- if (mp.name === "claude-plugins-official") {
854
+ if (item.isCommunitySection) {
855
+ statusText = "3rd Party";
856
+ statusColor = "gray";
857
+ }
858
+ else if (mp.name === "claude-plugins-official") {
781
859
  statusText = "★ Official";
782
860
  statusColor = "yellow";
783
861
  }
@@ -857,6 +935,8 @@ export function PluginsScreen() {
857
935
  const isEnabled = selectedItem.marketplaceEnabled;
858
936
  // Get appropriate badge for marketplace type
859
937
  const getBadge = () => {
938
+ if (selectedItem.isCommunitySection)
939
+ return " 3rd Party";
860
940
  if (mp.name === "claude-plugins-official")
861
941
  return " ★";
862
942
  if (mp.name === "claude-code-plugins")
@@ -41,6 +41,11 @@ import {
41
41
  } from "../../services/plugin-setup.js";
42
42
  import type { Marketplace } from "../../types/index.js";
43
43
 
44
+ // Virtual marketplace name for the community sub-section of claude-plugins-official
45
+ const COMMUNITY_VIRTUAL_MARKETPLACE = "claude-plugins-official:community";
46
+ // The marketplace that gets split into Anthropic Official + Community sections
47
+ const SPLIT_MARKETPLACE = "claude-plugins-official";
48
+
44
49
  interface ListItem {
45
50
  id: string;
46
51
  type: "category" | "plugin";
@@ -50,6 +55,8 @@ interface ListItem {
50
55
  plugin?: PluginInfo;
51
56
  pluginCount?: number;
52
57
  isExpanded?: boolean;
58
+ /** True for the virtual Community sub-section derived from claude-plugins-official */
59
+ isCommunitySection?: boolean;
53
60
  }
54
61
 
55
62
  export function PluginsScreen() {
@@ -128,6 +135,72 @@ export function PluginsScreen() {
128
135
  const isEnabled = marketplacePlugins.length > 0 || marketplace.official;
129
136
  const hasPlugins = marketplacePlugins.length > 0;
130
137
 
138
+ // Special handling: split claude-plugins-official into two sub-sections
139
+ if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
140
+ const anthropicPlugins = marketplacePlugins.filter(
141
+ (p) => p.author?.name?.toLowerCase() === "anthropic",
142
+ );
143
+ const communityPlugins = marketplacePlugins.filter(
144
+ (p) => p.author?.name?.toLowerCase() !== "anthropic",
145
+ );
146
+
147
+ // Sub-section 1: Anthropic Official (plugins by Anthropic)
148
+ const anthropicCollapsed = collapsed.has(marketplace.name);
149
+ const anthropicHasPlugins = anthropicPlugins.length > 0;
150
+ items.push({
151
+ id: `mp:${marketplace.name}`,
152
+ type: "category",
153
+ label: marketplace.displayName,
154
+ marketplace,
155
+ marketplaceEnabled: isEnabled,
156
+ pluginCount: anthropicPlugins.length,
157
+ isExpanded: !anthropicCollapsed && anthropicHasPlugins,
158
+ });
159
+ if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
160
+ for (const plugin of anthropicPlugins) {
161
+ items.push({
162
+ id: `pl:${plugin.id}`,
163
+ type: "plugin",
164
+ label: plugin.name,
165
+ plugin,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Sub-section 2: Community (third-party plugins in same marketplace)
171
+ if (communityPlugins.length > 0) {
172
+ const communityVirtualMp: Marketplace = {
173
+ name: COMMUNITY_VIRTUAL_MARKETPLACE,
174
+ displayName: "Community",
175
+ source: marketplace.source,
176
+ description: "Third-party plugins from the community",
177
+ };
178
+ const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
179
+ items.push({
180
+ id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
181
+ type: "category",
182
+ label: "Community",
183
+ marketplace: communityVirtualMp,
184
+ marketplaceEnabled: true,
185
+ pluginCount: communityPlugins.length,
186
+ isExpanded: !communityCollapsed,
187
+ isCommunitySection: true,
188
+ });
189
+ if (!communityCollapsed) {
190
+ for (const plugin of communityPlugins) {
191
+ items.push({
192
+ id: `pl:${plugin.id}`,
193
+ type: "plugin",
194
+ label: plugin.name,
195
+ plugin,
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ continue;
202
+ }
203
+
131
204
  // Category header (marketplace)
132
205
  items.push({
133
206
  id: `mp:${marketplace.name}`,
@@ -168,34 +241,47 @@ export function PluginsScreen() {
168
241
  const pluginItems = allItems.filter((item) => item.type === "plugin");
169
242
  const fuzzyResults = fuzzyFilter(pluginItems, query, (item) => item.label);
170
243
 
171
- // Include parent categories for matched plugins
172
- const matchedMarketplaces = new Set<string>();
244
+ // Build a set of matched plugin item ids for O(1) lookup
245
+ const matchedPluginIds = new Set<string>();
173
246
  for (const result of fuzzyResults) {
174
- if (result.item.plugin) {
175
- matchedMarketplaces.add(result.item.plugin.marketplace);
247
+ matchedPluginIds.add(result.item.id);
248
+ }
249
+
250
+ // Walk allItems sequentially: track the current category section.
251
+ // For each category, include it only if any plugin under it matched.
252
+ // We build a map from category item id -> whether any plugin below matched.
253
+ const categoryHasMatch = new Map<string, boolean>();
254
+ let currentCategoryId: string | null = null;
255
+ for (const item of allItems) {
256
+ if (item.type === "category") {
257
+ currentCategoryId = item.id;
258
+ if (!categoryHasMatch.has(item.id)) {
259
+ categoryHasMatch.set(item.id, false);
260
+ }
261
+ } else if (item.type === "plugin" && currentCategoryId) {
262
+ if (matchedPluginIds.has(item.id)) {
263
+ categoryHasMatch.set(currentCategoryId, true);
264
+ }
176
265
  }
177
266
  }
178
267
 
179
268
  const result: ListItem[] = [];
180
- let currentMarketplace: string | null = null;
269
+ let currentCatIncluded = false;
270
+ currentCategoryId = null;
181
271
 
182
272
  for (const item of allItems) {
183
- if (item.type === "category" && item.marketplace) {
184
- if (matchedMarketplaces.has(item.marketplace.name)) {
273
+ if (item.type === "category") {
274
+ currentCategoryId = item.id;
275
+ currentCatIncluded = categoryHasMatch.get(item.id) === true;
276
+ if (currentCatIncluded) {
185
277
  result.push(item);
186
- currentMarketplace = item.marketplace.name;
187
- } else {
188
- currentMarketplace = null;
189
278
  }
190
- } else if (item.type === "plugin" && item.plugin) {
191
- if (currentMarketplace === item.plugin.marketplace) {
192
- // Check if this plugin matched
279
+ } else if (item.type === "plugin" && currentCatIncluded) {
280
+ if (matchedPluginIds.has(item.id)) {
193
281
  const matched = fuzzyResults.find((r) => r.item.id === item.id);
194
- if (matched) {
195
- result.push({ ...item, _matches: matched.matches } as ListItem & {
196
- _matches?: number[];
197
- });
198
- }
282
+ result.push({ ...item, _matches: matched?.matches } as ListItem & {
283
+ _matches?: number[];
284
+ });
199
285
  }
200
286
  }
201
287
  }
@@ -1005,7 +1091,10 @@ export function PluginsScreen() {
1005
1091
  let statusText = "";
1006
1092
  let statusColor = "green";
1007
1093
  if (item.marketplaceEnabled) {
1008
- if (mp.name === "claude-plugins-official") {
1094
+ if (item.isCommunitySection) {
1095
+ statusText = "3rd Party";
1096
+ statusColor = "gray";
1097
+ } else if (mp.name === "claude-plugins-official") {
1009
1098
  statusText = "★ Official";
1010
1099
  statusColor = "yellow";
1011
1100
  } else if (mp.name === "claude-code-plugins") {
@@ -1131,6 +1220,7 @@ export function PluginsScreen() {
1131
1220
 
1132
1221
  // Get appropriate badge for marketplace type
1133
1222
  const getBadge = () => {
1223
+ if (selectedItem.isCommunitySection) return " 3rd Party";
1134
1224
  if (mp.name === "claude-plugins-official") return " ★";
1135
1225
  if (mp.name === "claude-code-plugins") return " ⚠";
1136
1226
  if (mp.official) return " ★";