claudeup 1.8.0 → 3.3.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 (301) hide show
  1. package/bin/claudeup.js +20 -2
  2. package/package.json +10 -19
  3. package/src/data/cli-tools.js +123 -0
  4. package/src/data/cli-tools.ts +140 -0
  5. package/{dist → src}/data/marketplaces.js +23 -24
  6. package/src/data/marketplaces.ts +95 -0
  7. package/src/data/mcp-servers.js +509 -0
  8. package/src/data/mcp-servers.ts +526 -0
  9. package/src/data/statuslines.js +159 -0
  10. package/src/data/statuslines.ts +188 -0
  11. package/src/index.js +4 -0
  12. package/src/index.ts +5 -0
  13. package/{dist → src}/main.js +46 -47
  14. package/src/main.tsx +145 -0
  15. package/src/opentui.d.ts +191 -0
  16. package/{dist → src}/prerunner/index.js +31 -41
  17. package/src/prerunner/index.ts +124 -0
  18. package/{dist → src}/services/claude-runner.js +9 -10
  19. package/src/services/claude-runner.ts +31 -0
  20. package/{dist → src}/services/claude-settings.js +90 -36
  21. package/src/services/claude-settings.ts +946 -0
  22. package/src/services/local-marketplace.js +339 -0
  23. package/src/services/local-marketplace.ts +489 -0
  24. package/{dist → src}/services/mcp-registry.js +13 -14
  25. package/src/services/mcp-registry.ts +105 -0
  26. package/{dist → src}/services/plugin-manager.js +65 -14
  27. package/src/services/plugin-manager.ts +696 -0
  28. package/{dist → src}/services/plugin-mcp-config.js +19 -18
  29. package/src/services/plugin-mcp-config.ts +242 -0
  30. package/{dist → src}/services/update-cache.js +0 -1
  31. package/src/services/update-cache.ts +78 -0
  32. package/{dist → src}/services/version-check.js +15 -14
  33. package/src/services/version-check.ts +122 -0
  34. package/src/types/index.js +1 -0
  35. package/src/types/index.ts +141 -0
  36. package/src/ui/App.js +213 -0
  37. package/src/ui/App.tsx +359 -0
  38. package/src/ui/components/CategoryHeader.js +9 -0
  39. package/src/ui/components/CategoryHeader.tsx +41 -0
  40. package/{dist → src}/ui/components/ScrollableList.js +19 -6
  41. package/src/ui/components/ScrollableList.tsx +98 -0
  42. package/src/ui/components/SearchInput.js +19 -0
  43. package/src/ui/components/SearchInput.tsx +56 -0
  44. package/src/ui/components/StyledText.js +39 -0
  45. package/src/ui/components/StyledText.tsx +70 -0
  46. package/src/ui/components/TabBar.js +38 -0
  47. package/src/ui/components/TabBar.tsx +88 -0
  48. package/src/ui/components/layout/Panel.js +6 -0
  49. package/src/ui/components/layout/Panel.tsx +62 -0
  50. package/src/ui/components/layout/ProgressBar.js +14 -0
  51. package/src/ui/components/layout/ProgressBar.tsx +47 -0
  52. package/src/ui/components/layout/ScopeTabs.js +6 -0
  53. package/src/ui/components/layout/ScopeTabs.tsx +53 -0
  54. package/src/ui/components/layout/ScreenLayout.js +21 -0
  55. package/src/ui/components/layout/ScreenLayout.tsx +147 -0
  56. package/src/ui/components/layout/index.js +4 -0
  57. package/src/ui/components/layout/index.ts +4 -0
  58. package/src/ui/components/modals/ConfirmModal.js +14 -0
  59. package/src/ui/components/modals/ConfirmModal.tsx +59 -0
  60. package/src/ui/components/modals/InputModal.js +16 -0
  61. package/src/ui/components/modals/InputModal.tsx +68 -0
  62. package/src/ui/components/modals/LoadingModal.js +14 -0
  63. package/src/ui/components/modals/LoadingModal.tsx +40 -0
  64. package/src/ui/components/modals/MessageModal.js +16 -0
  65. package/src/ui/components/modals/MessageModal.tsx +64 -0
  66. package/src/ui/components/modals/ModalContainer.js +56 -0
  67. package/src/ui/components/modals/ModalContainer.tsx +104 -0
  68. package/src/ui/components/modals/SelectModal.js +26 -0
  69. package/src/ui/components/modals/SelectModal.tsx +82 -0
  70. package/src/ui/components/modals/index.js +6 -0
  71. package/src/ui/components/modals/index.ts +6 -0
  72. package/src/ui/hooks/index.js +3 -0
  73. package/src/ui/hooks/index.ts +3 -0
  74. package/{dist → src}/ui/hooks/useAsyncData.js +21 -22
  75. package/src/ui/hooks/useAsyncData.ts +127 -0
  76. package/src/ui/hooks/useKeyboard.js +13 -0
  77. package/src/ui/hooks/useKeyboard.ts +26 -0
  78. package/src/ui/hooks/useKeyboardHandler.js +39 -0
  79. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  80. package/{dist → src}/ui/screens/CliToolsScreen.js +60 -54
  81. package/src/ui/screens/CliToolsScreen.tsx +468 -0
  82. package/src/ui/screens/EnvVarsScreen.js +154 -0
  83. package/src/ui/screens/EnvVarsScreen.tsx +269 -0
  84. package/{dist → src}/ui/screens/McpRegistryScreen.js +56 -55
  85. package/src/ui/screens/McpRegistryScreen.tsx +331 -0
  86. package/{dist → src}/ui/screens/McpScreen.js +46 -47
  87. package/src/ui/screens/McpScreen.tsx +392 -0
  88. package/src/ui/screens/ModelSelectorScreen.js +292 -0
  89. package/src/ui/screens/ModelSelectorScreen.tsx +441 -0
  90. package/{dist → src}/ui/screens/PluginsScreen.js +305 -293
  91. package/src/ui/screens/PluginsScreen.tsx +1231 -0
  92. package/src/ui/screens/StatusLineScreen.js +200 -0
  93. package/src/ui/screens/StatusLineScreen.tsx +411 -0
  94. package/src/ui/screens/index.js +7 -0
  95. package/src/ui/screens/index.ts +7 -0
  96. package/src/ui/state/AnimationContext.js +34 -0
  97. package/src/ui/state/AnimationContext.tsx +76 -0
  98. package/{dist → src}/ui/state/AppContext.js +31 -32
  99. package/src/ui/state/AppContext.tsx +235 -0
  100. package/{dist → src}/ui/state/DimensionsContext.js +16 -17
  101. package/src/ui/state/DimensionsContext.tsx +144 -0
  102. package/{dist → src}/ui/state/reducer.js +89 -90
  103. package/src/ui/state/reducer.ts +467 -0
  104. package/src/ui/state/types.js +1 -0
  105. package/src/ui/state/types.ts +273 -0
  106. package/{dist → src}/utils/command-utils.js +3 -4
  107. package/src/utils/command-utils.ts +20 -0
  108. package/{dist → src}/utils/fuzzy-search.js +2 -3
  109. package/src/utils/fuzzy-search.ts +138 -0
  110. package/{dist → src}/utils/string-utils.js +6 -6
  111. package/src/utils/string-utils.ts +88 -0
  112. package/dist/data/cli-tools.d.ts +0 -13
  113. package/dist/data/cli-tools.d.ts.map +0 -1
  114. package/dist/data/cli-tools.js +0 -124
  115. package/dist/data/cli-tools.js.map +0 -1
  116. package/dist/data/marketplaces.d.ts +0 -6
  117. package/dist/data/marketplaces.d.ts.map +0 -1
  118. package/dist/data/marketplaces.js.map +0 -1
  119. package/dist/data/mcp-servers.d.ts +0 -8
  120. package/dist/data/mcp-servers.d.ts.map +0 -1
  121. package/dist/data/mcp-servers.js +0 -503
  122. package/dist/data/mcp-servers.js.map +0 -1
  123. package/dist/data/statuslines.d.ts +0 -10
  124. package/dist/data/statuslines.d.ts.map +0 -1
  125. package/dist/data/statuslines.js +0 -160
  126. package/dist/data/statuslines.js.map +0 -1
  127. package/dist/index.d.ts +0 -3
  128. package/dist/index.d.ts.map +0 -1
  129. package/dist/index.js +0 -90
  130. package/dist/index.js.map +0 -1
  131. package/dist/main.d.ts +0 -3
  132. package/dist/main.d.ts.map +0 -1
  133. package/dist/main.js.map +0 -1
  134. package/dist/prerunner/index.d.ts +0 -7
  135. package/dist/prerunner/index.d.ts.map +0 -1
  136. package/dist/prerunner/index.js.map +0 -1
  137. package/dist/services/claude-runner.d.ts +0 -7
  138. package/dist/services/claude-runner.d.ts.map +0 -1
  139. package/dist/services/claude-runner.js.map +0 -1
  140. package/dist/services/claude-settings.d.ts +0 -73
  141. package/dist/services/claude-settings.d.ts.map +0 -1
  142. package/dist/services/claude-settings.js.map +0 -1
  143. package/dist/services/local-marketplace.d.ts +0 -111
  144. package/dist/services/local-marketplace.d.ts.map +0 -1
  145. package/dist/services/local-marketplace.js +0 -599
  146. package/dist/services/local-marketplace.js.map +0 -1
  147. package/dist/services/mcp-registry.d.ts +0 -10
  148. package/dist/services/mcp-registry.d.ts.map +0 -1
  149. package/dist/services/mcp-registry.js.map +0 -1
  150. package/dist/services/plugin-manager.d.ts +0 -65
  151. package/dist/services/plugin-manager.d.ts.map +0 -1
  152. package/dist/services/plugin-manager.js.map +0 -1
  153. package/dist/services/plugin-mcp-config.d.ts +0 -52
  154. package/dist/services/plugin-mcp-config.d.ts.map +0 -1
  155. package/dist/services/plugin-mcp-config.js.map +0 -1
  156. package/dist/services/update-cache.d.ts +0 -21
  157. package/dist/services/update-cache.d.ts.map +0 -1
  158. package/dist/services/update-cache.js.map +0 -1
  159. package/dist/services/version-check.d.ts +0 -20
  160. package/dist/services/version-check.d.ts.map +0 -1
  161. package/dist/services/version-check.js.map +0 -1
  162. package/dist/types/index.d.ts +0 -105
  163. package/dist/types/index.d.ts.map +0 -1
  164. package/dist/types/index.js +0 -2
  165. package/dist/types/index.js.map +0 -1
  166. package/dist/ui/InkApp.d.ts +0 -5
  167. package/dist/ui/InkApp.d.ts.map +0 -1
  168. package/dist/ui/InkApp.js +0 -188
  169. package/dist/ui/InkApp.js.map +0 -1
  170. package/dist/ui/components/CategoryHeader.d.ts +0 -16
  171. package/dist/ui/components/CategoryHeader.d.ts.map +0 -1
  172. package/dist/ui/components/CategoryHeader.js +0 -11
  173. package/dist/ui/components/CategoryHeader.js.map +0 -1
  174. package/dist/ui/components/ScrollableList.d.ts +0 -16
  175. package/dist/ui/components/ScrollableList.d.ts.map +0 -1
  176. package/dist/ui/components/ScrollableList.js.map +0 -1
  177. package/dist/ui/components/SearchInput.d.ts +0 -18
  178. package/dist/ui/components/SearchInput.d.ts.map +0 -1
  179. package/dist/ui/components/SearchInput.js +0 -30
  180. package/dist/ui/components/SearchInput.js.map +0 -1
  181. package/dist/ui/components/TabBar.d.ts +0 -8
  182. package/dist/ui/components/TabBar.d.ts.map +0 -1
  183. package/dist/ui/components/TabBar.js +0 -18
  184. package/dist/ui/components/TabBar.js.map +0 -1
  185. package/dist/ui/components/layout/Footer.d.ts +0 -14
  186. package/dist/ui/components/layout/Footer.d.ts.map +0 -1
  187. package/dist/ui/components/layout/Footer.js +0 -23
  188. package/dist/ui/components/layout/Footer.js.map +0 -1
  189. package/dist/ui/components/layout/Header.d.ts +0 -4
  190. package/dist/ui/components/layout/Header.d.ts.map +0 -1
  191. package/dist/ui/components/layout/Header.js +0 -25
  192. package/dist/ui/components/layout/Header.js.map +0 -1
  193. package/dist/ui/components/layout/Panel.d.ts +0 -22
  194. package/dist/ui/components/layout/Panel.d.ts.map +0 -1
  195. package/dist/ui/components/layout/Panel.js +0 -8
  196. package/dist/ui/components/layout/Panel.js.map +0 -1
  197. package/dist/ui/components/layout/ProgressBar.d.ts +0 -12
  198. package/dist/ui/components/layout/ProgressBar.d.ts.map +0 -1
  199. package/dist/ui/components/layout/ProgressBar.js +0 -16
  200. package/dist/ui/components/layout/ProgressBar.js.map +0 -1
  201. package/dist/ui/components/layout/ScopeTabs.d.ts +0 -12
  202. package/dist/ui/components/layout/ScopeTabs.d.ts.map +0 -1
  203. package/dist/ui/components/layout/ScopeTabs.js +0 -8
  204. package/dist/ui/components/layout/ScopeTabs.js.map +0 -1
  205. package/dist/ui/components/layout/ScreenLayout.d.ts +0 -30
  206. package/dist/ui/components/layout/ScreenLayout.d.ts.map +0 -1
  207. package/dist/ui/components/layout/ScreenLayout.js +0 -23
  208. package/dist/ui/components/layout/ScreenLayout.js.map +0 -1
  209. package/dist/ui/components/layout/index.d.ts +0 -7
  210. package/dist/ui/components/layout/index.d.ts.map +0 -1
  211. package/dist/ui/components/layout/index.js +0 -7
  212. package/dist/ui/components/layout/index.js.map +0 -1
  213. package/dist/ui/components/modals/ConfirmModal.d.ts +0 -14
  214. package/dist/ui/components/modals/ConfirmModal.d.ts.map +0 -1
  215. package/dist/ui/components/modals/ConfirmModal.js +0 -15
  216. package/dist/ui/components/modals/ConfirmModal.js.map +0 -1
  217. package/dist/ui/components/modals/InputModal.d.ts +0 -16
  218. package/dist/ui/components/modals/InputModal.d.ts.map +0 -1
  219. package/dist/ui/components/modals/InputModal.js +0 -23
  220. package/dist/ui/components/modals/InputModal.js.map +0 -1
  221. package/dist/ui/components/modals/LoadingModal.d.ts +0 -8
  222. package/dist/ui/components/modals/LoadingModal.d.ts.map +0 -1
  223. package/dist/ui/components/modals/LoadingModal.js +0 -8
  224. package/dist/ui/components/modals/LoadingModal.js.map +0 -1
  225. package/dist/ui/components/modals/MessageModal.d.ts +0 -14
  226. package/dist/ui/components/modals/MessageModal.d.ts.map +0 -1
  227. package/dist/ui/components/modals/MessageModal.js +0 -17
  228. package/dist/ui/components/modals/MessageModal.js.map +0 -1
  229. package/dist/ui/components/modals/ModalContainer.d.ts +0 -7
  230. package/dist/ui/components/modals/ModalContainer.d.ts.map +0 -1
  231. package/dist/ui/components/modals/ModalContainer.js +0 -38
  232. package/dist/ui/components/modals/ModalContainer.js.map +0 -1
  233. package/dist/ui/components/modals/SelectModal.d.ts +0 -17
  234. package/dist/ui/components/modals/SelectModal.d.ts.map +0 -1
  235. package/dist/ui/components/modals/SelectModal.js +0 -33
  236. package/dist/ui/components/modals/SelectModal.js.map +0 -1
  237. package/dist/ui/components/modals/index.d.ts +0 -7
  238. package/dist/ui/components/modals/index.d.ts.map +0 -1
  239. package/dist/ui/components/modals/index.js +0 -7
  240. package/dist/ui/components/modals/index.js.map +0 -1
  241. package/dist/ui/hooks/index.d.ts +0 -3
  242. package/dist/ui/hooks/index.d.ts.map +0 -1
  243. package/dist/ui/hooks/index.js +0 -3
  244. package/dist/ui/hooks/index.js.map +0 -1
  245. package/dist/ui/hooks/useAsyncData.d.ts +0 -40
  246. package/dist/ui/hooks/useAsyncData.d.ts.map +0 -1
  247. package/dist/ui/hooks/useAsyncData.js.map +0 -1
  248. package/dist/ui/hooks/useKeyboardNavigation.d.ts +0 -27
  249. package/dist/ui/hooks/useKeyboardNavigation.d.ts.map +0 -1
  250. package/dist/ui/hooks/useKeyboardNavigation.js +0 -82
  251. package/dist/ui/hooks/useKeyboardNavigation.js.map +0 -1
  252. package/dist/ui/screens/CliToolsScreen.d.ts +0 -4
  253. package/dist/ui/screens/CliToolsScreen.d.ts.map +0 -1
  254. package/dist/ui/screens/CliToolsScreen.js.map +0 -1
  255. package/dist/ui/screens/EnvVarsScreen.d.ts +0 -4
  256. package/dist/ui/screens/EnvVarsScreen.d.ts.map +0 -1
  257. package/dist/ui/screens/EnvVarsScreen.js +0 -145
  258. package/dist/ui/screens/EnvVarsScreen.js.map +0 -1
  259. package/dist/ui/screens/McpRegistryScreen.d.ts +0 -4
  260. package/dist/ui/screens/McpRegistryScreen.d.ts.map +0 -1
  261. package/dist/ui/screens/McpRegistryScreen.js.map +0 -1
  262. package/dist/ui/screens/McpScreen.d.ts +0 -4
  263. package/dist/ui/screens/McpScreen.d.ts.map +0 -1
  264. package/dist/ui/screens/McpScreen.js.map +0 -1
  265. package/dist/ui/screens/ModelSelectorScreen.d.ts +0 -4
  266. package/dist/ui/screens/ModelSelectorScreen.d.ts.map +0 -1
  267. package/dist/ui/screens/ModelSelectorScreen.js +0 -143
  268. package/dist/ui/screens/ModelSelectorScreen.js.map +0 -1
  269. package/dist/ui/screens/PluginsScreen.d.ts +0 -4
  270. package/dist/ui/screens/PluginsScreen.d.ts.map +0 -1
  271. package/dist/ui/screens/PluginsScreen.js.map +0 -1
  272. package/dist/ui/screens/StatusLineScreen.d.ts +0 -4
  273. package/dist/ui/screens/StatusLineScreen.d.ts.map +0 -1
  274. package/dist/ui/screens/StatusLineScreen.js +0 -197
  275. package/dist/ui/screens/StatusLineScreen.js.map +0 -1
  276. package/dist/ui/screens/index.d.ts +0 -8
  277. package/dist/ui/screens/index.d.ts.map +0 -1
  278. package/dist/ui/screens/index.js +0 -8
  279. package/dist/ui/screens/index.js.map +0 -1
  280. package/dist/ui/state/AppContext.d.ts +0 -40
  281. package/dist/ui/state/AppContext.d.ts.map +0 -1
  282. package/dist/ui/state/AppContext.js.map +0 -1
  283. package/dist/ui/state/DimensionsContext.d.ts +0 -27
  284. package/dist/ui/state/DimensionsContext.d.ts.map +0 -1
  285. package/dist/ui/state/DimensionsContext.js.map +0 -1
  286. package/dist/ui/state/reducer.d.ts +0 -4
  287. package/dist/ui/state/reducer.d.ts.map +0 -1
  288. package/dist/ui/state/reducer.js.map +0 -1
  289. package/dist/ui/state/types.d.ts +0 -266
  290. package/dist/ui/state/types.d.ts.map +0 -1
  291. package/dist/ui/state/types.js +0 -2
  292. package/dist/ui/state/types.js.map +0 -1
  293. package/dist/utils/command-utils.d.ts +0 -8
  294. package/dist/utils/command-utils.d.ts.map +0 -1
  295. package/dist/utils/command-utils.js.map +0 -1
  296. package/dist/utils/fuzzy-search.d.ts +0 -33
  297. package/dist/utils/fuzzy-search.d.ts.map +0 -1
  298. package/dist/utils/fuzzy-search.js.map +0 -1
  299. package/dist/utils/string-utils.d.ts +0 -24
  300. package/dist/utils/string-utils.d.ts.map +0 -1
  301. package/dist/utils/string-utils.js.map +0 -1
