magus-cli 0.0.0-placeholder → 7.0.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 (331) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +684 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +445 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +174 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +335 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/moved-marketplace.test.ts +86 -0
  96. package/src/__tests__/open-file.test.ts +59 -0
  97. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  98. package/src/__tests__/plugin-checkout.test.ts +66 -0
  99. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  100. package/src/__tests__/plugin-contents.test.ts +371 -0
  101. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  102. package/src/__tests__/plugin-releases.test.ts +98 -0
  103. package/src/__tests__/plugin-requires.test.ts +187 -0
  104. package/src/__tests__/plugin-row-text.test.ts +336 -0
  105. package/src/__tests__/plugin-setup.test.ts +1016 -0
  106. package/src/__tests__/profile-adopt.test.ts +177 -0
  107. package/src/__tests__/profile-command.test.ts +110 -0
  108. package/src/__tests__/profile-materializer.test.ts +238 -0
  109. package/src/__tests__/profile-sync.test.ts +154 -0
  110. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  111. package/src/__tests__/resolve-executable.test.ts +42 -0
  112. package/src/__tests__/resolver.test.ts +351 -0
  113. package/src/__tests__/scope-action.test.ts +134 -0
  114. package/src/__tests__/scope-squares.test.tsx +164 -0
  115. package/src/__tests__/selection-scope.test.ts +100 -0
  116. package/src/__tests__/settings-file.test.ts +179 -0
  117. package/src/__tests__/shell-script-callers.test.ts +92 -0
  118. package/src/__tests__/skill-install-files.test.ts +136 -0
  119. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  120. package/src/__tests__/style-wrap.test.ts +220 -0
  121. package/src/__tests__/styles-manager.test.ts +1098 -0
  122. package/src/__tests__/styles-origins.test.ts +414 -0
  123. package/src/__tests__/styles-screen-state.test.ts +457 -0
  124. package/src/__tests__/styles-status-line.test.ts +72 -0
  125. package/src/__tests__/styles-sync.test.ts +452 -0
  126. package/src/__tests__/symlink-manager.test.ts +180 -0
  127. package/src/__tests__/tabbar-layout.test.ts +100 -0
  128. package/src/__tests__/terminology-filler.test.ts +214 -0
  129. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  130. package/src/__tests__/theme-env.test.ts +281 -0
  131. package/src/__tests__/theme-resolve.test.ts +380 -0
  132. package/src/__tests__/tool-probe.test.ts +1400 -0
  133. package/src/__tests__/toolchain.test.ts +232 -0
  134. package/src/__tests__/ui-version-writers.test.ts +88 -0
  135. package/src/__tests__/update-apply.test.ts +637 -0
  136. package/src/__tests__/update-plan.test.ts +1373 -0
  137. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  138. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  139. package/src/__tests__/version-check-refresh.test.ts +45 -0
  140. package/src/__tests__/version-snapshot.test.ts +212 -0
  141. package/src/__tests__/version-unknown-state.test.ts +388 -0
  142. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  143. package/src/cli/ansi.ts +520 -0
  144. package/src/cli/bootstrap.ts +134 -0
  145. package/src/cli/doctor.ts +218 -0
  146. package/src/cli/hook.ts +129 -0
  147. package/src/cli/install.ts +382 -0
  148. package/src/cli/live.ts +374 -0
  149. package/src/cli/models.ts +297 -0
  150. package/src/cli/profile.ts +197 -0
  151. package/src/cli/prompt.ts +32 -0
  152. package/src/cli/router.ts +207 -0
  153. package/src/cli/update-view.ts +526 -0
  154. package/src/cli/update.ts +613 -0
  155. package/src/cli/upgrade.ts +97 -0
  156. package/src/data/alias-flags.ts +301 -0
  157. package/src/data/cli-tools.ts +146 -0
  158. package/src/data/community-styles.ts +531 -0
  159. package/src/data/gitignore-defaults.ts +33 -0
  160. package/src/data/gitignore-reasons.ts +93 -0
  161. package/src/data/gitignore-templates.ts +25 -0
  162. package/src/data/marketplaces.ts +278 -0
  163. package/src/data/mcp-servers.ts +51 -0
  164. package/src/data/models-presets.ts +329 -0
  165. package/src/data/predefined-profiles.ts +252 -0
  166. package/src/data/settings-catalog.ts +714 -0
  167. package/src/data/skill-repos.ts +91 -0
  168. package/src/data/styles/asd-ste100.md +38 -0
  169. package/src/data/styles/calibrated.md +24 -0
  170. package/src/data/styles/direct.md +23 -0
  171. package/src/data/styles/evidence-first.md +22 -0
  172. package/src/data/styles/explanatory.md +23 -0
  173. package/src/data/styles/index.ts +57 -0
  174. package/src/data/styles/no-slop.md +32 -0
  175. package/src/data/styles/plain-language.md +23 -0
  176. package/src/data/styles/structured.md +30 -0
  177. package/src/data/styles/terminology.md +28 -0
  178. package/src/data/styles/terse.md +20 -0
  179. package/src/main.tsx +110 -0
  180. package/src/markdown.d.ts +13 -0
  181. package/src/opentui.d.ts +203 -0
  182. package/src/services/active-closure.ts +117 -0
  183. package/src/services/alias-settings.ts +60 -0
  184. package/src/services/alias-shell-writer.ts +1244 -0
  185. package/src/services/alias-store.ts +172 -0
  186. package/src/services/binary-signing.ts +78 -0
  187. package/src/services/catalog-cache-store.ts +316 -0
  188. package/src/services/claude-cli.ts +304 -0
  189. package/src/services/claude-runner.ts +31 -0
  190. package/src/services/claude-settings.ts +1589 -0
  191. package/src/services/cli-tool-commands.ts +133 -0
  192. package/src/services/community-fetcher.ts +90 -0
  193. package/src/services/community-styles.ts +1194 -0
  194. package/src/services/content-drift.ts +208 -0
  195. package/src/services/conventions-manager.ts +945 -0
  196. package/src/services/doctor-bins.ts +63 -0
  197. package/src/services/doctor.ts +570 -0
  198. package/src/services/dotenv.ts +181 -0
  199. package/src/services/git-worktree.ts +129 -0
  200. package/src/services/github-budget.ts +274 -0
  201. package/src/services/gitignore-check.ts +63 -0
  202. package/src/services/gitignore-detector.ts +167 -0
  203. package/src/services/gitignore-fixer.ts +280 -0
  204. package/src/services/gitignore-resolver.ts +208 -0
  205. package/src/services/gitignore-service.ts +167 -0
  206. package/src/services/hook-registration.ts +218 -0
  207. package/src/services/install-plan.ts +107 -0
  208. package/src/services/local-marketplace.ts +587 -0
  209. package/src/services/manifest.ts +250 -0
  210. package/src/services/marketplace-catalog-git.ts +173 -0
  211. package/src/services/marketplace-catalog.ts +129 -0
  212. package/src/services/marketplace-fetcher.ts +400 -0
  213. package/src/services/marketplace-refresh.ts +253 -0
  214. package/src/services/marketplace-sync.ts +145 -0
  215. package/src/services/mate-availability.ts +133 -0
  216. package/src/services/mate-catalog.ts +265 -0
  217. package/src/services/mcp-registry.ts +171 -0
  218. package/src/services/models-core.ts +969 -0
  219. package/src/services/models-manager.ts +606 -0
  220. package/src/services/plugin-checkout.ts +186 -0
  221. package/src/services/plugin-manager.ts +878 -0
  222. package/src/services/plugin-mcp-config.ts +242 -0
  223. package/src/services/plugin-releases.ts +47 -0
  224. package/src/services/plugin-requires.ts +227 -0
  225. package/src/services/plugin-setup.ts +965 -0
  226. package/src/services/profile-adopt.ts +206 -0
  227. package/src/services/profile-materializer.ts +133 -0
  228. package/src/services/profile-sync.ts +158 -0
  229. package/src/services/profiles.ts +225 -0
  230. package/src/services/resolver.ts +344 -0
  231. package/src/services/settings-file.ts +69 -0
  232. package/src/services/settings-manager.ts +307 -0
  233. package/src/services/skills-manager.ts +1111 -0
  234. package/src/services/skillsmp-client.ts +193 -0
  235. package/src/services/styles-manager.ts +1294 -0
  236. package/src/services/symlink-manager.ts +189 -0
  237. package/src/services/terminology-filler.ts +266 -0
  238. package/src/services/tool-probe.ts +1637 -0
  239. package/src/services/toolchain.ts +431 -0
  240. package/src/services/update-engine.ts +691 -0
  241. package/src/services/update-plan.ts +863 -0
  242. package/src/services/version-check.ts +127 -0
  243. package/src/services/version-snapshot.ts +189 -0
  244. package/src/tui.tsx +112 -0
  245. package/src/types/bun.d.ts +27 -0
  246. package/src/types/gitignore.ts +68 -0
  247. package/src/types/index.ts +447 -0
  248. package/src/ui/App.tsx +538 -0
  249. package/src/ui/adapters/catalogNotice.ts +122 -0
  250. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  251. package/src/ui/adapters/modelsAdapter.ts +213 -0
  252. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  253. package/src/ui/adapters/settingsAdapter.ts +168 -0
  254. package/src/ui/adapters/skillsAdapter.ts +229 -0
  255. package/src/ui/adapters/stylesAdapter.ts +403 -0
  256. package/src/ui/components/CategoryHeader.tsx +64 -0
  257. package/src/ui/components/EmptyFilterState.tsx +32 -0
  258. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  259. package/src/ui/components/ScopeIndicator.tsx +63 -0
  260. package/src/ui/components/ScrollableList.tsx +95 -0
  261. package/src/ui/components/SearchInput.tsx +57 -0
  262. package/src/ui/components/StyledText.tsx +75 -0
  263. package/src/ui/components/TabBar.tsx +101 -0
  264. package/src/ui/components/layout/FooterHints.tsx +78 -0
  265. package/src/ui/components/layout/Panel.tsx +63 -0
  266. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  267. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  268. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  269. package/src/ui/components/layout/index.ts +9 -0
  270. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  271. package/src/ui/components/modals/InputModal.tsx +75 -0
  272. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  273. package/src/ui/components/modals/MessageModal.tsx +66 -0
  274. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  275. package/src/ui/components/modals/SelectModal.tsx +90 -0
  276. package/src/ui/components/modals/index.ts +6 -0
  277. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  278. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  279. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  280. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  281. package/src/ui/components/primitives/MetaText.tsx +40 -0
  282. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  283. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  284. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  285. package/src/ui/components/primitives/index.ts +9 -0
  286. package/src/ui/hooks/index.ts +2 -0
  287. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  288. package/src/ui/hooks/useKeyboard.ts +24 -0
  289. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  290. package/src/ui/registry.ts +33 -0
  291. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  292. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  293. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  294. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  295. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  296. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  297. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  298. package/src/ui/renderers/pluginRowText.ts +70 -0
  299. package/src/ui/renderers/pluginStatusText.ts +56 -0
  300. package/src/ui/renderers/profileRenderers.tsx +406 -0
  301. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  302. package/src/ui/renderers/skillRenderers.tsx +479 -0
  303. package/src/ui/renderers/styleRenderers.tsx +813 -0
  304. package/src/ui/screens/AliasScreen.tsx +1558 -0
  305. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  306. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  307. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  308. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  309. package/src/ui/screens/McpScreen.tsx +256 -0
  310. package/src/ui/screens/ModelsScreen.tsx +542 -0
  311. package/src/ui/screens/PluginsScreen.tsx +1464 -0
  312. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  313. package/src/ui/screens/SkillsScreen.tsx +853 -0
  314. package/src/ui/screens/StylesScreen.tsx +1064 -0
  315. package/src/ui/screens/index.ts +11 -0
  316. package/src/ui/state/AppContext.tsx +233 -0
  317. package/src/ui/state/DimensionsContext.tsx +154 -0
  318. package/src/ui/state/reducer.ts +810 -0
  319. package/src/ui/state/types.ts +503 -0
  320. package/src/ui/theme-mode.ts +352 -0
  321. package/src/ui/theme-resolve.ts +147 -0
  322. package/src/ui/theme.ts +175 -0
  323. package/src/utils/clipboard.ts +58 -0
  324. package/src/utils/command-utils.ts +144 -0
  325. package/src/utils/config-dir.ts +45 -0
  326. package/src/utils/file-locking.ts +146 -0
  327. package/src/utils/fuzzy-search.ts +138 -0
  328. package/src/utils/go-module.ts +36 -0
  329. package/src/utils/open-file.ts +84 -0
  330. package/src/utils/run.ts +67 -0
  331. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,242 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+
