claudeup 3.17.0 → 4.0.1

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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/data/predefined-profiles.js +69 -12
  3. package/src/data/predefined-profiles.ts +73 -14
  4. package/src/services/claude-cli.js +8 -1
  5. package/src/services/claude-cli.ts +10 -7
  6. package/src/services/plugin-manager.js +40 -4
  7. package/src/services/plugin-manager.ts +57 -6
  8. package/src/ui/adapters/pluginsAdapter.js +139 -0
  9. package/src/ui/adapters/pluginsAdapter.ts +202 -0
  10. package/src/ui/adapters/settingsAdapter.js +111 -0
  11. package/src/ui/adapters/settingsAdapter.ts +165 -0
  12. package/src/ui/components/ScrollableDetail.js +23 -0
  13. package/src/ui/components/ScrollableDetail.tsx +55 -0
  14. package/src/ui/components/ScrollableList.js +4 -4
  15. package/src/ui/components/ScrollableList.tsx +4 -4
  16. package/src/ui/components/SearchInput.js +2 -2
  17. package/src/ui/components/SearchInput.tsx +3 -3
  18. package/src/ui/components/StyledText.js +1 -1
  19. package/src/ui/components/StyledText.tsx +5 -1
  20. package/src/ui/components/layout/ProgressBar.js +1 -1
  21. package/src/ui/components/layout/ProgressBar.tsx +1 -5
  22. package/src/ui/components/layout/ScreenLayout.js +1 -1
  23. package/src/ui/components/layout/ScreenLayout.tsx +11 -8
  24. package/src/ui/components/modals/InputModal.tsx +1 -6
  25. package/src/ui/components/modals/LoadingModal.js +1 -1
  26. package/src/ui/components/modals/LoadingModal.tsx +1 -3
  27. package/src/ui/hooks/index.js +3 -3
  28. package/src/ui/hooks/index.ts +3 -3
  29. package/src/ui/hooks/useKeyboard.ts +1 -3
  30. package/src/ui/hooks/useKeyboardHandler.js +9 -9
  31. package/src/ui/hooks/useKeyboardHandler.ts +9 -9
  32. package/src/ui/renderers/cliToolRenderers.js +33 -0
  33. package/src/ui/renderers/cliToolRenderers.tsx +153 -0
  34. package/src/ui/renderers/mcpRenderers.js +26 -0
  35. package/src/ui/renderers/mcpRenderers.tsx +145 -0
  36. package/src/ui/renderers/pluginRenderers.js +124 -0
  37. package/src/ui/renderers/pluginRenderers.tsx +362 -0
  38. package/src/ui/renderers/profileRenderers.js +177 -0
  39. package/src/ui/renderers/profileRenderers.tsx +361 -0
  40. package/src/ui/renderers/settingsRenderers.js +69 -0
  41. package/src/ui/renderers/settingsRenderers.tsx +205 -0
  42. package/src/ui/screens/CliToolsScreen.js +14 -58
  43. package/src/ui/screens/CliToolsScreen.tsx +36 -196
  44. package/src/ui/screens/EnvVarsScreen.js +12 -168
  45. package/src/ui/screens/EnvVarsScreen.tsx +16 -327
  46. package/src/ui/screens/McpScreen.js +12 -62
  47. package/src/ui/screens/McpScreen.tsx +21 -190
  48. package/src/ui/screens/PluginsScreen.js +52 -425
  49. package/src/ui/screens/PluginsScreen.tsx +70 -758
  50. package/src/ui/screens/ProfilesScreen.js +32 -97
  51. package/src/ui/screens/ProfilesScreen.tsx +58 -328
  52. package/src/ui/screens/SkillsScreen.js +16 -16
  53. package/src/ui/screens/SkillsScreen.tsx +20 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.17.0",