@@ -0,0 +1,946 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import type {
5
+ ClaudeSettings,
6
+ ClaudeLocalSettings,
7
+ McpServerConfig,
8
+ MarketplaceSource,
9
+ DiscoveredMarketplace,
10
+ InstalledPluginsRegistry,
11
+ InstalledPluginEntry,
12
+ } from "../types/index.js";
13
+ import { parsePluginId } from "../utils/string-utils.js";
14
+
15
+ const CLAUDE_DIR = ".claude";
16
+ const SETTINGS_FILE = "settings.json";
17
+ const LOCAL_SETTINGS_FILE = "settings.local.json";
18
+ const MCP_CONFIG_FILE = ".mcp.json";
19
+
20
+ // MCP config file types
21
+ interface McpConfigFile {
22
+ mcpServers?: Record<string, McpServerConfig>;
23
+ }
24
+
25
+ export function getClaudeDir(projectPath?: string): string {
26
+ const base = projectPath || process.cwd();
27
+ return path.join(base, CLAUDE_DIR);
28
+ }
29
+
30
+ export function getGlobalClaudeDir(): string {
31
+ return path.join(os.homedir(), CLAUDE_DIR);
32
+ }
33
+
34
+ export async function ensureClaudeDir(projectPath?: string): Promise<string> {
35
+ const claudeDir = getClaudeDir(projectPath);
36
+ await fs.ensureDir(claudeDir);
37
+ return claudeDir;
38
+ }
39
+
40
+ export async function readSettings(
41
+ projectPath?: string,
42
+ ): Promise<ClaudeSettings> {
43
+ const settingsPath = path.join(getClaudeDir(projectPath), SETTINGS_FILE);
44
+ try {
45
+ if (await fs.pathExists(settingsPath)) {
46
+ return await fs.readJson(settingsPath);
47
+ }
48
+ } catch {
49
+ // Return empty settings on error
50
+ }
51
+ return {};
52
+ }
53
+
54
+ export async function writeSettings(
55
+ settings: ClaudeSettings,
56
+ projectPath?: string,
57
+ ): Promise<void> {
58
+ const claudeDir = await ensureClaudeDir(projectPath);
59
+ const settingsPath = path.join(claudeDir, SETTINGS_FILE);
60
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
61
+ }
62
+
63
+ export async function readLocalSettings(
64
+ projectPath?: string,
65
+ ): Promise<ClaudeLocalSettings> {
66
+ const localPath = path.join(getClaudeDir(projectPath), LOCAL_SETTINGS_FILE);
67
+ try {
68
+ if (await fs.pathExists(localPath)) {
69
+ return await fs.readJson(localPath);
70
+ }
71
+ } catch {
72
+ // Return empty settings on error
73
+ }
74
+ return {};
75
+ }
76
+
77
+ export async function writeLocalSettings(
78
+ settings: ClaudeLocalSettings,
79
+ projectPath?: string,
80
+ ): Promise<void> {
81
+ const claudeDir = await ensureClaudeDir(projectPath);
82
+ const localPath = path.join(claudeDir, LOCAL_SETTINGS_FILE);
83
+ await fs.writeJson(localPath, settings, { spaces: 2 });
84
+ }
85
+
86
+ // MCP config file management (.mcp.json at project root)
87
+ export function getMcpConfigPath(projectPath?: string): string {
88
+ const base = projectPath || process.cwd();
89
+ return path.join(base, MCP_CONFIG_FILE);
90
+ }
91
+
92
+ export async function readMcpConfig(
93
+ projectPath?: string,
94
+ ): Promise<McpConfigFile> {
95
+ const mcpPath = getMcpConfigPath(projectPath);
96
+ try {
97
+ if (await fs.pathExists(mcpPath)) {
98
+ return await fs.readJson(mcpPath);
99
+ }
100
+ } catch {
101
+ // Return empty config on error
102
+ }
103
+ return {};
104
+ }
105
+
106
+ export async function writeMcpConfig(
107
+ config: McpConfigFile,
108
+ projectPath?: string,
109
+ ): Promise<void> {
110
+ const mcpPath = getMcpConfigPath(projectPath);
111
+ await fs.writeJson(mcpPath, config, { spaces: 2 });
112
+ }
113
+
114
+ export async function readGlobalSettings(): Promise<ClaudeSettings> {
115
+ const settingsPath = path.join(getGlobalClaudeDir(), SETTINGS_FILE);
116
+ try {
117
+ if (await fs.pathExists(settingsPath)) {
118
+ return await fs.readJson(settingsPath);
119
+ }
120
+ } catch {
121
+ // Return empty settings on error
122
+ }
123
+ return {};
124
+ }
125
+
126
+ export async function writeGlobalSettings(
127
+ settings: ClaudeSettings,
128
+ ): Promise<void> {
129
+ await fs.ensureDir(getGlobalClaudeDir());
130
+ const settingsPath = path.join(getGlobalClaudeDir(), SETTINGS_FILE);
131
+ await fs.writeJson(settingsPath, settings, { spaces: 2 });
132
+ }
133
+
134
+ // MCP Server management (writes to .mcp.json at project root)
135
+ export async function addMcpServer(
136
+ name: string,
137
+ config: McpServerConfig,
138
+ projectPath?: string,
139
+ ): Promise<void> {
140
+ // Extract env vars from config - they go to settings.local.json, not .mcp.json
141
+ const envVars = config.env || {};
142
+ const configWithoutEnv: McpServerConfig = { ...config };
143
+ delete configWithoutEnv.env;
144
+
145
+ // Add to .mcp.json (without env vars)
146
+ const mcpConfig = await readMcpConfig(projectPath);
147
+ mcpConfig.mcpServers = mcpConfig.mcpServers || {};
148
+ mcpConfig.mcpServers[name] = configWithoutEnv;
149
+ await writeMcpConfig(mcpConfig, projectPath);
150
+
151
+ // Enable in settings.local.json and add env vars
152
+ const localSettings = await readLocalSettings(projectPath);
153
+ const enabledServers = localSettings.enabledMcpjsonServers || [];
154
+ if (!enabledServers.includes(name)) {
155
+ enabledServers.push(name);
156
+ }
157
+ localSettings.enabledMcpjsonServers = enabledServers;
158
+ localSettings.enableAllProjectMcpServers = true;
159
+
160
+ // Add env vars to settings.local.json
161
+ if (Object.keys(envVars).length > 0) {
162
+ localSettings.env = localSettings.env || {};
163
+ for (const [key, value] of Object.entries(envVars)) {
164
+ // Only add non-reference values (references like ${VAR} don't need to be stored)
165
+ if (!value.startsWith("${") || !value.endsWith("}")) {
166
+ localSettings.env[key] = value;
167
+ }
168
+ }
169
+ }
170
+
171
+ await writeLocalSettings(localSettings, projectPath);
172
+ }
173
+
174
+ export async function removeMcpServer(
175
+ name: string,
176
+ projectPath?: string,
177
+ ): Promise<void> {
178
+ // Remove from .mcp.json
179
+ const mcpConfig = await readMcpConfig(projectPath);
180
+ if (mcpConfig.mcpServers) {
181
+ delete mcpConfig.mcpServers[name];
182
+ }
183
+ await writeMcpConfig(mcpConfig, projectPath);
184
+
185
+ // Remove from settings.local.json
186
+ const localSettings = await readLocalSettings(projectPath);
187
+ if (localSettings.enabledMcpjsonServers) {
188
+ localSettings.enabledMcpjsonServers =
189
+ localSettings.enabledMcpjsonServers.filter((s) => s !== name);
190
+ await writeLocalSettings(localSettings, projectPath);
191
+ }
192
+ }
193
+
194
+ export async function toggleMcpServer(
195
+ name: string,
196
+ enabled: boolean,
197
+ projectPath?: string,
198
+ ): Promise<void> {
199
+ // Toggle is now a remove operation since .mcp.json doesn't have enabled/disabled state
200
+ // If disabled, remove from config; if enabled, the server should already be in config
201
+ if (!enabled) {
202
+ await removeMcpServer(name, projectPath);
203
+ }
204
+ // If enabling, the server should already exist in the config
205
+ }
206
+
207
+ export async function setAllowMcp(
208
+ _allow: boolean,
209
+ _projectPath?: string,
210
+ ): Promise<void> {
211
+ // .mcp.json doesn't have an allowMcp setting - servers are either in the file or not
212
+ // This function is kept for API compatibility but is now a no-op
213
+ }
214
+
215
+ // Marketplace management - READ ONLY
216
+ // Use Claude Code CLI commands to add/remove marketplaces:
217
+ // claude marketplace add owner/repo
218
+ // claude marketplace remove name
219
+
220
+ // Plugin management
221
+ export async function enablePlugin(
222
+ pluginId: string,
223
+ enabled: boolean,
224
+ projectPath?: string,
225
+ ): Promise<void> {
226
+ const settings = await readSettings(projectPath);
227
+ settings.enabledPlugins = settings.enabledPlugins || {};
228
+ if (enabled) {
229
+ settings.enabledPlugins[pluginId] = true;
230
+ } else {
231
+ delete settings.enabledPlugins[pluginId];
232
+ }
233
+ await writeSettings(settings, projectPath);
234
+ }
235
+
236
+ export async function getEnabledPlugins(
237
+ projectPath?: string,
238
+ ): Promise<Record<string, boolean>> {
239
+ const settings = await readSettings(projectPath);
240
+ return settings.enabledPlugins || {};
241
+ }
242
+
243
+ export async function getLocalEnabledPlugins(
244
+ projectPath?: string,
245
+ ): Promise<Record<string, boolean>> {
246
+ const settings = await readLocalSettings(projectPath);
247
+ return settings.enabledPlugins || {};
248
+ }
249
+
250
+ export async function getLocalInstalledPluginVersions(
251
+ projectPath?: string,
252
+ ): Promise<Record<string, string>> {
253
+ const settings = await readLocalSettings(projectPath);
254
+ return settings.installedPluginVersions || {};
255
+ }
256
+
257
+ // Local plugin management (writes to settings.local.json)
258
+ export async function enableLocalPlugin(
259
+ pluginId: string,
260
+ enabled: boolean,
261
+ projectPath?: string,
262
+ ): Promise<void> {
263
+ const settings = await readLocalSettings(projectPath);
264
+ settings.enabledPlugins = settings.enabledPlugins || {};
265
+ if (enabled) {
266
+ settings.enabledPlugins[pluginId] = true;
267
+ } else {
268
+ delete settings.enabledPlugins[pluginId];
269
+ }
270
+ await writeLocalSettings(settings, projectPath);
271
+ }
272
+
273
+ export async function saveLocalInstalledPluginVersion(
274
+ pluginId: string,
275
+ version: string,
276
+ projectPath?: string,
277
+ ): Promise<void> {
278
+ const settings = await readLocalSettings(projectPath);
279
+ settings.installedPluginVersions = settings.installedPluginVersions || {};
280
+ settings.installedPluginVersions[pluginId] = version;
281
+ await writeLocalSettings(settings, projectPath);
282
+
283
+ // Update registry for local scope
284
+ await updateInstalledPluginsRegistry(
285
+ pluginId,
286
+ version,
287
+ "local",
288
+ projectPath ? path.resolve(projectPath) : undefined,
289
+ );
290
+ }
291
+
292
+ export async function removeLocalInstalledPluginVersion(
293
+ pluginId: string,
294
+ projectPath?: string,
295
+ ): Promise<void> {
296
+ const settings = await readLocalSettings(projectPath);
297
+ if (settings.installedPluginVersions) {
298
+ delete settings.installedPluginVersions[pluginId];
299
+ }
300
+ if (settings.enabledPlugins) {
301
+ delete settings.enabledPlugins[pluginId];
302
+ }
303
+ await writeLocalSettings(settings, projectPath);
304
+
305
+ // Remove from registry for local scope
306
+ await removeFromInstalledPluginsRegistry(
307
+ pluginId,
308
+ "local",
309
+ projectPath ? path.resolve(projectPath) : undefined,
310
+ );
311
+ }
312
+
313
+ // Status line management
314
+ export async function setStatusLine(
315
+ template: string,
316
+ projectPath?: string,
317
+ ): Promise<void> {
318
+ const settings = await readSettings(projectPath);
319
+ settings.statusLine = template;
320
+ await writeSettings(settings, projectPath);
321
+ }
322
+
323
+ export async function getStatusLine(
324
+ projectPath?: string,
325
+ ): Promise<string | undefined> {
326
+ const settings = await readSettings(projectPath);
327
+ return settings.statusLine;
328
+ }
329
+
330
+ // Global status line management
331
+ export async function setGlobalStatusLine(template: string): Promise<void> {
332
+ const settings = await readGlobalSettings();
333
+ settings.statusLine = template;
334
+ await writeGlobalSettings(settings);
335
+ }
336
+
337
+ export async function getGlobalStatusLine(): Promise<string | undefined> {
338
+ const settings = await readGlobalSettings();
339
+ return settings.statusLine;
340
+ }
341
+
342
+ // Get effective status line (project overrides global)
343
+ export async function getEffectiveStatusLine(projectPath?: string): Promise<{
344
+ template: string | undefined;
345
+ source: "project" | "global" | "default";
346
+ }> {
347
+ const projectStatusLine = await getStatusLine(projectPath);
348
+ if (projectStatusLine) {
349
+ return { template: projectStatusLine, source: "project" };
350
+ }
351
+
352
+ const globalStatusLine = await getGlobalStatusLine();
353
+ if (globalStatusLine) {
354
+ return { template: globalStatusLine, source: "global" };
355
+ }
356
+
357
+ return { template: undefined, source: "default" };
358
+ }
359
+
360
+ // Check if .claude directory exists
361
+ export async function hasClaudeDir(projectPath?: string): Promise<boolean> {
362
+ return fs.pathExists(getClaudeDir(projectPath));
363
+ }
364
+
365
+ // Get installed MCP servers (from .mcp.json)
366
+ export async function getInstalledMcpServers(
367
+ projectPath?: string,
368
+ ): Promise<Record<string, McpServerConfig>> {
369
+ const mcpConfig = await readMcpConfig(projectPath);
370
+ return mcpConfig.mcpServers || {};
371
+ }
372
+
373
+ // Get env vars for MCP servers (from settings.local.json)
374
+ export async function getMcpEnvVars(
375
+ projectPath?: string,
376
+ ): Promise<Record<string, string>> {
377
+ const localSettings = await readLocalSettings(projectPath);
378
+ return localSettings.env || {};
379
+ }
380
+
381
+ // Set an env var for MCP servers (in settings.local.json)
382
+ export async function setMcpEnvVar(
383
+ name: string,
384
+ value: string,
385
+ projectPath?: string,
386
+ ): Promise<void> {
387
+ const localSettings = await readLocalSettings(projectPath);
388
+ localSettings.env = localSettings.env || {};
389
+ localSettings.env[name] = value;
390
+ await writeLocalSettings(localSettings, projectPath);
391
+ }
392
+
393
+ // Remove an env var (from settings.local.json)
394
+ export async function removeMcpEnvVar(
395
+ name: string,
396
+ projectPath?: string,
397
+ ): Promise<void> {
398
+ const localSettings = await readLocalSettings(projectPath);
399
+ if (localSettings.env) {
400
+ delete localSettings.env[name];
401
+ await writeLocalSettings(localSettings, projectPath);
402
+ }
403
+ }
404
+
405
+ // Get enabled MCP servers (all servers in .mcp.json are considered enabled)
406
+ export async function getEnabledMcpServers(
407
+ projectPath?: string,
408
+ ): Promise<Record<string, boolean>> {
409
+ const mcpConfig = await readMcpConfig(projectPath);
410
+ const servers = mcpConfig.mcpServers || {};
411
+ const enabled: Record<string, boolean> = {};
412
+ for (const name of Object.keys(servers)) {
413
+ enabled[name] = true;
414
+ }
415
+ return enabled;
416
+ }
417
+
418
+ // Get all configured marketplaces
419
+ export async function getConfiguredMarketplaces(
420
+ projectPath?: string,
421
+ ): Promise<Record<string, MarketplaceSource>> {
422
+ const settings = await readSettings(projectPath);
423
+ return settings.extraKnownMarketplaces || {};
424
+ }
425
+
426
+ // Global marketplace management - READ ONLY
427
+ // Marketplaces are managed via Claude Code's native system
428
+
429
+ export async function getGlobalConfiguredMarketplaces(): Promise<
430
+ Record<string, MarketplaceSource>
431
+ > {
432
+ const settings = await readGlobalSettings();
433
+ return settings.extraKnownMarketplaces || {};
434
+ }
435
+
436
+ // Global plugin management
437
+ export async function enableGlobalPlugin(
438
+ pluginId: string,
439
+ enabled: boolean,
440
+ ): Promise<void> {
441
+ const settings = await readGlobalSettings();
442
+ settings.enabledPlugins = settings.enabledPlugins || {};
443
+ if (enabled) {
444
+ settings.enabledPlugins[pluginId] = true;
445
+ } else {
446
+ delete settings.enabledPlugins[pluginId];
447
+ }
448
+ await writeGlobalSettings(settings);
449
+ }
450
+
451
+ export async function getGlobalEnabledPlugins(): Promise<
452
+ Record<string, boolean>
453
+ > {
454
+ const settings = await readGlobalSettings();
455
+ return settings.enabledPlugins || {};
456
+ }
457
+
458
+ export async function getGlobalInstalledPluginVersions(): Promise<
459
+ Record<string, string>
460
+ > {
461
+ const settings = await readGlobalSettings();
462
+ return settings.installedPluginVersions || {};
463
+ }
464
+
465
+ export async function saveGlobalInstalledPluginVersion(
466
+ pluginId: string,
467
+ version: string,
468
+ ): Promise<void> {
469
+ const settings = await readGlobalSettings();
470
+ settings.installedPluginVersions = settings.installedPluginVersions || {};
471
+ settings.installedPluginVersions[pluginId] = version;
472
+ await writeGlobalSettings(settings);
473
+
474
+ // Update registry for user scope
475
+ await updateInstalledPluginsRegistry(pluginId, version, "user");
476
+ }
477
+
478
+ export async function removeGlobalInstalledPluginVersion(
479
+ pluginId: string,
480
+ ): Promise<void> {
481
+ const settings = await readGlobalSettings();
482
+ if (settings.installedPluginVersions) {
483
+ delete settings.installedPluginVersions[pluginId];
484
+ }
485
+ if (settings.enabledPlugins) {
486
+ delete settings.enabledPlugins[pluginId];
487
+ }
488
+ await writeGlobalSettings(settings);
489
+
490
+ // Remove from registry for user scope
491
+ await removeFromInstalledPluginsRegistry(pluginId, "user");
492
+ }
493
+
494
+ // Shared logic for discovering marketplaces from settings
495
+ function discoverMarketplacesFromSettings(
496
+ settings: ClaudeSettings,
497
+ ): DiscoveredMarketplace[] {
498
+ const discovered = new Map<string, DiscoveredMarketplace>();
499
+
500
+ // 1. From extraKnownMarketplaces (explicitly configured)
501
+ for (const [name, config] of Object.entries(
502
+ settings.extraKnownMarketplaces || {},
503
+ )) {
504
+ discovered.set(name, { name, source: "configured", config });
505
+ }
506
+
507
+ // 2. From enabledPlugins (infer marketplace from plugin ID format: pluginName@marketplaceName)
508
+ for (const pluginId of Object.keys(settings.enabledPlugins || {})) {
509
+ const parsed = parsePluginId(pluginId);
510
+ if (parsed && !discovered.has(parsed.marketplace)) {
511
+ discovered.set(parsed.marketplace, {
512
+ name: parsed.marketplace,
513
+ source: "inferred",
514
+ });
515
+ }
516
+ }
517
+
518
+ // 3. From installedPluginVersions (same format)
519
+ for (const pluginId of Object.keys(settings.installedPluginVersions || {})) {
520
+ const parsed = parsePluginId(pluginId);
521
+ if (parsed && !discovered.has(parsed.marketplace)) {
522
+ discovered.set(parsed.marketplace, {
523
+ name: parsed.marketplace,
524
+ source: "inferred",
525
+ });
526
+ }
527
+ }
528
+
529
+ return Array.from(discovered.values());
530
+ }
531
+
532
+ // Discover all marketplaces from settings (configured + inferred from plugins)
533
+ export async function discoverAllMarketplaces(
534
+ projectPath?: string,
535
+ ): Promise<DiscoveredMarketplace[]> {
536
+ try {
537
+ const settings = await readSettings(projectPath);
538
+ return discoverMarketplacesFromSettings(settings);
539
+ } catch (error) {
540
+ // Graceful degradation - return empty array instead of crashing
541
+ console.error(
542
+ "Failed to discover project marketplaces:",
543
+ error instanceof Error ? error.message : "Unknown error",
544
+ );
545
+ return [];
546
+ }
547
+ }
548
+
549
+ // Discover all marketplaces from global settings
550
+ export async function discoverAllGlobalMarketplaces(): Promise<
551
+ DiscoveredMarketplace[]
552
+ > {
553
+ try {
554
+ const settings = await readGlobalSettings();
555
+ return discoverMarketplacesFromSettings(settings);
556
+ } catch (error) {
557
+ // Graceful degradation - return empty array instead of crashing
558
+ console.error(
559
+ "Failed to discover global marketplaces:",
560
+ error instanceof Error ? error.message : "Unknown error",
561
+ );
562
+ return [];
563
+ }
564
+ }
565
+
566
+ // installed_plugins.json registry management
567
+ const INSTALLED_PLUGINS_FILE = path.join(
568
+ os.homedir(),
569
+ ".claude",
570
+ "plugins",
571
+ "installed_plugins.json",
572
+ );
573
+
574
+ const KNOWN_MARKETPLACES_FILE = path.join(
575
+ os.homedir(),
576
+ ".claude",
577
+ "plugins",
578
+ "known_marketplaces.json",
579
+ );
580
+
581
+ interface KnownMarketplaceEntry {
582
+ source: { source: string; path?: string; repo?: string };
583
+ installLocation: string;
584
+ lastUpdated: string;
585
+ autoUpdate?: boolean;
586
+ }
587
+
588
+ type KnownMarketplaces = Record<string, KnownMarketplaceEntry>;
589
+
590
+ /**
591
+ * Write known_marketplaces.json
592
+ */
593
+ async function writeKnownMarketplaces(
594
+ marketplaces: KnownMarketplaces,
595
+ ): Promise<void> {
596
+ await fs.ensureDir(path.dirname(KNOWN_MARKETPLACES_FILE));
597
+ await fs.writeJson(KNOWN_MARKETPLACES_FILE, marketplaces, { spaces: 2 });
598
+ }
599
+
600
+ /**
601
+ * Set autoUpdate flag for a marketplace in known_marketplaces.json
602
+ * This is where Claude Code actually reads the autoUpdate setting
603
+ */
604
+ export async function setMarketplaceAutoUpdate(
605
+ marketplaceName: string,
606
+ autoUpdate: boolean,
607
+ ): Promise<boolean> {
608
+ const known = await readKnownMarketplaces();
609
+ if (known[marketplaceName]) {
610
+ known[marketplaceName].autoUpdate = autoUpdate;
611
+ await writeKnownMarketplaces(known);
612
+ return true;
613
+ }
614
+ return false; // Marketplace not yet installed by Claude Code
615
+ }
616
+
617
+ /**
618
+ * Get autoUpdate status for a marketplace
619
+ */
620
+ export async function getMarketplaceAutoUpdate(
621
+ marketplaceName: string,
622
+ ): Promise<boolean | undefined> {
623
+ const known = await readKnownMarketplaces();
624
+ return known[marketplaceName]?.autoUpdate;
625
+ }
626
+
627
+ export interface MarketplaceRecoveryResult {
628
+ enabledAutoUpdate: string[];
629
+ removed: string[];
630
+ }
631
+
632
+ /**
633
+ * Check if a marketplace is from MadAppGang
634
+ */
635
+ function isMadAppGangMarketplace(entry: KnownMarketplaceEntry): boolean {
636
+ const repo = entry.source?.repo?.toLowerCase() || "";
637
+ return repo.includes("madappgang");
638
+ }
639
+
640
+ /**
641
+ * Recover/sync marketplace settings:
642
+ * - Enable autoUpdate for MadAppGang marketplaces that don't have it set
643
+ * - Remove entries for marketplaces whose installLocation no longer exists
644
+ */
645
+ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryResult> {
646
+ const known = await readKnownMarketplaces();
647
+ const result: MarketplaceRecoveryResult = {
648
+ enabledAutoUpdate: [],
649
+ removed: [],
650
+ };
651
+
652
+ const updatedKnown: KnownMarketplaces = {};
653
+
654
+ for (const [name, entry] of Object.entries(known)) {
655
+ // Check if install location still exists
656
+ if (
657
+ entry.installLocation &&
658
+ !(await fs.pathExists(entry.installLocation))
659
+ ) {
660
+ result.removed.push(name);
661
+ continue;
662
+ }
663
+
664
+ // Enable autoUpdate if not set - only for MadAppGang marketplaces
665
+ if (entry.autoUpdate === undefined && isMadAppGangMarketplace(entry)) {
666
+ entry.autoUpdate = true;
667
+ result.enabledAutoUpdate.push(name);
668
+ }
669
+
670
+ updatedKnown[name] = entry;
671
+ }
672
+
673
+ // Write back if any changes were made
674
+ if (result.enabledAutoUpdate.length > 0 || result.removed.length > 0) {
675
+ await writeKnownMarketplaces(updatedKnown);
676
+ }
677
+
678
+ return result;
679
+ }
680
+
681
+ /**
682
+ * Read known_marketplaces.json to get marketplace source info
683
+ */
684
+ async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
685
+ try {
686
+ if (await fs.pathExists(KNOWN_MARKETPLACES_FILE)) {
687
+ return await fs.readJson(KNOWN_MARKETPLACES_FILE);
688
+ }
689
+ } catch {
690
+ // Return empty if can't read
691
+ }
692
+ return {};
693
+ }
694
+
695
+ /**
696
+ * Get the source path for a plugin from its marketplace
697
+ * For directory-based marketplaces, returns the local directory path
698
+ * For GitHub marketplaces, returns the cloned repo path in ~/.claude/plugins/marketplaces/
699
+ */
700
+ async function getPluginSourcePath(
701
+ pluginName: string,
702
+ marketplace: string,
703
+ ): Promise<string | null> {
704
+ const known = await readKnownMarketplaces();
705
+ const mpEntry = known[marketplace];
706
+
707
+ if (!mpEntry) {
708
+ return null;
709
+ }
710
+
711
+ let basePath: string;
712
+
713
+ if (mpEntry.source.source === "directory" && mpEntry.source.path) {
714
+ // Directory-based marketplace - use the source path directly
715
+ basePath = mpEntry.source.path;
716
+ } else {
717
+ // GitHub-based marketplace - use installLocation (cloned repo path)
718
+ basePath = mpEntry.installLocation;
719
+ }
720
+
721
+ // Look for plugin in standard locations
722
+ const possiblePaths = [
723
+ path.join(basePath, "plugins", pluginName),
724
+ path.join(basePath, pluginName),
725
+ ];
726
+
727
+ for (const pluginPath of possiblePaths) {
728
+ if (await fs.pathExists(pluginPath)) {
729
+ return pluginPath;
730
+ }
731
+ }
732
+
733
+ return null;
734
+ }
735
+
736
+ /**
737
+ * Copy plugin files from source to cache
738
+ * This ensures the cache is populated with the latest plugin version
739
+ */
740
+ async function copyPluginToCache(
741
+ pluginId: string,
742
+ version: string,
743
+ marketplace: string,
744
+ ): Promise<boolean> {
745
+ const { pluginName } = parsePluginId(pluginId) || {
746
+ pluginName: pluginId.split("@")[0],
747
+ };
748
+
749
+ const sourcePath = await getPluginSourcePath(pluginName, marketplace);
750
+ if (!sourcePath) {
751
+ return false;
752
+ }
753
+
754
+ const cachePath = getPluginCachePath(pluginId, version, marketplace);
755
+
756
+ try {
757
+ // Remove existing cache directory if it exists
758
+ if (await fs.pathExists(cachePath)) {
759
+ await fs.remove(cachePath);
760
+ }
761
+
762
+ // Copy plugin files to cache
763
+ await fs.copy(sourcePath, cachePath, {
764
+ overwrite: true,
765
+ errorOnExist: false,
766
+ });
767
+
768
+ return true;
769
+ } catch (error) {
770
+ console.warn(
771
+ `Failed to copy plugin ${pluginId} to cache:`,
772
+ error instanceof Error ? error.message : "Unknown error",
773
+ );
774
+ return false;
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Read installed_plugins.json registry
780
+ */
781
+ export async function readInstalledPluginsRegistry(): Promise<InstalledPluginsRegistry> {
782
+ try {
783
+ if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
784
+ const content = await fs.readJson(INSTALLED_PLUGINS_FILE);
785
+ // Validate structure
786
+ if (!content.version || !content.plugins) {
787
+ throw new Error("Invalid registry structure");
788
+ }
789
+ return content;
790
+ }
791
+ } catch (error) {
792
+ // Backup corrupted file
793
+ if (await fs.pathExists(INSTALLED_PLUGINS_FILE)) {
794
+ try {
795
+ const backup = `${INSTALLED_PLUGINS_FILE}.backup.${Date.now()}`;
796
+ await fs.copy(INSTALLED_PLUGINS_FILE, backup);
797
+ console.warn(`Corrupted registry backed up to: ${backup}`);
798
+ } catch {
799
+ // Ignore backup errors
800
+ }
801
+ }
802
+ }
803
+ return { version: 2, plugins: {} };
804
+ }
805
+
806
+ /**
807
+ * Write installed_plugins.json registry
808
+ */
809
+ export async function writeInstalledPluginsRegistry(
810
+ registry: InstalledPluginsRegistry,
811
+ ): Promise<void> {
812
+ await fs.ensureDir(path.dirname(INSTALLED_PLUGINS_FILE));
813
+ await fs.writeJson(INSTALLED_PLUGINS_FILE, registry, { spaces: 2 });
814
+ }
815
+
816
+ /**
817
+ * Get install path for a plugin version in cache
818
+ */
819
+ function getPluginCachePath(
820
+ pluginId: string,
821
+ version: string,
822
+ marketplace: string,
823
+ ): string {
824
+ const { pluginName } = parsePluginId(pluginId) || {
825
+ pluginName: pluginId.split("@")[0],
826
+ };
827
+ return path.join(
828
+ os.homedir(),
829
+ ".claude",
830
+ "plugins",
831
+ "cache",
832
+ marketplace,
833
+ pluginName,
834
+ version,
835
+ );
836
+ }
837
+
838
+ /**
839
+ * Update installed_plugins.json when a plugin is installed/updated
840
+ * Also copies plugin files from source to cache to ensure latest version is available
841
+ */
842
+ export async function updateInstalledPluginsRegistry(
843
+ pluginId: string,
844
+ version: string,
845
+ scope: "user" | "project" | "local",
846
+ projectPath?: string,
847
+ ): Promise<void> {
848
+ try {
849
+ const registry = await readInstalledPluginsRegistry();
850
+
851
+ // Get marketplace from plugin ID
852
+ const parsed = parsePluginId(pluginId);
853
+ if (!parsed) {
854
+ console.warn(`Invalid plugin ID: ${pluginId}, skipping registry update`);
855
+ return;
856
+ }
857
+
858
+ const { marketplace } = parsed;
859
+
860
+ // Copy plugin files from source to cache
861
+ // This ensures the cache has the latest plugin version
862
+ await copyPluginToCache(pluginId, version, marketplace);
863
+
864
+ const installPath = getPluginCachePath(pluginId, version, marketplace);
865
+ const now = new Date().toISOString();
866
+
867
+ // Initialize plugin array if it doesn't exist
868
+ if (!registry.plugins[pluginId]) {
869
+ registry.plugins[pluginId] = [];
870
+ }
871
+
872
+ // Find existing entry for this scope and project
873
+ const existingIndex = registry.plugins[pluginId].findIndex((entry) => {
874
+ if (entry.scope !== scope) return false;
875
+ if (scope === "user") return true;
876
+ return entry.projectPath === projectPath;
877
+ });
878
+
879
+ const entry: InstalledPluginEntry = {
880
+ scope,
881
+ projectPath,
882
+ installPath,
883
+ version,
884
+ installedAt:
885
+ existingIndex >= 0
886
+ ? registry.plugins[pluginId][existingIndex].installedAt
887
+ : now,
888
+ lastUpdated: now,
889
+ gitCommitSha:
890
+ existingIndex >= 0
891
+ ? registry.plugins[pluginId][existingIndex].gitCommitSha
892
+ : undefined,
893
+ };
894
+
895
+ if (existingIndex >= 0) {
896
+ // Update existing entry
897
+ registry.plugins[pluginId][existingIndex] = entry;
898
+ } else {
899
+ // Add new entry
900
+ registry.plugins[pluginId].push(entry);
901
+ }
902
+
903
+ await writeInstalledPluginsRegistry(registry);
904
+ } catch (error) {
905
+ // Log warning but don't block plugin operation
906
+ console.warn(
907
+ `Failed to update registry for ${pluginId}:`,
908
+ error instanceof Error ? error.message : "Unknown error",
909
+ );
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Remove plugin from installed_plugins.json registry
915
+ */
916
+ export async function removeFromInstalledPluginsRegistry(
917
+ pluginId: string,
918
+ scope: "user" | "project" | "local",
919
+ projectPath?: string,
920
+ ): Promise<void> {
921
+ try {
922
+ const registry = await readInstalledPluginsRegistry();
923
+
924
+ if (!registry.plugins[pluginId]) return;
925
+
926
+ // Remove entry matching scope and projectPath
927
+ registry.plugins[pluginId] = registry.plugins[pluginId].filter((entry) => {
928
+ if (entry.scope !== scope) return true;
929
+ if (scope === "user") return false;
930
+ return entry.projectPath !== projectPath;
931
+ });
932
+
933
+ // Remove plugin key if no entries remain
934
+ if (registry.plugins[pluginId].length === 0) {
935
+ delete registry.plugins[pluginId];
936
+ }
937
+
938
+ await writeInstalledPluginsRegistry(registry);
939
+ } catch (error) {
940
+ // Log warning but don't block plugin operation
941
+ console.warn(
942
+ `Failed to remove from registry for ${pluginId}:`,
943
+ error instanceof Error ? error.message : "Unknown error",
944
+ );
945
+ }
946
+ }