5
+ const CLAUDE_PLUGINS_DIR = path.join(
6
+ os.homedir(),
7
+ ".claude",
8
+ "plugins",
9
+ "marketplaces",
10
+ );
11
+
12
+ /**
13
+ * Represents a required environment variable for a plugin's MCP server
14
+ */
15
+ export interface PluginEnvRequirement {
16
+ /** The environment variable name (e.g., "FIGMA_ACCESS_TOKEN") */
17
+ name: string;
18
+ /** User-friendly label (defaults to name if not provided) */
19
+ label: string;
20
+ /** The MCP server name this env var is for */
21
+ serverName: string;
22
+ /** Whether this env var is required (all detected vars are considered required) */
23
+ required: boolean;
24
+ }
25
+
26
+ /**
27
+ * Extract ${VAR_NAME} patterns from a value
28
+ * @param value - The value to search for env var references
29
+ * @returns Array of env var names found
30
+ */
31
+ function extractEnvVarReferences(value: unknown): string[] {
32
+ if (typeof value !== "string") return [];
33
+
34
+ const matches = value.match(/\$\{([A-Z_][A-Z0-9_]*)\}/g);
35
+ if (!matches) return [];
36
+
37
+ return matches
38
+ .map((match) => {
39
+ // Extract VAR_NAME from ${VAR_NAME}
40
+ const innerMatch = match.match(/\$\{([A-Z_][A-Z0-9_]*)\}/);
41
+ return innerMatch ? innerMatch[1] : "";
42
+ })
43
+ .filter(Boolean);
44
+ }
45
+
46
+ /**
47
+ * Recursively extract all env var references from an object
48
+ */
49
+ function extractAllEnvVarReferences(obj: unknown): string[] {
50
+ const vars: string[] = [];
51
+
52
+ if (typeof obj === "string") {
53
+ vars.push(...extractEnvVarReferences(obj));
54
+ } else if (Array.isArray(obj)) {
55
+ for (const item of obj) {
56
+ vars.push(...extractAllEnvVarReferences(item));
57
+ }
58
+ } else if (obj && typeof obj === "object") {
59
+ for (const value of Object.values(obj)) {
60
+ vars.push(...extractAllEnvVarReferences(value));
61
+ }
62
+ }
63
+
64
+ return vars;
65
+ }
66
+
67
+ interface McpServerConfig {
68
+ command?: string;
69
+ args?: string[];
70
+ env?: Record<string, string>;
71
+ type?: "http";
72
+ url?: string;
73
+ }
74
+
75
+ /**
76
+ * Get the path to a plugin's mcp-config.json in the marketplace cache
77
+ * @param marketplaceName - Name of the marketplace (e.g., "magus")
78
+ * @param pluginSource - Plugin source path from marketplace.json (e.g., "./plugins/frontend")
79
+ * @returns Full path to mcp-config.json or undefined if not found
80
+ */
81
+ function getPluginMcpConfigPath(
82
+ marketplaceName: string,
83
+ pluginSource?: string,
84
+ ): string | undefined {
85
+ if (!pluginSource) return undefined;
86
+
87
+ const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
88
+ const pluginPath = path.join(
89
+ marketplacePath,
90
+ pluginSource.replace(/^\.\//, ""),
91
+ );
92
+ const mcpConfigPath = path.join(pluginPath, "mcp-servers", "mcp-config.json");
93
+
94
+ return mcpConfigPath;
95
+ }
96
+
97
+ /**
98
+ * Read and parse a plugin's MCP configuration
99
+ * @param marketplaceName - Name of the marketplace
100
+ * @param pluginSource - Plugin source path from marketplace.json
101
+ * @returns Parsed MCP config or empty object if not found
102
+ */
103
+ export async function readPluginMcpConfig(
104
+ marketplaceName: string,
105
+ pluginSource?: string,
106
+ ): Promise<Record<string, McpServerConfig>> {
107
+ const configPath = getPluginMcpConfigPath(marketplaceName, pluginSource);
108
+ if (!configPath) return {};
109
+
110
+ try {
111
+ if (await fs.pathExists(configPath)) {
112
+ const content = await fs.readJson(configPath);
113
+ // Filter out comment fields (keys starting with _)
114
+ const filtered: Record<string, McpServerConfig> = {};
115
+ for (const [key, value] of Object.entries(content)) {
116
+ if (!key.startsWith("_") && value && typeof value === "object") {
117
+ filtered[key] = value as McpServerConfig;
118
+ }
119
+ }
120
+ return filtered;
121
+ }
122
+ } catch (error) {
123
+ console.error(`Failed to read plugin MCP config: ${error}`);
124
+ }
125
+
126
+ return {};
127
+ }
128
+
129
+ /**
130
+ * Generate user-friendly label from env var name
131
+ * @param varName - Environment variable name (e.g., "FIGMA_ACCESS_TOKEN")
132
+ * @returns Human-readable label (e.g., "Figma Access Token")
133
+ */
134
+ function generateLabel(varName: string): string {
135
+ return varName
136
+ .split("_")
137
+ .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
138
+ .join(" ");
139
+ }
140
+
141
+ /**
142
+ * Get all environment variable requirements for a plugin
143
+ * Extracts ${VAR_NAME} patterns from the plugin's mcp-config.json
144
+ *
145
+ * @param marketplaceName - Name of the marketplace (e.g., "magus")
146
+ * @param pluginSource - Plugin source path from marketplace.json (e.g., "./plugins/frontend")
147
+ * @returns Array of required env vars with metadata
148
+ */
149
+ export async function getPluginEnvRequirements(
150
+ marketplaceName: string,
151
+ pluginSource?: string,
152
+ ): Promise<PluginEnvRequirement[]> {
153
+ const mcpConfig = await readPluginMcpConfig(marketplaceName, pluginSource);
154
+ const requirements: PluginEnvRequirement[] = [];
155
+ const seenVars = new Set<string>();
156
+
157
+ for (const [serverName, config] of Object.entries(mcpConfig)) {
158
+ // Extract from env object
159
+ if (config.env) {
160
+ for (const value of Object.values(config.env)) {
161
+ const vars = extractEnvVarReferences(value);
162
+ for (const varName of vars) {
163
+ if (!seenVars.has(varName)) {
164
+ seenVars.add(varName);
165
+ requirements.push({
166
+ name: varName,
167
+ label: generateLabel(varName),
168
+ serverName,
169
+ required: true,
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ // Also check args array for env var references (e.g., sqlite with ${DATABASE_PATH})
177
+ if (config.args) {
178
+ const argsVars = extractAllEnvVarReferences(config.args);
179
+ for (const varName of argsVars) {
180
+ if (!seenVars.has(varName)) {
181
+ seenVars.add(varName);
182
+ requirements.push({
183
+ name: varName,
184
+ label: generateLabel(varName),
185
+ serverName,
186
+ required: true,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ return requirements;
194
+ }
195
+
196
+ /**
197
+ * Get plugin source path from local marketplace cache
198
+ * @param marketplaceName - Name of the marketplace
199
+ * @param pluginName - Name of the plugin
200
+ * @returns Plugin source path or undefined
201
+ */
202
+ export async function getPluginSourcePath(
203
+ marketplaceName: string,
204
+ pluginName: string,
205
+ ): Promise<string | undefined> {
206
+ const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
207
+ const manifestPath = path.join(
208
+ marketplacePath,
209
+ ".claude-plugin",
210
+ "marketplace.json",
211
+ );
212
+
213
+ try {
214
+ if (await fs.pathExists(manifestPath)) {
215
+ const manifest = await fs.readJson(manifestPath);
216
+ if (manifest.plugins && Array.isArray(manifest.plugins)) {
217
+ const plugin = manifest.plugins.find(
218
+ (p: { name: string }) => p.name === pluginName,
219
+ );
220
+ return plugin?.source;
221
+ }
222
+ }
223
+ } catch {
224
+ // Return undefined if can't read
225
+ }
226
+
227
+ return undefined;
228
+ }
229
+
230
+ /**
231
+ * Get all MCP server names from a plugin's config
232
+ * @param marketplaceName - Name of the marketplace
233
+ * @param pluginSource - Plugin source path
234
+ * @returns Array of MCP server names
235
+ */
236
+ export async function getPluginMcpServerNames(
237
+ marketplaceName: string,
238
+ pluginSource?: string,
239
+ ): Promise<string[]> {
240
+ const mcpConfig = await readPluginMcpConfig(marketplaceName, pluginSource);
241
+ return Object.keys(mcpConfig);
242
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Normalisation for the `releases` array published in marketplace manifests.
3
+ *
4
+ * Lives apart from both readers because both need it: `marketplace-fetcher.ts`
5
+ * (manifest over the network) and `local-marketplace.ts` (same manifest from the
6
+ * on-disk cache). Putting it in either would make one import the other, and
7
+ * marketplace-fetcher was split out precisely to avoid importing local-marketplace,
8
+ * whose module-level `os.homedir()` constants contaminate the test suite.
9
+ */
10
+
11
+ import type { PluginRelease } from "../types/index.js";
12
+
13
+ /** Most release entries the panel will show, however many the manifest declares. */
14
+ export const MAX_RELEASES = 5;
15
+
16
+ /**
17
+ * Coerce a manifest `releases` value into renderable shape, or undefined.
18
+ *
19
+ * The remote manifest is fetched over the network from a repo whose contents we do
20
+ * not control at read time, so nothing about its shape may be assumed — a number
21
+ * where a string belongs, or a ten-thousand-entry array, would otherwise reach the
22
+ * renderer. Entries missing a version or summary are dropped rather than rendered
23
+ * blank, and the array is capped so one manifest cannot flood the detail panel.
24
+ */
25
+ export function normalizeReleases(input: unknown): PluginRelease[] | undefined {
26
+ if (!Array.isArray(input)) return undefined;
27
+
28
+ const out: PluginRelease[] = [];
29
+ for (const raw of input.slice(0, MAX_RELEASES)) {
30
+ if (!raw || typeof raw !== "object") continue;
31
+ const r = raw as Record<string, unknown>;
32
+ if (typeof r.version !== "string" || typeof r.summary !== "string")
33
+ continue;
34
+ if (!r.version.trim() || !r.summary.trim()) continue;
35
+ out.push({
36
+ version: r.version.trim(),
37
+ ...(typeof r.date === "string" && r.date.trim()
38
+ ? { date: r.date.trim() }
39
+ : {}),
40
+ ...(Array.isArray(r.kinds)
41
+ ? { kinds: r.kinds.filter((k): k is string => typeof k === "string") }
42
+ : {}),
43
+ summary: r.summary.trim(),
44
+ });
45
+ }
46
+ return out.length > 0 ? out : undefined;
47
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Read plugin-declared binary dependencies from plugin.json `requires.bin`.
3
+ *
4
+ * A plugin declares the binaries it needs on PATH (e.g. terminal@magus needs
5
+ * `tmux` and `tmux-mcp`). `magus install` resolves these transitively so a
6
+ * teammate who enables a plugin also gets its binaries — closing the class of
7
+ * bug where a plugin's MCP server silently fails because its binary is missing.
8
+ */
9
+
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import fs from "fs-extra";
13
+ import type {
14
+ BinInstaller,
15
+ BinRequirement,
16
+ PluginRequires,
17
+ } from "../types/index.js";
18
+ import { goBinaryName } from "../utils/go-module.js";
19
+ import { parsePluginId } from "../utils/string-utils.js";
20
+ import { getLocalMarketplacesInfo } from "./plugin-manager.js";
21
+
22
+ const MARKETPLACES_DIR = (): string =>
23
+ path.join(os.homedir(), ".claude", "plugins", "marketplaces");
24
+
25
+ const VALID_INSTALLERS: readonly BinInstaller[] = [
26
+ "bun",
27
+ "brew",
28
+ "npm",
29
+ "pip",
30
+ "go",
31
+ ];
32
+
33
+ /**
34
+ * Normalize a raw `requires.bin` entry into a complete BinRequirement,
35
+ * filling in defaults. Returns null for entries that can't be made valid
36
+ * (missing name, or an unrecognized installer).
37
+ */
38
+ function normalizeBin(raw: unknown): BinRequirement | null {
39
+ if (!raw || typeof raw !== "object") return null;
40
+ const o = raw as Record<string, unknown>;
41
+ const name = typeof o.name === "string" ? o.name.trim() : "";
42
+ if (!name) return null;
43
+
44
+ const via = o.via as BinInstaller;
45
+ if (!VALID_INSTALLERS.includes(via)) return null;
46
+
47
+ const bin: BinRequirement = {
48
+ name,
49
+ via,
50
+ check:
51
+ typeof o.check === "string" && o.check.trim()
52
+ ? o.check.trim()
53
+ : `${name} --version`,
54
+ };
55
+ if (typeof o.package === "string") bin.package = o.package;
56
+ if (typeof o.formula === "string") bin.formula = o.formula;
57
+ if (typeof o.module === "string") bin.module = o.module;
58
+ if (typeof o.version === "string") bin.version = o.version;
59
+ return bin;
60
+ }
61
+
62
+ /** Split a package/module spec into its base and optional version. Handles
63
+ * scoped npm (@scope/pkg@1.2.0) and go modules (host/path@latest). */
64
+ function splitVersion(spec: string): { base: string; version?: string } {
65
+ const at = spec.lastIndexOf("@");
66
+ // at > 0 avoids treating a leading npm scope "@scope/..." as a version.
67
+ if (at > 0) return { base: spec.slice(0, at), version: spec.slice(at + 1) };
68
+ return { base: spec };
69
+ }
70
+
71
+ const basename = (p: string): string => p.split("/").pop() ?? p;
72
+
73
+ /**
74
+ * Map a plugin.json `setup` block (the established convention: browser-use uses
75
+ * `pip`, terminal uses `go` + `required`) to BinRequirements.
76
+ * setup: { bun|npm|pip: string[], go: string[], brew: string[], required: string[] }
77
+ */
78
+ function parseSetup(setup: unknown): BinRequirement[] {
79
+ if (!setup || typeof setup !== "object") return [];
80
+ const s = setup as Record<string, unknown>;
81
+ const bins: BinRequirement[] = [];
82
+
83
+ const pkgManager = (key: "bun" | "npm" | "pip"): void => {
84
+ for (const spec of (s[key] as string[] | undefined) ?? []) {
85
+ if (typeof spec !== "string") continue;
86
+ const { base, version } = splitVersion(spec);
87
+ bins.push({
88
+ name: basename(base),
89
+ via: key,
90
+ package: base,
91
+ version,
92
+ check: `${basename(base)} --version`,
93
+ });
94
+ }
95
+ };
96
+ pkgManager("bun");
97
+ pkgManager("npm");
98
+ pkgManager("pip");
99
+
100
+ // Not basename(): a Go module's trailing /vN is its major version, not the
101
+ // binary. `tmux-mcp/v2` installs `tmux-mcp`; naming it `v2` here made every
102
+ // downstream presence check look for a binary that never exists.
103
+ for (const spec of (s.go as string[] | undefined) ?? []) {
104
+ if (typeof spec !== "string") continue;
105
+ const { base, version } = splitVersion(spec);
106
+ const name = goBinaryName(base);
107
+ bins.push({
108
+ name,
109
+ via: "go",
110
+ module: base,
111
+ version,
112
+ check: `${name} --version`,
113
+ });
114
+ }
115
+ for (const spec of (s.brew as string[] | undefined) ?? []) {
116
+ if (typeof spec !== "string") continue;
117
+ bins.push({
118
+ name: spec,
119
+ via: "brew",
120
+ formula: spec,
121
+ check: `${spec} --version`,
122
+ });
123
+ }
124
+ // `required` names a system tool that must be present; brew is the assumed
125
+ // installer (matches the "system tools via brew" decision).
126
+ for (const name of (s.required as string[] | undefined) ?? []) {
127
+ if (typeof name !== "string") continue;
128
+ bins.push({ name, via: "brew", formula: name, check: `${name} --version` });
129
+ }
130
+
131
+ return bins;
132
+ }
133
+
134
+ /**
135
+ * Parse a plugin.json's binary requirements from BOTH the established `setup`
136
+ * block and the richer explicit `requires.bin` form. On a name collision the
137
+ * explicit `requires.bin` entry wins (it can override installer/check/version).
138
+ */
139
+ export function parsePluginRequires(pluginJson: unknown): PluginRequires {
140
+ if (!pluginJson || typeof pluginJson !== "object") return {};
141
+ const obj = pluginJson as Record<string, unknown>;
142
+
143
+ const fromSetup = parseSetup(obj.setup);
144
+
145
+ const requires = obj.requires;
146
+ const binRaw =
147
+ requires && typeof requires === "object"
148
+ ? (requires as Record<string, unknown>).bin
149
+ : undefined;
150
+ const fromRequires = Array.isArray(binRaw)
151
+ ? binRaw.map(normalizeBin).filter((b): b is BinRequirement => b !== null)
152
+ : [];
153
+
154
+ // Merge, requires.bin winning by name.
155
+ const byName = new Map<string, BinRequirement>();
156
+ for (const b of fromSetup) byName.set(b.name, b);
157
+ for (const b of fromRequires) byName.set(b.name, b);
158
+
159
+ const bin = [...byName.values()];
160
+ return bin.length > 0 ? { bin } : {};
161
+ }
162
+
163
+ /** Read `requires` from a plugin.json at a known plugin directory. */
164
+ export async function readPluginRequires(
165
+ pluginPath: string,
166
+ ): Promise<PluginRequires> {
167
+ // `.claude-plugin/plugin.json` FIRST — that is the only location Claude Code's
168
+ // runtime loader reads, and the only one its documentation describes. The root
169
+ // fallback stays for third-party plugins whose authors put it there; such a manifest
170
+ // is inert as far as Claude Code is concerned, but magus can still read metadata
171
+ // out of it rather than reporting nothing.
172
+ const manifestPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
173
+ try {
174
+ if (await fs.pathExists(manifestPath)) {
175
+ return parsePluginRequires(await fs.readJson(manifestPath));
176
+ }
177
+ const rootLevel = path.join(pluginPath, "plugin.json");
178
+ if (await fs.pathExists(rootLevel)) {
179
+ return parsePluginRequires(await fs.readJson(rootLevel));
180
+ }
181
+ } catch {
182
+ // Unreadable/malformed — treat as no requirements.
183
+ }
184
+ return {};
185
+ }
186
+
187
+ /**
188
+ * Resolve a plugin id (name@marketplace) to its on-disk source directory in
189
+ * the local marketplace cache. Returns null if the marketplace/plugin/source
190
+ * can't be located. Mirrors doctor.ts's resolvePluginPath.
191
+ */
192
+ export async function resolvePluginPath(
193
+ pluginId: string,
194
+ ): Promise<string | null> {
195
+ const parsed = parsePluginId(pluginId);
196
+ if (!parsed) return null;
197
+ const { pluginName, marketplace } = parsed;
198
+
199
+ const localMarketplaces = await getLocalMarketplacesInfo();
200
+ const localMp = localMarketplaces.get(marketplace);
201
+ if (!localMp) return null;
202
+
203
+ const localPlugin = localMp.plugins.find((p) => p.name === pluginName);
204
+ if (!localPlugin?.source || typeof localPlugin.source !== "string") {
205
+ return null;
206
+ }
207
+
208
+ const pluginPath = path.join(
209
+ MARKETPLACES_DIR(),
210
+ marketplace,
211
+ localPlugin.source.replace("./", ""),
212
+ );
213
+ return (await fs.pathExists(pluginPath)) ? pluginPath : null;
214
+ }
215
+
216
+ /**
217
+ * Resolve the binary requirements a plugin declares. Returns an empty array
218
+ * when the plugin can't be located or declares none.
219
+ */
220
+ export async function resolvePluginBinRequirements(
221
+ pluginId: string,
222
+ ): Promise<BinRequirement[]> {
223
+ const pluginPath = await resolvePluginPath(pluginId);
224
+ if (!pluginPath) return [];
225
+ const requires = await readPluginRequires(pluginPath);
226
+ return requires.bin ?? [];
227
+ }