claudeup 4.19.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 (180) 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-refresh.test.ts +267 -0
  18. package/src/__tests__/plugin-requires.test.ts +133 -0
  19. package/src/__tests__/plugin-setup.test.ts +7 -0
  20. package/src/__tests__/profile-command.test.ts +116 -0
  21. package/src/__tests__/profile-materializer.test.ts +82 -0
  22. package/src/__tests__/profile-sync.test.ts +136 -0
  23. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  24. package/src/__tests__/resolve-executable.test.ts +42 -0
  25. package/src/__tests__/resolver.test.ts +218 -0
  26. package/src/__tests__/symlink-manager.test.ts +99 -0
  27. package/src/__tests__/toolchain.test.ts +56 -0
  28. package/src/cli/claude.ts +21 -0
  29. package/src/cli/doctor.ts +132 -0
  30. package/src/cli/install.ts +360 -0
  31. package/src/cli/profile.ts +166 -0
  32. package/src/cli/router.ts +107 -0
  33. package/src/cli/update.ts +85 -0
  34. package/src/data/cli-tools.ts +7 -7
  35. package/src/data/gitignore-defaults.ts +4 -0
  36. package/src/data/marketplaces.ts +12 -0
  37. package/src/data/predefined-profiles.ts +3 -4
  38. package/src/main.tsx +11 -154
  39. package/src/prerunner/index.ts +62 -27
  40. package/src/services/claude-cli.ts +19 -22
  41. package/src/services/claude-settings.ts +99 -26
  42. package/src/services/conventions-manager.ts +865 -0
  43. package/src/services/doctor-bins.ts +49 -0
  44. package/src/services/doctor.ts +509 -0
  45. package/src/services/install-plan.ts +107 -0
  46. package/src/services/manifest.ts +166 -0
  47. package/src/services/marketplace-refresh.ts +173 -0
  48. package/src/services/plugin-manager.ts +7 -1
  49. package/src/services/plugin-requires.ts +215 -0
  50. package/src/services/plugin-version-check.ts +9 -4
  51. package/src/services/profile-materializer.ts +61 -0
  52. package/src/services/profile-sync.ts +150 -0
  53. package/src/services/resolver.ts +293 -0
  54. package/src/services/symlink-manager.ts +146 -0
  55. package/src/services/toolchain.ts +97 -0
  56. package/src/services/version-check.ts +10 -11
  57. package/src/types/index.ts +138 -31
  58. package/src/ui/App.tsx +0 -4
  59. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  60. package/src/ui/screens/index.ts +0 -1
  61. package/src/ui/state/reducer.ts +0 -101
  62. package/src/ui/state/types.ts +0 -35
  63. package/src/utils/command-utils.ts +23 -0
  64. package/src/utils/file-locking.ts +144 -0
  65. package/src/data/alias-flags.js +0 -205
  66. package/src/data/cli-tools.js +0 -123
  67. package/src/data/gitignore-defaults.js +0 -24
  68. package/src/data/gitignore-reasons.js +0 -97
  69. package/src/data/gitignore-templates.js +0 -21
  70. package/src/data/marketplaces.js +0 -138
  71. package/src/data/mcp-servers.js +0 -509
  72. package/src/data/predefined-profiles.js +0 -248
  73. package/src/data/settings-catalog.js +0 -625
  74. package/src/data/skill-repos.js +0 -160
  75. package/src/data/statuslines.js +0 -159
  76. package/src/data/statuslines.ts +0 -188
  77. package/src/index.js +0 -4
  78. package/src/index.ts +0 -5
  79. package/src/main.js +0 -170
  80. package/src/prerunner/index.js +0 -252
  81. package/src/services/alias-settings.js +0 -51
  82. package/src/services/alias-shell-writer.js +0 -1018
  83. package/src/services/alias-store.js +0 -129
  84. package/src/services/claude-cli.js +0 -189
  85. package/src/services/claude-runner.js +0 -28
  86. package/src/services/claude-settings.js +0 -1223
  87. package/src/services/gitignore-detector.js +0 -155
  88. package/src/services/gitignore-fixer.js +0 -231
  89. package/src/services/gitignore-prerun.js +0 -46
  90. package/src/services/gitignore-resolver.js +0 -143
  91. package/src/services/gitignore-service.js +0 -117
  92. package/src/services/local-marketplace.js +0 -339
  93. package/src/services/marketplace-fetcher.js +0 -96
  94. package/src/services/marketplace-sync.js +0 -94
  95. package/src/services/mcp-registry.js +0 -87
  96. package/src/services/plugin-manager.js +0 -473
  97. package/src/services/plugin-mcp-config.js +0 -177
  98. package/src/services/plugin-setup.js +0 -499
  99. package/src/services/plugin-version-check.js +0 -262
  100. package/src/services/profiles.js +0 -161
  101. package/src/services/settings-manager.js +0 -243
  102. package/src/services/skills-manager.js +0 -393
  103. package/src/services/skillsmp-client.js +0 -87
  104. package/src/services/update-cache.js +0 -52
  105. package/src/services/version-check.js +0 -99
  106. package/src/types/gitignore.js +0 -6
  107. package/src/types/index.js +0 -1
  108. package/src/ui/App.js +0 -355
  109. package/src/ui/adapters/pluginsAdapter.js +0 -139
  110. package/src/ui/adapters/settingsAdapter.js +0 -111
  111. package/src/ui/adapters/skillsAdapter.js +0 -154
  112. package/src/ui/components/CategoryHeader.js +0 -9
  113. package/src/ui/components/EmptyFilterState.js +0 -4
  114. package/src/ui/components/FlagDetailEditor.js +0 -0
  115. package/src/ui/components/ScopeIndicator.js +0 -30
  116. package/src/ui/components/ScrollableList.js +0 -36
  117. package/src/ui/components/SearchInput.js +0 -19
  118. package/src/ui/components/StyledText.js +0 -39
  119. package/src/ui/components/TabBar.js +0 -19
  120. package/src/ui/components/layout/FooterHints.js +0 -29
  121. package/src/ui/components/layout/Panel.js +0 -6
  122. package/src/ui/components/layout/ProgressBar.js +0 -14
  123. package/src/ui/components/layout/ScopeTabs.js +0 -6
  124. package/src/ui/components/layout/ScreenLayout.js +0 -16
  125. package/src/ui/components/layout/index.js +0 -5
  126. package/src/ui/components/modals/ConfirmModal.js +0 -14
  127. package/src/ui/components/modals/InputModal.js +0 -5
  128. package/src/ui/components/modals/LoadingModal.js +0 -17
  129. package/src/ui/components/modals/MessageModal.js +0 -16
  130. package/src/ui/components/modals/ModalContainer.js +0 -137
  131. package/src/ui/components/modals/SelectModal.js +0 -18
  132. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  133. package/src/ui/components/modals/index.js +0 -6
  134. package/src/ui/components/primitives/ActionHints.js +0 -13
  135. package/src/ui/components/primitives/DetailSection.js +0 -7
  136. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  137. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  138. package/src/ui/components/primitives/MetaText.js +0 -8
  139. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  140. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  141. package/src/ui/components/primitives/SelectableRow.js +0 -5
  142. package/src/ui/components/primitives/index.js +0 -8
  143. package/src/ui/hooks/index.js +0 -4
  144. package/src/ui/hooks/useAsyncData.js +0 -77
  145. package/src/ui/hooks/useGitignoreModal.js +0 -74
  146. package/src/ui/hooks/useKeyboard.js +0 -13
  147. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  148. package/src/ui/hooks/useMismatchModal.js +0 -77
  149. package/src/ui/registry.js +0 -1
  150. package/src/ui/renderers/cliToolRenderers.js +0 -54
  151. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  152. package/src/ui/renderers/mcpRenderers.js +0 -26
  153. package/src/ui/renderers/pluginRenderers.js +0 -124
  154. package/src/ui/renderers/profileRenderers.js +0 -177
  155. package/src/ui/renderers/settingsRenderers.js +0 -73
  156. package/src/ui/renderers/skillRenderers.js +0 -138
  157. package/src/ui/screens/AliasScreen.js +0 -1111
  158. package/src/ui/screens/CliToolsScreen.js +0 -338
  159. package/src/ui/screens/EnvVarsScreen.js +0 -152
  160. package/src/ui/screens/GitignoreScreen.js +0 -328
  161. package/src/ui/screens/McpRegistryScreen.js +0 -238
  162. package/src/ui/screens/McpScreen.js +0 -176
  163. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  164. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  165. package/src/ui/screens/PluginsScreen.js +0 -769
  166. package/src/ui/screens/ProfilesScreen.js +0 -298
  167. package/src/ui/screens/SkillsScreen.js +0 -549
  168. package/src/ui/screens/StatusLineScreen.js +0 -206
  169. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  170. package/src/ui/screens/index.js +0 -10
  171. package/src/ui/state/AnimationContext.js +0 -34
  172. package/src/ui/state/AppContext.js +0 -162
  173. package/src/ui/state/DimensionsContext.js +0 -71
  174. package/src/ui/state/reducer.js +0 -547
  175. package/src/ui/state/types.js +0 -1
  176. package/src/ui/theme.js +0 -47
  177. package/src/utils/clipboard.js +0 -56
  178. package/src/utils/command-utils.js +0 -19
  179. package/src/utils/fuzzy-search.js +0 -101
  180. package/src/utils/string-utils.js +0 -62