3
+ "version": "4.0.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -1,9 +1,73 @@
1
1
  export const PREDEFINED_PROFILES = [
2
+ {
3
+ id: "must-have",
4
+ name: "Must Have",
5
+ description: "Essential plugins every Claude Code user should have",
6
+ icon: "★",
7
+ magusPlugins: ["statusline", "multimodel"],
8
+ anthropicPlugins: [
9
+ "claude-code-setup",
10
+ "claude-md-management",
11
+ "code-simplifier",
12
+ "explanatory-output-style",
13
+ "playground",
14
+ "skill-creator",
15
+ ],
16
+ skills: ["Find Skills"],
17
+ settings: {
18
+ effortLevel: "high",
19
+ alwaysThinkingEnabled: true,
20
+ outputStyle: "explanatory",
21
+ env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
22
+ respectGitignore: true,
23
+ enableAllProjectMcpServers: true,
24
+ },
25
+ },
26
+ {
27
+ id: "developer-essentials",
28
+ name: "Developer Essentials",
29
+ description: "Must Have + full dev toolkit: code analysis, browser, terminal, design, review",
30
+ icon: "▶",
31
+ magusPlugins: [
32
+ "statusline",
33
+ "multimodel",
34
+ "code-analysis",
35
+ "dev",
36
+ "browser-use",
37
+ "designer",
38
+ "terminal",
39
+ "kanban",
40
+ ],
41
+ anthropicPlugins: [
42
+ "claude-code-setup",
43
+ "claude-md-management",
44
+ "code-simplifier",
45
+ "explanatory-output-style",
46
+ "playground",
47
+ "skill-creator",
48
+ "code-review",
49
+ "commit-commands",
50
+ "feature-dev",
51
+ ],
52
+ skills: ["Find Skills", "Systematic Debugging"],
53
+ settings: {
54
+ effortLevel: "high",
55
+ alwaysThinkingEnabled: true,
56
+ outputStyle: "explanatory",
57
+ env: {
58
+ CLAUDE_CODE_ENABLE_TASKS: "true",
59
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
60
+ },
61
+ includeGitInstructions: true,
62
+ respectGitignore: true,
63
+ enableAllProjectMcpServers: true,
64
+ },
65
+ },
2
66
  {
3
67
  id: "frontend-pro",
4
68
  name: "Frontend Pro",
5
69
  description: "UI implementation, design fidelity, browser-driven workflows",
6
- icon: "🎨",
70
+ icon: "",
7
71
  magusPlugins: [
8
72
  "dev",
9
73
  "code-analysis",
@@ -30,7 +94,6 @@ export const PREDEFINED_PROFILES = [
30
94
  settings: {
31
95
  effortLevel: "high",
32
96
  alwaysThinkingEnabled: true,
33
- model: "claude-sonnet-4-6",
34
97
  outputStyle: "explanatory",
35
98
  env: {
36
99
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -45,7 +108,7 @@ export const PREDEFINED_PROFILES = [
45
108
  id: "backend-forge",
46
109
  name: "Backend Forge",
47
110
  description: "API development, debugging, code quality, data workflows",
48
- icon: "⚙️",
111
+ icon: "",
49
112
  magusPlugins: [
50
113
  "dev",
51
114
  "code-analysis",
@@ -61,13 +124,11 @@ export const PREDEFINED_PROFILES = [
61
124
  "code-simplifier",
62
125
  "commit-commands",
63
126
  "security-guidance",
64
- "agent-sdk-dev",
65
127
  ],
66
128
  skills: ["Systematic Debugging", "Neon Postgres", "Find Skills"],
67
129
  settings: {
68
130
  effortLevel: "high",
69
131
  alwaysThinkingEnabled: true,
70
- model: "claude-sonnet-4-6",
71
132
  outputStyle: "concise",
72
133
  env: {
73
134
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -82,7 +143,7 @@ export const PREDEFINED_PROFILES = [
82
143
  id: "infra-ops",
83
144
  name: "Infra Ops",
84
145
  description: "Infrastructure, operational debugging, automation, terminal-first",
85
- icon: "🔧",
146
+ icon: "",
86
147
  magusPlugins: [
87
148
  "dev",
88
149
  "code-analysis",
@@ -104,7 +165,6 @@ export const PREDEFINED_PROFILES = [
104
165
  settings: {
105
166
  effortLevel: "high",
106
167
  alwaysThinkingEnabled: true,
107
- model: "claude-opus-4-6",
108
168
  outputStyle: "concise",
109
169
  env: {
110
170
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -119,7 +179,7 @@ export const PREDEFINED_PROFILES = [
119
179
  id: "growth-marketer",
120
180
  name: "Growth Marketer",
121
181
  description: "SEO, website audits, content production, marketing automation",
122
- icon: "📈",
182
+ icon: "",
123
183
  magusPlugins: [
124
184
  "dev",
125
185
  "code-analysis",
@@ -143,8 +203,6 @@ export const PREDEFINED_PROFILES = [
143
203
  ],
144
204
  settings: {
145
205
  effortLevel: "medium",
146
- alwaysThinkingEnabled: false,
147
- model: "claude-sonnet-4-6",
148
206
  outputStyle: "explanatory",
149
207
  env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
150
208
  respectGitignore: true,
@@ -155,7 +213,7 @@ export const PREDEFINED_PROFILES = [
155
213
  id: "team-lead",
156
214
  name: "Team Lead",
157
215
  description: "Planning, code review, coordination, and broad repo visibility",
158
- icon: "👔",
216
+ icon: "",
159
217
  magusPlugins: [
160
218
  "dev",
161
219
  "code-analysis",
@@ -177,7 +235,6 @@ export const PREDEFINED_PROFILES = [
177
235
  settings: {
178
236
  effortLevel: "medium",
179
237
  alwaysThinkingEnabled: true,
180
- model: "claude-sonnet-4-6",
181
238
  outputStyle: "explanatory",
182
239
  env: {
183
240
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -2,7 +2,7 @@ export interface PredefinedProfile {
2
2
  id: string;
3
3
  name: string;
4
4
  description: string;
5
- icon: string; // emoji or symbol
5
+ icon: string;
6
6
  magusPlugins: string[];
7
7
  anthropicPlugins: string[];
8
8
  skills: string[];
@@ -10,11 +10,77 @@ export interface PredefinedProfile {
10
10
  }
11
11
 
12
12
  export const PREDEFINED_PROFILES: PredefinedProfile[] = [
13
+ {
14
+ id: "must-have",
15
+ name: "Must Have",
16
+ description: "Essential plugins every Claude Code user should have",
17
+ icon: "★",
18
+ magusPlugins: ["statusline", "multimodel"],
19
+ anthropicPlugins: [
20
+ "claude-code-setup",
21
+ "claude-md-management",
22
+ "code-simplifier",
23
+ "explanatory-output-style",
24
+ "playground",
25
+ "skill-creator",
26
+ ],
27
+ skills: ["Find Skills"],
28
+ settings: {
29
+ effortLevel: "high",
30
+ alwaysThinkingEnabled: true,
31
+ outputStyle: "explanatory",
32
+ env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
33
+ respectGitignore: true,
34
+ enableAllProjectMcpServers: true,
35
+ },
36
+ },
37
+ {
38
+ id: "developer-essentials",
39
+ name: "Developer Essentials",
40
+ description:
41
+ "Must Have + full dev toolkit: code analysis, browser, terminal, design, review",
42
+ icon: "▶",
43
+ magusPlugins: [
44
+ "statusline",
45
+ "multimodel",
46
+ "code-analysis",
47
+ "dev",
48
+ "browser-use",
49
+ "designer",
50
+ "terminal",
51
+ "kanban",
52
+ ],
53
+ anthropicPlugins: [
54
+ "claude-code-setup",
55
+ "claude-md-management",
56
+ "code-simplifier",
57
+ "explanatory-output-style",
58
+ "playground",
59
+ "skill-creator",
60
+ "code-review",
61
+ "commit-commands",
62
+ "feature-dev",
63
+ ],
64
+ skills: ["Find Skills", "Systematic Debugging"],
65
+ settings: {
66
+ effortLevel: "high",
67
+ alwaysThinkingEnabled: true,
68
+ outputStyle: "explanatory",
69
+ env: {
70
+ CLAUDE_CODE_ENABLE_TASKS: "true",
71
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
72
+ },
73
+ includeGitInstructions: true,
74
+ respectGitignore: true,
75
+ enableAllProjectMcpServers: true,
76
+ },
77
+ },
13
78
  {
14
79
  id: "frontend-pro",
15
80
  name: "Frontend Pro",
16
- description: "UI implementation, design fidelity, browser-driven workflows",
17
- icon: "🎨",
81
+ description:
82
+ "UI implementation, design fidelity, browser-driven workflows",
83
+ icon: "★",
18
84
  magusPlugins: [
19
85
  "dev",
20
86
  "code-analysis",
@@ -41,7 +107,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
41
107
  settings: {
42
108
  effortLevel: "high",
43
109
  alwaysThinkingEnabled: true,
44
- model: "claude-sonnet-4-6",
45
110
  outputStyle: "explanatory",
46
111
  env: {
47
112
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -56,7 +121,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
56
121
  id: "backend-forge",
57
122
  name: "Backend Forge",
58
123
  description: "API development, debugging, code quality, data workflows",
59
- icon: "⚙️",
124
+ icon: "",
60
125
  magusPlugins: [
61
126
  "dev",
62
127
  "code-analysis",
@@ -72,13 +137,11 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
72
137
  "code-simplifier",
73
138
  "commit-commands",
74
139
  "security-guidance",
75
- "agent-sdk-dev",
76
140
  ],
77
141
  skills: ["Systematic Debugging", "Neon Postgres", "Find Skills"],
78
142
  settings: {
79
143
  effortLevel: "high",
80
144
  alwaysThinkingEnabled: true,
81
- model: "claude-sonnet-4-6",
82
145
  outputStyle: "concise",
83
146
  env: {
84
147
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -94,7 +157,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
94
157
  name: "Infra Ops",
95
158
  description:
96
159
  "Infrastructure, operational debugging, automation, terminal-first",
97
- icon: "🔧",
160
+ icon: "",
98
161
  magusPlugins: [
99
162
  "dev",
100
163
  "code-analysis",
@@ -116,7 +179,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
116
179
  settings: {
117
180
  effortLevel: "high",
118
181
  alwaysThinkingEnabled: true,
119
- model: "claude-opus-4-6",
120
182
  outputStyle: "concise",
121
183
  env: {
122
184
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -132,7 +194,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
132
194
  name: "Growth Marketer",
133
195
  description:
134
196
  "SEO, website audits, content production, marketing automation",
135
- icon: "📈",
197
+ icon: "",
136
198
  magusPlugins: [
137
199
  "dev",
138
200
  "code-analysis",
@@ -156,8 +218,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
156
218
  ],
157
219
  settings: {
158
220
  effortLevel: "medium",
159
- alwaysThinkingEnabled: false,
160
- model: "claude-sonnet-4-6",
161
221
  outputStyle: "explanatory",
162
222
  env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
163
223
  respectGitignore: true,
@@ -169,7 +229,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
169
229
  name: "Team Lead",
170
230
  description:
171
231
  "Planning, code review, coordination, and broad repo visibility",
172
- icon: "👔",
232
+ icon: "",
173
233
  magusPlugins: [
174
234
  "dev",
175
235
  "code-analysis",
@@ -191,7 +251,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
191
251
  settings: {
192
252
  effortLevel: "medium",
193
253
  alwaysThinkingEnabled: true,
194
- model: "claude-sonnet-4-6",
195
254
  outputStyle: "explanatory",
196
255
  env: {
197
256
  CLAUDE_CODE_ENABLE_TASKS: "true",
@@ -12,7 +12,7 @@ import { execFile } from "node:child_process";
12
12
  import { promisify } from "node:util";
13
13
  import { which } from "../utils/command-utils.js";
14
14
  import { removeGlobalInstalledPluginVersion, removeLocalInstalledPluginVersion, } from "./claude-settings.js";
15
- import { removeInstalledPluginVersion, } from "./plugin-manager.js";
15
+ import { removeInstalledPluginVersion } from "./plugin-manager.js";
16
16
  const execFileAsync = promisify(execFile);
17
17
  /**
18
18
  * Get the path to the claude CLI binary
@@ -107,6 +107,13 @@ export async function updatePlugin(pluginId, scope = "user") {
107
107
  export async function addMarketplace(repo) {
108
108
  await execClaude(["plugin", "marketplace", "add", repo], 60000);
109
109
  }
110
+ /**
111
+ * Update marketplace cache by running git pull via Claude CLI
112
+ * Uses longer timeout since this involves git network operations
113
+ */
114
+ export async function updateMarketplace(name) {
115
+ await execClaude(["plugin", "marketplace", "update", name], 60000);
116
+ }
110
117
  /**
111
118
  * Check if the claude CLI is available
112
119
  * @returns true if claude CLI is found in PATH
@@ -16,9 +16,7 @@ import {
16
16
  removeGlobalInstalledPluginVersion,
17
17
  removeLocalInstalledPluginVersion,
18
18
  } from "./claude-settings.js";
19
- import {
20
- removeInstalledPluginVersion,
21
- } from "./plugin-manager.js";
19
+ import { removeInstalledPluginVersion } from "./plugin-manager.js";
22
20
 
23
21
  const execFileAsync = promisify(execFile);
24
22
 
@@ -44,10 +42,7 @@ async function getClaudePath(): Promise<string> {
44
42
  * @param timeoutMs - Timeout in milliseconds (default: 30s)
45
43
  * @returns stdout from the command
46
44
  */
47
- async function execClaude(
48
- args: string[],
49
- timeoutMs = 30000,
50
- ): Promise<string> {
45
+ async function execClaude(args: string[], timeoutMs = 30000): Promise<string> {
51
46
  const claudePath = await getClaudePath();
52
47
  try {
53
48
  const { stdout } = await execFileAsync(claudePath, args, {
@@ -148,6 +143,14 @@ export async function addMarketplace(repo: string): Promise<void> {
148
143
  await execClaude(["plugin", "marketplace", "add", repo], 60000);
149
144
  }
150
145
 
146
+ /**
147
+ * Update marketplace cache by running git pull via Claude CLI
148
+ * Uses longer timeout since this involves git network operations
149
+ */
150
+ export async function updateMarketplace(name: string): Promise<void> {
151
+ await execClaude(["plugin", "marketplace", "update", name], 60000);
152
+ }
153
+
151
154
  /**
152
155
  * Check if the claude CLI is available
153
156
  * @returns true if claude CLI is found in PATH
@@ -5,6 +5,7 @@ import { getConfiguredMarketplaces, getEnabledPlugins, readSettings, writeSettin
5
5
  import { defaultMarketplaces } from "../data/marketplaces.js";
6
6
  import { scanLocalMarketplaces, repairAllMarketplaces, } from "./local-marketplace.js";
7
7
  import { formatMarketplaceName, isValidGitHubRepo, parsePluginId, } from "../utils/string-utils.js";
8
+ import { updateMarketplace } from "./claude-cli.js";
8
9
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
9
10
  let localMarketplacesPromise = null;
10
11
  // Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
@@ -104,12 +105,16 @@ export async function getAvailablePlugins(projectPath) {
104
105
  marketplaceNames.add(mp.name);
105
106
  }
106
107
  }
108
+ // Fetch local marketplace caches up front so we can detect stale caches
109
+ let localMarketplaces = await getLocalMarketplaces();
107
110
  // Fetch plugins from each configured marketplace
108
111
  for (const mpName of marketplaceNames) {
109
112
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
110
113
  if (!marketplace)
111
114
  continue;
112
115
  const marketplacePlugins = await fetchMarketplacePlugins(mpName, marketplace.source.repo);
116
+ // Auto-sync local cache if remote has plugins the local cache doesn't
117
+ localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
113
118
  for (const plugin of marketplacePlugins) {
114
119
  const pluginId = `${plugin.name}@${mpName}`;
115
120
  const installedVersion = installedVersions[pluginId];
@@ -136,8 +141,6 @@ export async function getAvailablePlugins(projectPath) {
136
141
  });
137
142
  }
138
143
  }
139
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
140
- const localMarketplaces = await getLocalMarketplaces();
141
144
  for (const [mpName, localMp] of localMarketplaces) {
142
145
  // Skip if already fetched from defaults
143
146
  if (marketplaceNames.has(mpName))
@@ -256,12 +259,16 @@ export async function getGlobalAvailablePlugins() {
256
259
  marketplaceNames.add(mp.name);
257
260
  }
258
261
  }
262
+ // Fetch local marketplace caches up front so we can detect stale caches
263
+ let localMarketplaces = await getLocalMarketplaces();
259
264
  // Fetch plugins from each configured marketplace
260
265
  for (const mpName of marketplaceNames) {
261
266
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
262
267
  if (!marketplace)
263
268
  continue;
264
269
  const marketplacePlugins = await fetchMarketplacePlugins(mpName, marketplace.source.repo);
270
+ // Auto-sync local cache if remote has plugins the local cache doesn't
271
+ localMarketplaces = await autoSyncIfStale(mpName, marketplacePlugins.map((p) => p.name), localMarketplaces);
265
272
  for (const plugin of marketplacePlugins) {
266
273
  const pluginId = `${plugin.name}@${mpName}`;
267
274
  const installedVersion = installedVersions[pluginId];
@@ -288,8 +295,6 @@ export async function getGlobalAvailablePlugins() {
288
295
  });
289
296
  }
290
297
  }
291
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
292
- const localMarketplaces = await getLocalMarketplaces();
293
298
  for (const [mpName, localMp] of localMarketplaces) {
294
299
  // Skip if already fetched from defaults
295
300
  if (marketplaceNames.has(mpName))
@@ -424,6 +429,37 @@ async function getLocalMarketplaces() {
424
429
  export async function getLocalMarketplacesInfo() {
425
430
  return getLocalMarketplaces();
426
431
  }
432
+ // Track which marketplaces have already been auto-synced this session
433
+ // so we only attempt the update once per marketplace per claudeup run.
434
+ const autoSyncedMarketplaces = new Set();
435
+ /**
436
+ * If the remote manifest lists plugins that aren't in the local cache,
437
+ * the local clone is stale. Silently run `claude plugin marketplace update`
438
+ * to pull the latest, then invalidate the local cache so the next scan
439
+ * picks up the new plugins.
440
+ */
441
+ async function autoSyncIfStale(mpName, remotePluginNames, localMarketplaces) {
442
+ if (autoSyncedMarketplaces.has(mpName))
443
+ return localMarketplaces;
444
+ const localMp = localMarketplaces.get(mpName);
445
+ if (!localMp)
446
+ return localMarketplaces;
447
+ const localNames = new Set(localMp.plugins.map((p) => p.name));
448
+ const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
449
+ if (!hasMissing)
450
+ return localMarketplaces;
451
+ autoSyncedMarketplaces.add(mpName);
452
+ try {
453
+ await updateMarketplace(mpName);
454
+ // Invalidate local cache so re-scan picks up new plugins
455
+ localMarketplacesPromise = null;
456
+ return getLocalMarketplaces();
457
+ }
458
+ catch {
459
+ // Update failed (no network, CLI missing, etc.) — continue with stale data
460
+ return localMarketplaces;
461
+ }
462
+ }
427
463
  /**
428
464
  * Refresh claudeup's internal cache
429
465
  * Note: Marketplace updates should be done via Claude Code's /plugin marketplace update command
@@ -27,6 +27,7 @@ import {
27
27
  isValidGitHubRepo,
28
28
  parsePluginId,
29
29
  } from "../utils/string-utils.js";
30
+ import { updateMarketplace } from "./claude-cli.js";
30
31
 
31
32
  // Cache for local marketplaces (session-level) - Promise-based to prevent race conditions
32
33
  let localMarketplacesPromise: Promise<Map<string, LocalMarketplace>> | null =
@@ -206,6 +207,9 @@ export async function getAvailablePlugins(
206
207
  }
207
208
  }
208
209
 
210
+ // Fetch local marketplace caches up front so we can detect stale caches
211
+ let localMarketplaces = await getLocalMarketplaces();
212
+
209
213
  // Fetch plugins from each configured marketplace
210
214
  for (const mpName of marketplaceNames) {
211
215
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
@@ -216,6 +220,13 @@ export async function getAvailablePlugins(
216
220
  marketplace.source.repo,
217
221
  );
218
222
 
223
+ // Auto-sync local cache if remote has plugins the local cache doesn't
224
+ localMarketplaces = await autoSyncIfStale(
225
+ mpName,
226
+ marketplacePlugins.map((p) => p.name),
227
+ localMarketplaces,
228
+ );
229
+
219
230
  for (const plugin of marketplacePlugins) {
220
231
  const pluginId = `${plugin.name}@${mpName}`;
221
232
  const installedVersion = installedVersions[pluginId];
@@ -245,9 +256,6 @@ export async function getAvailablePlugins(
245
256
  }
246
257
  }
247
258
 
248
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
249
- const localMarketplaces = await getLocalMarketplaces();
250
-
251
259
  for (const [mpName, localMp] of localMarketplaces) {
252
260
  // Skip if already fetched from defaults
253
261
  if (marketplaceNames.has(mpName)) continue;
@@ -383,6 +391,9 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
383
391
  }
384
392
  }
385
393
 
394
+ // Fetch local marketplace caches up front so we can detect stale caches
395
+ let localMarketplaces = await getLocalMarketplaces();
396
+
386
397
  // Fetch plugins from each configured marketplace
387
398
  for (const mpName of marketplaceNames) {
388
399
  const marketplace = defaultMarketplaces.find((m) => m.name === mpName);
@@ -393,6 +404,13 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
393
404
  marketplace.source.repo,
394
405
  );
395
406
 
407
+ // Auto-sync local cache if remote has plugins the local cache doesn't
408
+ localMarketplaces = await autoSyncIfStale(
409
+ mpName,
410
+ marketplacePlugins.map((p) => p.name),
411
+ localMarketplaces,
412
+ );
413
+
396
414
  for (const plugin of marketplacePlugins) {
397
415
  const pluginId = `${plugin.name}@${mpName}`;
398
416
  const installedVersion = installedVersions[pluginId];
@@ -422,9 +440,6 @@ export async function getGlobalAvailablePlugins(): Promise<PluginInfo[]> {
422
440
  }
423
441
  }
424
442
 
425
- // Fetch ALL plugins from local marketplace caches (for marketplaces not in defaults)
426
- const localMarketplaces = await getLocalMarketplaces();
427
-
428
443
  for (const [mpName, localMp] of localMarketplaces) {
429
444
  // Skip if already fetched from defaults
430
445
  if (marketplaceNames.has(mpName)) continue;
@@ -598,6 +613,42 @@ export async function getLocalMarketplacesInfo(): Promise<
598
613
  return getLocalMarketplaces();
599
614
  }
600
615
 
616
+ // Track which marketplaces have already been auto-synced this session
617
+ // so we only attempt the update once per marketplace per claudeup run.
618
+ const autoSyncedMarketplaces = new Set<string>();
619
+
620
+ /**
621
+ * If the remote manifest lists plugins that aren't in the local cache,
622
+ * the local clone is stale. Silently run `claude plugin marketplace update`
623
+ * to pull the latest, then invalidate the local cache so the next scan
624
+ * picks up the new plugins.
625
+ */
626
+ async function autoSyncIfStale(
627
+ mpName: string,
628
+ remotePluginNames: string[],
629
+ localMarketplaces: Map<string, LocalMarketplace>,
630
+ ): Promise<Map<string, LocalMarketplace>> {
631
+ if (autoSyncedMarketplaces.has(mpName)) return localMarketplaces;
632
+
633
+ const localMp = localMarketplaces.get(mpName);
634
+ if (!localMp) return localMarketplaces;
635
+
636
+ const localNames = new Set(localMp.plugins.map((p) => p.name));
637
+ const hasMissing = remotePluginNames.some((name) => !localNames.has(name));
638
+ if (!hasMissing) return localMarketplaces;
639
+
640
+ autoSyncedMarketplaces.add(mpName);
641
+ try {
642
+ await updateMarketplace(mpName);
643
+ // Invalidate local cache so re-scan picks up new plugins
644
+ localMarketplacesPromise = null;
645
+ return getLocalMarketplaces();
646
+ } catch {
647
+ // Update failed (no network, CLI missing, etc.) — continue with stale data
648
+ return localMarketplaces;
649
+ }
650
+ }
651
+
601
652
  export interface RefreshAndRepairResult {
602
653
  refresh: never[];
603
654
  repair: RepairMarketplaceResult[];