@@ -1,1223 +0,0 @@
1
- import fs from "fs-extra";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import semver from "semver";
5
- import { parsePluginId } from "../utils/string-utils.js";
6
- const CLAUDE_DIR = ".claude";
7
- const SETTINGS_FILE = "settings.json";
8
- const LOCAL_SETTINGS_FILE = "settings.local.json";
9
- const MCP_CONFIG_FILE = ".mcp.json";
10
- export function getClaudeDir(projectPath) {
11
- const base = projectPath || process.cwd();
12
- return path.join(base, CLAUDE_DIR);
13
- }
14
- export function getGlobalClaudeDir() {
15
- return path.join(os.homedir(), CLAUDE_DIR);
16
- }
17
- export async function ensureClaudeDir(projectPath) {
18
- const claudeDir = getClaudeDir(projectPath);
19
- await fs.ensureDir(claudeDir);
20
- return claudeDir;
21
- }
22
- export async function readSettings(projectPath) {
23
- const settingsPath = path.join(getClaudeDir(projectPath), SETTINGS_FILE);
24
- try {
25
- if (await fs.pathExists(settingsPath)) {
26
- return await fs.readJson(settingsPath);
27
- }
28
- }
29
- catch {
30
- // Return empty settings on error
31
- }
32
- return {};
33
- }
34
- export async function writeSettings(settings, projectPath) {
35
- const claudeDir = await ensureClaudeDir(projectPath);
36
- const settingsPath = path.join(claudeDir, SETTINGS_FILE);
37
- await fs.writeJson(settingsPath, settings, { spaces: 2 });
38
- }
39
- export async function readLocalSettings(projectPath) {
40
- const localPath = path.join(getClaudeDir(projectPath), LOCAL_SETTINGS_FILE);
41
- try {
42
- if (await fs.pathExists(localPath)) {
43
- return await fs.readJson(localPath);
44
- }
45
- }
46
- catch {
47
- // Return empty settings on error
48
- }
49
- return {};
50
- }
51
- export async function writeLocalSettings(settings, projectPath) {
52
- const claudeDir = await ensureClaudeDir(projectPath);
53
- const localPath = path.join(claudeDir, LOCAL_SETTINGS_FILE);
54
- await fs.writeJson(localPath, settings, { spaces: 2 });
55
- }
56
- // MCP config file management (.mcp.json at project root)
57
- export function getMcpConfigPath(projectPath) {
58
- const base = projectPath || process.cwd();
59
- return path.join(base, MCP_CONFIG_FILE);
60
- }
61
- export async function readMcpConfig(projectPath) {
62
- const mcpPath = getMcpConfigPath(projectPath);
63
- try {
64
- if (await fs.pathExists(mcpPath)) {
65
- return await fs.readJson(mcpPath);
66
- }
67
- }
68
- catch {
69
- // Return empty config on error
70
- }
71
- return {};
72
- }
73
- export async function writeMcpConfig(config, projectPath) {
74
- const mcpPath = getMcpConfigPath(projectPath);
75
- await fs.writeJson(mcpPath, config, { spaces: 2 });
76
- }
77
- export async function readGlobalSettings() {
78
- const settingsPath = path.join(getGlobalClaudeDir(), SETTINGS_FILE);
79
- try {
80
- if (await fs.pathExists(settingsPath)) {
81
- return await fs.readJson(settingsPath);
82
- }
83
- }
84
- catch {
85
- // Return empty settings on error
86
- }
87
- return {};
88
- }
89
- export async function writeGlobalSettings(settings) {
90
- await fs.ensureDir(getGlobalClaudeDir());
91
- const settingsPath = path.join(getGlobalClaudeDir(), SETTINGS_FILE);
92
- await fs.writeJson(settingsPath, settings, { spaces: 2 });
93
- }
94
- // MCP Server management (writes to .mcp.json at project root)
95
- export async function addMcpServer(name, config, projectPath) {
96
- // Extract env vars from config - they go to settings.local.json, not .mcp.json
97
- const envVars = config.env || {};
98
- const configWithoutEnv = { ...config };
99
- delete configWithoutEnv.env;
100
- // Add to .mcp.json (without env vars)
101
- const mcpConfig = await readMcpConfig(projectPath);
102
- mcpConfig.mcpServers = mcpConfig.mcpServers || {};
103
- mcpConfig.mcpServers[name] = configWithoutEnv;
104
- await writeMcpConfig(mcpConfig, projectPath);
105
- // Enable in settings.local.json and add env vars
106
- const localSettings = await readLocalSettings(projectPath);
107
- const enabledServers = localSettings.enabledMcpjsonServers || [];
108
- if (!enabledServers.includes(name)) {
109
- enabledServers.push(name);
110
- }
111
- localSettings.enabledMcpjsonServers = enabledServers;
112
- localSettings.enableAllProjectMcpServers = true;
113
- // Add env vars to settings.local.json
114
- if (Object.keys(envVars).length > 0) {
115
- localSettings.env = localSettings.env || {};
116
- for (const [key, value] of Object.entries(envVars)) {
117
- // Only add non-reference values (references like ${VAR} don't need to be stored)
118
- if (!value.startsWith("${") || !value.endsWith("}")) {
119
- localSettings.env[key] = value;
120
- }
121
- }
122
- }
123
- await writeLocalSettings(localSettings, projectPath);
124
- }
125
- export async function removeMcpServer(name, projectPath) {
126
- // Remove from .mcp.json
127
- const mcpConfig = await readMcpConfig(projectPath);
128
- if (mcpConfig.mcpServers) {
129
- delete mcpConfig.mcpServers[name];
130
- }
131
- await writeMcpConfig(mcpConfig, projectPath);
132
- // Remove from settings.local.json
133
- const localSettings = await readLocalSettings(projectPath);
134
- if (localSettings.enabledMcpjsonServers) {
135
- localSettings.enabledMcpjsonServers =
136
- localSettings.enabledMcpjsonServers.filter((s) => s !== name);
137
- await writeLocalSettings(localSettings, projectPath);
138
- }
139
- }
140
- export async function toggleMcpServer(name, enabled, projectPath) {
141
- // Toggle is now a remove operation since .mcp.json doesn't have enabled/disabled state
142
- // If disabled, remove from config; if enabled, the server should already be in config
143
- if (!enabled) {
144
- await removeMcpServer(name, projectPath);
145
- }
146
- // If enabling, the server should already exist in the config
147
- }
148
- export async function setAllowMcp(_allow, _projectPath) {
149
- // .mcp.json doesn't have an allowMcp setting - servers are either in the file or not
150
- // This function is kept for API compatibility but is now a no-op
151
- }
152
- // Marketplace management - READ ONLY
153
- // Use Claude Code CLI commands to add/remove marketplaces:
154
- // claude marketplace add owner/repo
155
- // claude marketplace remove name
156
- // Plugin management
157
- export async function enablePlugin(pluginId, enabled, projectPath) {
158
- const settings = await readSettings(projectPath);
159
- settings.enabledPlugins = settings.enabledPlugins || {};
160
- if (enabled) {
161
- settings.enabledPlugins[pluginId] = true;
162
- }
163
- else {
164
- delete settings.enabledPlugins[pluginId];
165
- }
166
- await writeSettings(settings, projectPath);
167
- }
168
- export async function getEnabledPlugins(projectPath) {
169
- const settings = await readSettings(projectPath);
170
- return settings.enabledPlugins || {};
171
- }
172
- export async function getLocalEnabledPlugins(projectPath) {
173
- const settings = await readLocalSettings(projectPath);
174
- return settings.enabledPlugins || {};
175
- }
176
- export async function getLocalInstalledPluginVersions(projectPath) {
177
- const settings = await readLocalSettings(projectPath);
178
- return settings.installedPluginVersions || {};
179
- }
180
- // Local plugin management (writes to settings.local.json)
181
- export async function enableLocalPlugin(pluginId, enabled, projectPath) {
182
- const settings = await readLocalSettings(projectPath);
183
- settings.enabledPlugins = settings.enabledPlugins || {};
184
- if (enabled) {
185
- settings.enabledPlugins[pluginId] = true;
186
- }
187
- else {
188
- delete settings.enabledPlugins[pluginId];
189
- }
190
- await writeLocalSettings(settings, projectPath);
191
- }
192
- export async function saveLocalInstalledPluginVersion(pluginId, version, projectPath) {
193
- const settings = await readLocalSettings(projectPath);
194
- settings.installedPluginVersions = settings.installedPluginVersions || {};
195
- settings.installedPluginVersions[pluginId] = version;
196
- await writeLocalSettings(settings, projectPath);
197
- // Update registry for local scope
198
- await updateInstalledPluginsRegistry(pluginId, version, "local", projectPath ? path.resolve(projectPath) : undefined);
199
- }
200
- export async function removeLocalInstalledPluginVersion(pluginId, projectPath) {
201
- const settings = await readLocalSettings(projectPath);
202
- if (settings.installedPluginVersions) {
203
- delete settings.installedPluginVersions[pluginId];
204
- }
205
- if (settings.enabledPlugins) {
206
- delete settings.enabledPlugins[pluginId];
207
- }
208
- await writeLocalSettings(settings, projectPath);
209
- // Remove from registry for local scope
210
- await removeFromInstalledPluginsRegistry(pluginId, "local", projectPath ? path.resolve(projectPath) : undefined);
211
- }
212
- // Status line management
213
- // Claude Code expects statusLine as an object: { type: "template", template: "..." }
214
- // or { type: "command", command: "..." }. Writing a bare string causes a validation error
215
- // that skips the entire settings file.
216
- function wrapStatusLine(template) {
217
- if (!template)
218
- return undefined;
219
- return { type: "template", template };
220
- }
221
- function unwrapStatusLine(value) {
222
- if (!value)
223
- return undefined;
224
- if (typeof value === "string")
225
- return value; // legacy format
226
- return value.template ?? value.command;
227
- }
228
- export async function setStatusLine(template, projectPath) {
229
- const settings = await readSettings(projectPath);
230
- const wrapped = wrapStatusLine(template);
231
- if (wrapped) {
232
- settings.statusLine = wrapped;
233
- }
234
- else {
235
- delete settings.statusLine;
236
- }
237
- await writeSettings(settings, projectPath);
238
- }
239
- export async function getStatusLine(projectPath) {
240
- const settings = await readSettings(projectPath);
241
- return unwrapStatusLine(settings.statusLine);
242
- }
243
- // Global status line management
244
- export async function setGlobalStatusLine(template) {
245
- const settings = await readGlobalSettings();
246
- const wrapped = wrapStatusLine(template);
247
- if (wrapped) {
248
- settings.statusLine = wrapped;
249
- }
250
- else {
251
- delete settings.statusLine;
252
- }
253
- await writeGlobalSettings(settings);
254
- }
255
- export async function getGlobalStatusLine() {
256
- const settings = await readGlobalSettings();
257
- return unwrapStatusLine(settings.statusLine);
258
- }
259
- // Get effective status line (project overrides global)
260
- export async function getEffectiveStatusLine(projectPath) {
261
- const projectStatusLine = await getStatusLine(projectPath);
262
- if (projectStatusLine) {
263
- return { template: projectStatusLine, source: "project" };
264
- }
265
- const globalStatusLine = await getGlobalStatusLine();
266
- if (globalStatusLine) {
267
- return { template: globalStatusLine, source: "global" };
268
- }
269
- return { template: undefined, source: "default" };
270
- }
271
- // Check if .claude directory exists
272
- export async function hasClaudeDir(projectPath) {
273
- return fs.pathExists(getClaudeDir(projectPath));
274
- }
275
- // Get installed MCP servers (from .mcp.json)
276
- export async function getInstalledMcpServers(projectPath) {
277
- const mcpConfig = await readMcpConfig(projectPath);
278
- return mcpConfig.mcpServers || {};
279
- }
280
- // Get env vars for MCP servers (from settings.local.json)
281
- export async function getMcpEnvVars(projectPath) {
282
- const localSettings = await readLocalSettings(projectPath);
283
- return localSettings.env || {};
284
- }
285
- // Set an env var for MCP servers (in settings.local.json)
286
- export async function setMcpEnvVar(name, value, projectPath) {
287
- const localSettings = await readLocalSettings(projectPath);
288
- localSettings.env = localSettings.env || {};
289
- localSettings.env[name] = value;
290
- await writeLocalSettings(localSettings, projectPath);
291
- }
292
- // Remove an env var (from settings.local.json)
293
- export async function removeMcpEnvVar(name, projectPath) {
294
- const localSettings = await readLocalSettings(projectPath);
295
- if (localSettings.env) {
296
- delete localSettings.env[name];
297
- await writeLocalSettings(localSettings, projectPath);
298
- }
299
- }
300
- // Get enabled MCP servers (all servers in .mcp.json are considered enabled)
301
- export async function getEnabledMcpServers(projectPath) {
302
- const mcpConfig = await readMcpConfig(projectPath);
303
- const servers = mcpConfig.mcpServers || {};
304
- const enabled = {};
305
- for (const name of Object.keys(servers)) {
306
- enabled[name] = true;
307
- }
308
- return enabled;
309
- }
310
- // Get all configured marketplaces — merges known_marketplaces.json (primary) with extraKnownMarketplaces (legacy fallback)
311
- export async function getConfiguredMarketplaces(projectPath) {
312
- const result = {};
313
- // Primary: known_marketplaces.json (official Claude Code registry)
314
- const known = await readKnownMarketplaces();
315
- for (const [name, entry] of Object.entries(known)) {
316
- if (entry.source?.repo) {
317
- result[name] = {
318
- source: { source: "github", repo: entry.source.repo },
319
- };
320
- }
321
- }
322
- // Fallback: extraKnownMarketplaces (legacy — only adds entries not already in known)
323
- const settings = await readSettings(projectPath);
324
- for (const [name, config] of Object.entries(settings.extraKnownMarketplaces || {})) {
325
- if (!result[name]) {
326
- result[name] = config;
327
- }
328
- }
329
- return result;
330
- }
331
- // Global marketplace management - READ ONLY
332
- // Marketplaces are managed via Claude Code's native system
333
- export async function getGlobalConfiguredMarketplaces() {
334
- const result = {};
335
- // Primary: known_marketplaces.json
336
- const known = await readKnownMarketplaces();
337
- for (const [name, entry] of Object.entries(known)) {
338
- if (entry.source?.repo) {
339
- result[name] = {
340
- source: { source: "github", repo: entry.source.repo },
341
- };
342
- }
343
- }
344
- // Fallback: extraKnownMarketplaces
345
- const settings = await readGlobalSettings();
346
- for (const [name, config] of Object.entries(settings.extraKnownMarketplaces || {})) {
347
- if (!result[name]) {
348
- result[name] = config;
349
- }
350
- }
351
- return result;
352
- }
353
- // Global plugin management
354
- export async function enableGlobalPlugin(pluginId, enabled) {
355
- const settings = await readGlobalSettings();
356
- settings.enabledPlugins = settings.enabledPlugins || {};
357
- if (enabled) {
358
- settings.enabledPlugins[pluginId] = true;
359
- }
360
- else {
361
- delete settings.enabledPlugins[pluginId];
362
- }
363
- await writeGlobalSettings(settings);
364
- }
365
- export async function getGlobalEnabledPlugins() {
366
- const settings = await readGlobalSettings();
367
- return settings.enabledPlugins || {};
368
- }
369
- export async function getGlobalInstalledPluginVersions() {
370
- const settings = await readGlobalSettings();
371
- return settings.installedPluginVersions || {};
372
- }
373
- export async function saveGlobalInstalledPluginVersion(pluginId, version) {
374
- const settings = await readGlobalSettings();
375
- settings.installedPluginVersions = settings.installedPluginVersions || {};
376
- settings.installedPluginVersions[pluginId] = version;
377
- await writeGlobalSettings(settings);
378
- // Update registry for user scope
379
- await updateInstalledPluginsRegistry(pluginId, version, "user");
380
- }
381
- /**
382
- * Gap-fill: ensure global installedPluginVersions is at least as high as
383
- * project and local scopes. When a plugin is updated at project scope,
384
- * global may lag behind, causing Claude Code to resolve stale cache paths.
385
- *
386
- * @returns Array of { pluginId, oldVersion, newVersion } for plugins that were bumped
387
- */
388
- export async function gapFillInstalledPluginVersions(projectPath) {
389
- const globalVersions = await getGlobalInstalledPluginVersions();
390
- // Collect versions from project and local scopes
391
- const allVersions = {};
392
- // Start with global versions
393
- for (const [id, ver] of Object.entries(globalVersions)) {
394
- allVersions[id] = [ver];
395
- }
396
- // Add project scope versions
397
- if (projectPath) {
398
- try {
399
- const settings = await readSettings(projectPath);
400
- for (const [id, ver] of Object.entries(settings.installedPluginVersions || {})) {
401
- if (!allVersions[id])
402
- allVersions[id] = [];
403
- allVersions[id].push(ver);
404
- }
405
- }
406
- catch {
407
- /* skip unreadable */
408
- }
409
- // Add local scope versions
410
- try {
411
- const localSettings = await readLocalSettings(projectPath);
412
- for (const [id, ver] of Object.entries(localSettings.installedPluginVersions || {})) {
413
- if (!allVersions[id])
414
- allVersions[id] = [];
415
- allVersions[id].push(ver);
416
- }
417
- }
418
- catch {
419
- /* skip unreadable */
420
- }
421
- }
422
- // Find plugins where global needs bumping
423
- const bumped = [];
424
- for (const [pluginId, versions] of Object.entries(allVersions)) {
425
- // Filter to valid semver versions and sort descending
426
- const valid = versions.filter((v) => semver.valid(v));
427
- if (valid.length === 0)
428
- continue;
429
- const highest = valid.sort((a, b) => semver.rcompare(a, b))[0];
430
- const globalVer = globalVersions[pluginId];
431
- // If global is missing, invalid semver, or lower than the highest, bump it
432
- if (!globalVer ||
433
- !semver.valid(globalVer) ||
434
- semver.lt(globalVer, highest)) {
435
- bumped.push({
436
- pluginId,
437
- oldVersion: globalVer || "(missing)",
438
- newVersion: highest,
439
- });
440
- }
441
- }
442
- // Apply bumps in a single write
443
- if (bumped.length > 0) {
444
- const settings = await readGlobalSettings();
445
- settings.installedPluginVersions =
446
- settings.installedPluginVersions || {};
447
- for (const { pluginId, newVersion } of bumped) {
448
- settings.installedPluginVersions[pluginId] = newVersion;
449
- }
450
- await writeGlobalSettings(settings);
451
- }
452
- return bumped;
453
- }
454
- export async function removeGlobalInstalledPluginVersion(pluginId) {
455
- const settings = await readGlobalSettings();
456
- if (settings.installedPluginVersions) {
457
- delete settings.installedPluginVersions[pluginId];
458
- }
459
- if (settings.enabledPlugins) {
460
- delete settings.enabledPlugins[pluginId];
461
- }
462
- await writeGlobalSettings(settings);
463
- // Remove from registry for user scope
464
- await removeFromInstalledPluginsRegistry(pluginId, "user");
465
- }
466
- // Shared logic for discovering marketplaces from settings
467
- function discoverMarketplacesFromSettings(settings, knownMarketplaces) {
468
- const discovered = new Map();
469
- // 1. From known_marketplaces.json (official Claude Code registry — primary source)
470
- if (knownMarketplaces) {
471
- for (const [name, entry] of Object.entries(knownMarketplaces)) {
472
- if (entry.source?.repo) {
473
- discovered.set(name, {
474
- name,
475
- source: "configured",
476
- config: {
477
- source: { source: "github", repo: entry.source.repo },
478
- },
479
- });
480
- }
481
- }
482
- }
483
- // 2. From extraKnownMarketplaces (legacy/team fallback — only adds entries not already in known_marketplaces.json)
484
- for (const [name, config] of Object.entries(settings.extraKnownMarketplaces || {})) {
485
- if (!discovered.has(name)) {
486
- discovered.set(name, { name, source: "configured", config });
487
- }
488
- }
489
- // 3. From enabledPlugins (infer marketplace from plugin ID format: pluginName@marketplaceName)
490
- for (const pluginId of Object.keys(settings.enabledPlugins || {})) {
491
- const parsed = parsePluginId(pluginId);
492
- if (parsed && !discovered.has(parsed.marketplace)) {
493
- discovered.set(parsed.marketplace, {
494
- name: parsed.marketplace,
495
- source: "inferred",
496
- });
497
- }
498
- }
499
- // 4. From installedPluginVersions (same format)
500
- for (const pluginId of Object.keys(settings.installedPluginVersions || {})) {
501
- const parsed = parsePluginId(pluginId);
502
- if (parsed && !discovered.has(parsed.marketplace)) {
503
- discovered.set(parsed.marketplace, {
504
- name: parsed.marketplace,
505
- source: "inferred",
506
- });
507
- }
508
- }
509
- return Array.from(discovered.values());
510
- }
511
- // Discover all marketplaces from settings (configured + inferred from plugins)
512
- export async function discoverAllMarketplaces(projectPath) {
513
- try {
514
- const settings = await readSettings(projectPath);
515
- const known = await readKnownMarketplaces();
516
- return discoverMarketplacesFromSettings(settings, known);
517
- }
518
- catch (error) {
519
- // Graceful degradation - return empty array instead of crashing
520
- console.error("Failed to discover project marketplaces:", error instanceof Error ? error.message : "Unknown error");
521
- return [];
522
- }
523
- }
524
- // Discover all marketplaces from global settings
525
- export async function discoverAllGlobalMarketplaces() {
526
- try {
527
- const settings = await readGlobalSettings();
528
- const known = await readKnownMarketplaces();
529
- return discoverMarketplacesFromSettings(settings, known);
530
- }
531
- catch (error) {
532
- // Graceful degradation - return empty array instead of crashing
533
- console.error("Failed to discover global marketplaces:", error instanceof Error ? error.message : "Unknown error");
534
- return [];
535
- }
536
- }
537
- // installed_plugins.json registry management
538
- const INSTALLED_PLUGINS_FILE = path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json");
539
- const KNOWN_MARKETPLACES_FILE = path.join(os.homedir(), ".claude", "plugins", "known_marketplaces.json");
540
- /**
541
- * Write known_marketplaces.json
542
- */
543
- async function writeKnownMarketplaces(marketplaces) {
544
- await fs.ensureDir(path.dirname(KNOWN_MARKETPLACES_FILE));
545
- await fs.writeJson(KNOWN_MARKETPLACES_FILE, marketplaces, { spaces: 2 });
546
- }
547
- /**
548
- * Set autoUpdate flag for a marketplace in known_marketplaces.json
549
- * This is where Claude Code actually reads the autoUpdate setting
550
- */
551
- export async function setMarketplaceAutoUpdate(marketplaceName, autoUpdate) {
552
- const known = await readKnownMarketplaces();
553
- if (known[marketplaceName]) {
554
- known[marketplaceName].autoUpdate = autoUpdate;
555
- await writeKnownMarketplaces(known);
556
- return true;
557
- }
558
- return false; // Marketplace not yet installed by Claude Code
559
- }
560
- /**
561
- * Get autoUpdate status for a marketplace
562
- */
563
- export async function getMarketplaceAutoUpdate(marketplaceName) {
564
- const known = await readKnownMarketplaces();
565
- return known[marketplaceName]?.autoUpdate;
566
- }
567
- // =============================================================================
568
- // MARKETPLACE RENAME MIGRATION: mag-claude-plugins / MadAppGang-claude-code → magus
569
- // =============================================================================
570
- const OLD_MARKETPLACE_NAMES = ["mag-claude-plugins", "MadAppGang-claude-code"];
571
- const NEW_MARKETPLACE_NAME = "magus";
572
- const NEW_MARKETPLACE_REPO = "MadAppGang/magus";
573
- const OLD_MARKETPLACE_REPOS = ["MadAppGang/claude-code"];
574
- /**
575
- * Rename plugin keys in a Record from any old marketplace name to new.
576
- * e.g., "frontend@mag-claude-plugins" → "frontend@magus"
577
- * "dev@MadAppGang-claude-code" → "dev@magus"
578
- * Returns [migratedRecord, count] — count=0 means no changes.
579
- */
580
- function migratePluginKeys(record) {
581
- if (!record)
582
- return [{}, 0];
583
- const migrated = {};
584
- let count = 0;
585
- for (const [key, value] of Object.entries(record)) {
586
- const oldName = OLD_MARKETPLACE_NAMES.find((n) => key.endsWith(`@${n}`));
587
- if (oldName) {
588
- const pluginName = key.slice(0, key.lastIndexOf("@"));
589
- const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
590
- // Don't overwrite if canonical key already exists
591
- if (!record[newKey]) {
592
- migrated[newKey] = value;
593
- }
594
- count++;
595
- }
596
- else {
597
- migrated[key] = value;
598
- }
599
- }
600
- return [migrated, count];
601
- }
602
- /**
603
- * Migrate a single settings object in-place.
604
- * Returns number of keys renamed.
605
- */
606
- function migrateSettingsObject(settings) {
607
- let total = 0;
608
- const [ep, epCount] = migratePluginKeys(settings.enabledPlugins);
609
- if (epCount > 0) {
610
- settings.enabledPlugins = ep;
611
- total += epCount;
612
- }
613
- const [iv, ivCount] = migratePluginKeys(settings.installedPluginVersions);
614
- if (ivCount > 0) {
615
- settings.installedPluginVersions = iv;
616
- total += ivCount;
617
- }
618
- // Migrate extraKnownMarketplaces keys
619
- for (const oldName of OLD_MARKETPLACE_NAMES) {
620
- if (settings.extraKnownMarketplaces?.[oldName]) {
621
- const entry = settings.extraKnownMarketplaces[oldName];
622
- delete settings.extraKnownMarketplaces[oldName];
623
- if (!settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME]) {
624
- settings.extraKnownMarketplaces[NEW_MARKETPLACE_NAME] = {
625
- ...entry,
626
- source: {
627
- ...entry.source,
628
- repo: NEW_MARKETPLACE_REPO,
629
- },
630
- };
631
- }
632
- total++;
633
- }
634
- }
635
- // Fix stale repo URL on existing magus entry (e.g. key is "magus" but repo is still "MadAppGang/claude-code")
636
- const magusEntry = settings.extraKnownMarketplaces?.[NEW_MARKETPLACE_NAME];
637
- if (magusEntry?.source?.repo &&
638
- OLD_MARKETPLACE_REPOS.includes(magusEntry.source.repo)) {
639
- magusEntry.source.repo = NEW_MARKETPLACE_REPO;
640
- total++;
641
- }
642
- return total;
643
- }
644
- /**
645
- * Migrate all settings from mag-claude-plugins to magus.
646
- * Safe to call multiple times — no-ops if already migrated.
647
- * Runs across project settings, global settings, local settings,
648
- * known_marketplaces.json, and installed_plugins.json.
649
- */
650
- /**
651
- * Decode a ~/.claude/projects/ directory name back to a filesystem path.
652
- * Claude Code encodes paths by replacing "/" with "-", which is lossy when
653
- * directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
654
- * Strategy: split on "-", then greedily recombine segments by checking which
655
- * combinations actually exist on disk.
656
- * Returns null if no valid path can be resolved.
657
- */
658
- async function decodeProjectDirName(encoded) {
659
- // Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
660
- const segments = encoded.replace(/^-/, "").split("-");
661
- if (segments.length === 0)
662
- return null;
663
- // Build path greedily: at each step, try joining the next segment with a dash first
664
- // (preserving directory names like "circl-infra"), fall back to slash (new path segment)
665
- let current = "/" + segments[0];
666
- for (let i = 1; i < segments.length; i++) {
667
- const withDash = current + "-" + segments[i];
668
- const withSlash = current + "/" + segments[i];
669
- // Prefer dash (keeps compound names intact) if that path prefix exists
670
- if (await fs.pathExists(withDash)) {
671
- current = withDash;
672
- }
673
- else {
674
- current = withSlash;
675
- }
676
- }
677
- return (await fs.pathExists(current)) ? current : null;
678
- }
679
- export async function migrateMarketplaceRename(projectPath) {
680
- const result = {
681
- projectMigrated: 0,
682
- globalMigrated: 0,
683
- localMigrated: 0,
684
- knownMarketplacesMigrated: false,
685
- registryMigrated: 0,
686
- };
687
- // 1. Project settings
688
- try {
689
- const settings = await readSettings(projectPath);
690
- const count = migrateSettingsObject(settings);
691
- if (count > 0) {
692
- await writeSettings(settings, projectPath);
693
- result.projectMigrated = count;
694
- }
695
- }
696
- catch {
697
- /* skip if unreadable */
698
- }
699
- // 2. Global settings
700
- try {
701
- const settings = await readGlobalSettings();
702
- const count = migrateSettingsObject(settings);
703
- if (count > 0) {
704
- await writeGlobalSettings(settings);
705
- result.globalMigrated = count;
706
- }
707
- }
708
- catch {
709
- /* skip if unreadable */
710
- }
711
- // 3. Local settings (settings.local.json)
712
- try {
713
- const local = await readLocalSettings(projectPath);
714
- let localCount = 0;
715
- const [ep, epCount] = migratePluginKeys(local.enabledPlugins);
716
- if (epCount > 0) {
717
- local.enabledPlugins = ep;
718
- localCount += epCount;
719
- }
720
- const [iv, ivCount] = migratePluginKeys(local.installedPluginVersions);
721
- if (ivCount > 0) {
722
- local.installedPluginVersions = iv;
723
- localCount += ivCount;
724
- }
725
- if (localCount > 0) {
726
- await writeLocalSettings(local, projectPath);
727
- result.localMigrated = localCount;
728
- }
729
- }
730
- catch {
731
- /* skip if unreadable */
732
- }
733
- // 4. known_marketplaces.json — rename old keys + physical directory cleanup
734
- const pluginsDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
735
- const newDir = path.join(pluginsDir, NEW_MARKETPLACE_NAME);
736
- try {
737
- const known = await readKnownMarketplaces();
738
- let knownModified = false;
739
- for (const oldName of OLD_MARKETPLACE_NAMES) {
740
- if (known[oldName]) {
741
- const oldEntry = known[oldName];
742
- // If canonical entry doesn't exist yet, create it
743
- if (!known[NEW_MARKETPLACE_NAME]) {
744
- known[NEW_MARKETPLACE_NAME] = {
745
- ...oldEntry,
746
- source: {
747
- ...oldEntry.source,
748
- repo: "MadAppGang/magus",
749
- },
750
- };
751
- }
752
- delete known[oldName];
753
- knownModified = true;
754
- }
755
- // Ensure installLocation doesn't reference old directory names
756
- if (known[NEW_MARKETPLACE_NAME]?.installLocation?.includes(oldName)) {
757
- known[NEW_MARKETPLACE_NAME].installLocation = known[NEW_MARKETPLACE_NAME].installLocation.replace(oldName, NEW_MARKETPLACE_NAME);
758
- knownModified = true;
759
- }
760
- }
761
- if (knownModified) {
762
- await writeKnownMarketplaces(known);
763
- result.knownMarketplacesMigrated = true;
764
- }
765
- }
766
- catch {
767
- /* skip if unreadable */
768
- }
769
- // 4b. Rename/remove old physical directories (runs even if keys were already migrated)
770
- for (const oldName of OLD_MARKETPLACE_NAMES) {
771
- const oldDir = path.join(pluginsDir, oldName);
772
- try {
773
- if (await fs.pathExists(oldDir)) {
774
- if (!(await fs.pathExists(newDir))) {
775
- await fs.rename(oldDir, newDir);
776
- }
777
- else {
778
- // Both exist — remove the old one (magus dir is canonical)
779
- await fs.remove(oldDir);
780
- }
781
- }
782
- }
783
- catch {
784
- /* non-fatal: directory cleanup is best-effort */
785
- }
786
- }
787
- // 4c. Update git remote URL in the marketplace clone (old → new repo)
788
- try {
789
- if (await fs.pathExists(path.join(newDir, ".git"))) {
790
- const { execSync } = await import("node:child_process");
791
- const remote = execSync("git remote get-url origin", {
792
- cwd: newDir,
793
- encoding: "utf-8",
794
- timeout: 5000,
795
- }).trim();
796
- if (remote.includes("claude-code") && remote.includes("MadAppGang")) {
797
- const newRemote = remote.replace("claude-code", NEW_MARKETPLACE_NAME);
798
- execSync(`git remote set-url origin "${newRemote}"`, {
799
- cwd: newDir,
800
- encoding: "utf-8",
801
- timeout: 5000,
802
- });
803
- }
804
- }
805
- }
806
- catch {
807
- /* non-fatal: git remote update is best-effort */
808
- }
809
- // 5. installed_plugins.json — rename plugin ID keys
810
- try {
811
- const registry = await readInstalledPluginsRegistry();
812
- let regCount = 0;
813
- const newPlugins = {};
814
- for (const [pluginId, entries] of Object.entries(registry.plugins)) {
815
- const oldName = OLD_MARKETPLACE_NAMES.find((n) => pluginId.endsWith(`@${n}`));
816
- if (oldName) {
817
- const pluginName = pluginId.slice(0, pluginId.lastIndexOf("@"));
818
- const newKey = `${pluginName}@${NEW_MARKETPLACE_NAME}`;
819
- if (!newPlugins[newKey]) {
820
- newPlugins[newKey] = entries;
821
- }
822
- regCount++;
823
- }
824
- else {
825
- newPlugins[pluginId] = entries;
826
- }
827
- }
828
- if (regCount > 0) {
829
- registry.plugins = newPlugins;
830
- await writeInstalledPluginsRegistry(registry);
831
- result.registryMigrated = regCount;
832
- }
833
- }
834
- catch {
835
- /* skip if unreadable */
836
- }
837
- // 6. Scan all known project settings (derived from ~/.claude/projects/ directory names)
838
- try {
839
- const projectsDir = path.join(os.homedir(), ".claude", "projects");
840
- if (await fs.pathExists(projectsDir)) {
841
- const entries = await fs.readdir(projectsDir);
842
- const seenPaths = new Set();
843
- // Current project (from step 1) already handled — skip it
844
- const currentProject = projectPath || process.cwd();
845
- seenPaths.add(currentProject);
846
- for (const entry of entries) {
847
- const decoded = await decodeProjectDirName(entry);
848
- if (!decoded || seenPaths.has(decoded))
849
- continue;
850
- seenPaths.add(decoded);
851
- const settingsFile = path.join(decoded, ".claude", "settings.json");
852
- try {
853
- if (await fs.pathExists(settingsFile)) {
854
- const raw = await fs.readJson(settingsFile);
855
- const count = migrateSettingsObject(raw);
856
- if (count > 0) {
857
- await fs.writeJson(settingsFile, raw, { spaces: 2 });
858
- result.projectMigrated += count;
859
- }
860
- }
861
- }
862
- catch {
863
- /* skip individual projects that fail */
864
- }
865
- }
866
- }
867
- }
868
- catch {
869
- /* non-fatal: cross-project scan is best-effort */
870
- }
871
- return result;
872
- }
873
- /**
874
- * Check if a marketplace is from Magus (MadAppGang)
875
- */
876
- function isMadAppGangMarketplace(entry) {
877
- const repo = entry.source?.repo?.toLowerCase() || "";
878
- return repo.includes("madappgang");
879
- }
880
- /**
881
- * Recover/sync marketplace settings:
882
- * - Enable autoUpdate for Magus marketplaces that don't have it set
883
- * - Remove entries for marketplaces whose installLocation no longer exists
884
- * - Fix stale "directory" source for known Magus marketplaces that should be "github"
885
- */
886
- export async function recoverMarketplaceSettings() {
887
- const known = await readKnownMarketplaces();
888
- const result = {
889
- enabledAutoUpdate: [],
890
- removed: [],
891
- reregistered: [],
892
- };
893
- const updatedKnown = {};
894
- // Import defaultMarketplaces lazily to get canonical repo URLs
895
- const { defaultMarketplaces } = await import("../data/marketplaces.js");
896
- for (const [name, entry] of Object.entries(known)) {
897
- // Check if install location still exists
898
- if (entry.installLocation &&
899
- !(await fs.pathExists(entry.installLocation))) {
900
- result.removed.push(name);
901
- continue;
902
- }
903
- // Fix stale "directory" source for known marketplaces.
904
- // Some machines have magus registered as source: "directory" pointing to
905
- // a local dev checkout. This prevents Claude Code's auto-update from
906
- // refreshing the marketplace, causing "plugin not found" errors for
907
- // newly added plugins. Re-register as "github" with the canonical repo.
908
- if (entry.source?.source === "directory") {
909
- const defaultMp = defaultMarketplaces.find((m) => m.name === name);
910
- if (defaultMp?.source.repo) {
911
- const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
912
- entry.source = {
913
- source: "github",
914
- repo: defaultMp.source.repo,
915
- };
916
- entry.installLocation = path.join(marketplacesDir, name);
917
- result.reregistered.push(name);
918
- }
919
- }
920
- // Enable autoUpdate if not set - only for Magus (MadAppGang) marketplaces
921
- if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
922
- entry.autoUpdate = true;
923
- result.enabledAutoUpdate.push(name);
924
- }
925
- updatedKnown[name] = entry;
926
- }
927
- // Write back if any changes were made
928
- if (result.enabledAutoUpdate.length > 0 ||
929
- result.removed.length > 0 ||
930
- result.reregistered.length > 0) {
931
- await writeKnownMarketplaces(updatedKnown);
932
- }
933
- return result;
934
- }
935
- /**
936
- * Clean up extraKnownMarketplaces entries that are now properly registered
937
- * in known_marketplaces.json (the official Claude Code marketplace registry).
938
- *
939
- * extraKnownMarketplaces was the old claudeup install mechanism. The official
940
- * way is `claude plugin marketplace add` which writes to known_marketplaces.json.
941
- * extraKnownMarketplaces should only be used in project settings for team
942
- * marketplace suggestions — not in user settings as a primary install path.
943
- *
944
- * This function removes magus-related entries from extraKnownMarketplaces in
945
- * both user and project settings when they're already in known_marketplaces.json.
946
- */
947
- export async function cleanupExtraKnownMarketplaces(projectPath) {
948
- const known = await readKnownMarketplaces();
949
- const cleaned = [];
950
- // Clean user-level settings
951
- try {
952
- const globalSettings = await readGlobalSettings();
953
- if (globalSettings.extraKnownMarketplaces) {
954
- let changed = false;
955
- for (const name of Object.keys(globalSettings.extraKnownMarketplaces)) {
956
- if (known[name]) {
957
- delete globalSettings.extraKnownMarketplaces[name];
958
- cleaned.push(`user:${name}`);
959
- changed = true;
960
- }
961
- }
962
- // Remove the field entirely if empty
963
- if (changed &&
964
- Object.keys(globalSettings.extraKnownMarketplaces).length === 0) {
965
- delete globalSettings.extraKnownMarketplaces;
966
- }
967
- if (changed) {
968
- await writeGlobalSettings(globalSettings);
969
- }
970
- }
971
- }
972
- catch {
973
- // Non-fatal: user settings cleanup is best-effort
974
- }
975
- // Clean project-level settings
976
- if (projectPath) {
977
- try {
978
- const projectSettings = await readSettings(projectPath);
979
- if (projectSettings.extraKnownMarketplaces) {
980
- let changed = false;
981
- for (const name of Object.keys(projectSettings.extraKnownMarketplaces)) {
982
- if (known[name]) {
983
- delete projectSettings.extraKnownMarketplaces[name];
984
- cleaned.push(`project:${name}`);
985
- changed = true;
986
- }
987
- }
988
- if (changed &&
989
- Object.keys(projectSettings.extraKnownMarketplaces).length === 0) {
990
- delete projectSettings.extraKnownMarketplaces;
991
- }
992
- if (changed) {
993
- await writeSettings(projectSettings, projectPath);
994
- }
995
- }
996
- }
997
- catch {
998
- // Non-fatal: project settings cleanup is best-effort
999
- }
1000
- }
1001
- return cleaned;
1002
- }
1003
- /**
1004
- * Read known_marketplaces.json to get marketplace source info
1005
- */
1006
- async function readKnownMarketplaces() {
1007
- try {
1008
- if (await fs.pathExists(KNOWN_MARKETPLACES_FILE)) {
1009
- return await fs.readJson(KNOWN_MARKETPLACES_FILE);
1010
- }
1011
- }
1012
- catch {
1013
- // Return empty if can't read
1014
- }
1015
- return {};
1016
- }
1017
- /**
1018
- * Check whether a marketplace is registered locally. A marketplace is
1019
- * considered registered when it has an entry in known_marketplaces.json
1020
- * AND its clone directory exists on disk. Either condition alone is not
1021
- * enough: an entry with a missing directory means the clone was lost; a
1022
- * directory without an entry means Claude Code doesn't know about it.
1023
- */
1024
- export async function isMarketplaceRegistered(name) {
1025
- const known = await readKnownMarketplaces();
1026
- if (!known[name])
1027
- return false;
1028
- const marketplacesDir = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
1029
- return fs.pathExists(path.join(marketplacesDir, name));
1030
- }
1031
- /**
1032
- * Get the source path for a plugin from its marketplace
1033
- * For directory-based marketplaces, returns the local directory path
1034
- * For GitHub marketplaces, returns the cloned repo path in ~/.claude/plugins/marketplaces/
1035
- */
1036
- async function getPluginSourcePath(pluginName, marketplace) {
1037
- const known = await readKnownMarketplaces();
1038
- const mpEntry = known[marketplace];
1039
- if (!mpEntry) {
1040
- return null;
1041
- }
1042
- let basePath;
1043
- if (mpEntry.source.source === "directory" && mpEntry.source.path) {
1044
- // Directory-based marketplace - use the source path directly
1045
- basePath = mpEntry.source.path;
1046
- }
1047
- else {
1048
- // GitHub-based marketplace - use installLocation (cloned repo path)
1049
- basePath = mpEntry.installLocation;
1050
- }
1051
- // Look for plugin in standard locations
1052
- const possiblePaths = [
1053
- path.join(basePath, "plugins", pluginName),
1054
- path.join(basePath, pluginName),
1055
- ];
1056
- for (const pluginPath of possiblePaths) {
1057
- if (await fs.pathExists(pluginPath)) {
1058
- return pluginPath;
1059
- }
1060
- }
1061
- return null;
1062
- }
1063
- /**
1064
- * Copy plugin files from source to cache
1065
- * This ensures the cache is populated with the latest plugin version
1066
- */
1067
- async function copyPluginToCache(pluginId, version, marketplace) {
1068
- const { pluginName } = parsePluginId(pluginId) || {
1069
- pluginName: pluginId.split("@")[0],
1070
- };
1071
- const sourcePath = await getPluginSourcePath(pluginName, marketplace);
1072
- if (!sourcePath) {
1073
- return false;
1074
- }
1075
- const cachePath = getPluginCachePath(pluginId, version, marketplace);
1076
- try {
1077
- // Remove existing cache directory if it exists
1078
- if (await fs.pathExists(cachePath)) {
1079
- await fs.remove(cachePath);
1080
- }
1081
- // Copy plugin files to cache
1082
- await fs.copy(sourcePath, cachePath, {
1083
- overwrite: true,
1084
- errorOnExist: false,
1085
- });
1086
- return true;
1087
- }
1088
- catch (error) {
1089
- console.warn(`Failed to copy plugin ${pluginId} to cache:`, error instanceof Error ? error.message : "Unknown error");
1090
- return false;
1091
- }
1092
- }
1093
- /**
1094
- * Read installed_plugins.json registry
1095
- */
1096
- export async function readInstalledPluginsRegistry() {
1097
- try {
1098
- if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
1099
- const content = await fs.readJson(INSTALLED_PLUGINS_FILE);
1100
- // Validate structure
1101
- if (!content.version || !content.plugins) {
1102
- throw new Error("Invalid registry structure");
1103
- }
1104
- return content;
1105
- }
1106
- }
1107
- catch (error) {
1108
- // Backup corrupted file
1109
- if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
1110
- try {
1111
- const backup = `${INSTALLED_PLUGINS_FILE}.backup.${Date.now()}`;
1112
- await fs.copy(INSTALLED_PLUGINS_FILE, backup);
1113
- console.warn(`Corrupted registry backed up to: ${backup}`);
1114
- }
1115
- catch {
1116
- // Ignore backup errors
1117
- }
1118
- }
1119
- }
1120
- return { version: 2, plugins: {} };
1121
- }
1122
- /**
1123
- * Write installed_plugins.json registry
1124
- */
1125
- export async function writeInstalledPluginsRegistry(registry) {
1126
- await fs.ensureDir(path.dirname(INSTALLED_PLUGINS_FILE));
1127
- await fs.writeJson(INSTALLED_PLUGINS_FILE, registry, { spaces: 2 });
1128
- }
1129
- /**
1130
- * Get install path for a plugin version in cache
1131
- */
1132
- function getPluginCachePath(pluginId, version, marketplace) {
1133
- const { pluginName } = parsePluginId(pluginId) || {
1134
- pluginName: pluginId.split("@")[0],
1135
- };
1136
- return path.join(os.homedir(), ".claude", "plugins", "cache", marketplace, pluginName, version);
1137
- }
1138
- /**
1139
- * Update installed_plugins.json when a plugin is installed/updated
1140
- * Also copies plugin files from source to cache to ensure latest version is available
1141
- */
1142
- export async function updateInstalledPluginsRegistry(pluginId, version, scope, projectPath) {
1143
- try {
1144
- const registry = await readInstalledPluginsRegistry();
1145
- // Get marketplace from plugin ID
1146
- const parsed = parsePluginId(pluginId);
1147
- if (!parsed) {
1148
- console.warn(`Invalid plugin ID: ${pluginId}, skipping registry update`);
1149
- return;
1150
- }
1151
- const { marketplace } = parsed;
1152
- // Copy plugin files from source to cache
1153
- // This ensures the cache has the latest plugin version
1154
- await copyPluginToCache(pluginId, version, marketplace);
1155
- const installPath = getPluginCachePath(pluginId, version, marketplace);
1156
- const now = new Date().toISOString();
1157
- // Initialize plugin array if it doesn't exist
1158
- if (!registry.plugins[pluginId]) {
1159
- registry.plugins[pluginId] = [];
1160
- }
1161
- // Find existing entry for this scope and project
1162
- const existingIndex = registry.plugins[pluginId].findIndex((entry) => {
1163
- if (entry.scope !== scope)
1164
- return false;
1165
- if (scope === "user")
1166
- return true;
1167
- return entry.projectPath === projectPath;
1168
- });
1169
- const entry = {
1170
- scope,
1171
- projectPath,
1172
- installPath,
1173
- version,
1174
- installedAt: existingIndex >= 0
1175
- ? registry.plugins[pluginId][existingIndex].installedAt
1176
- : now,
1177
- lastUpdated: now,
1178
- gitCommitSha: existingIndex >= 0
1179
- ? registry.plugins[pluginId][existingIndex].gitCommitSha
1180
- : undefined,
1181
- };
1182
- if (existingIndex >= 0) {
1183
- // Update existing entry
1184
- registry.plugins[pluginId][existingIndex] = entry;
1185
- }
1186
- else {
1187
- // Add new entry
1188
- registry.plugins[pluginId].push(entry);
1189
- }
1190
- await writeInstalledPluginsRegistry(registry);
1191
- }
1192
- catch (error) {
1193
- // Log warning but don't block plugin operation
1194
- console.warn(`Failed to update registry for ${pluginId}:`, error instanceof Error ? error.message : "Unknown error");
1195
- }
1196
- }
1197
- /**
1198
- * Remove plugin from installed_plugins.json registry
1199
- */
1200
- export async function removeFromInstalledPluginsRegistry(pluginId, scope, projectPath) {
1201
- try {
1202
- const registry = await readInstalledPluginsRegistry();
1203
- if (!registry.plugins[pluginId])
1204
- return;
1205
- // Remove entry matching scope and projectPath
1206
- registry.plugins[pluginId] = registry.plugins[pluginId].filter((entry) => {
1207
- if (entry.scope !== scope)
1208
- return true;
1209
- if (scope === "user")
1210
- return false;
1211
- return entry.projectPath !== projectPath;
1212
- });
1213
- // Remove plugin key if no entries remain
1214
- if (registry.plugins[pluginId].length === 0) {
1215
- delete registry.plugins[pluginId];
1216
- }
1217
- await writeInstalledPluginsRegistry(registry);
1218
- }
1219
- catch (error) {
1220
- // Log warning but don't block plugin operation
1221
- console.warn(`Failed to remove from registry for ${pluginId}:`, error instanceof Error ? error.message : "Unknown error");
1222
- }
1223
- }