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,109 @@
1
+ /**
2
+ * What `claude` subcommand does each plugin operation actually run?
3
+ *
4
+ * This file exists because the answer was wrong for a long time and nothing
5
+ * could see it. `updatePlugin` ran `claude plugin install`, on a comment that
6
+ * asserted install "handles both fresh installs and re-installs (upgrades)".
7
+ * It does not. Measured against Claude Code 2.1.252, with designer@magus
8
+ * installed at 0.6.0 and 0.6.1 published:
9
+ *
10
+ * $ claude plugin install designer@magus --scope project
11
+ * ✔ Plugin "designer@magus" is already installed (scope: project) # exit 0
12
+ *
13
+ * $ claude plugin update designer@magus --scope project
14
+ * ✔ Plugin "designer" updated from 0.6.0 to 0.6.1 for scope project # exit 0
15
+ *
16
+ * Both exit 0. The no-op is indistinguishable from success at every layer
17
+ * above, so every magus update path — the TUI's `U`, "update all", the
18
+ * scope menu, `magus update` — reported
19
+ * success and changed nothing, for ever.
20
+ *
21
+ * Mocking the CLI cannot catch that: a stub answers whatever the test asks it
22
+ * to. Only the argv is checkable here, so the argv is what these assert, with a
23
+ * real `claude` shim on PATH recording what it was called with.
24
+ */
25
+
26
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
27
+ import {
28
+ chmodSync,
29
+ mkdirSync,
30
+ mkdtempSync,
31
+ readFileSync,
32
+ rmSync,
33
+ writeFileSync,
34
+ } from "node:fs";
35
+ import { tmpdir } from "node:os";
36
+ import path from "node:path";
37
+ import {
38
+ installPlugin,
39
+ repairPlugin,
40
+ updatePlugin,
41
+ } from "../services/claude-cli.js";
42
+
43
+ let root: string;
44
+ let logFile: string;
45
+ let savedPath: string | undefined;
46
+
47
+ beforeEach(() => {
48
+ root = mkdtempSync(path.join(tmpdir(), "claude-argv-"));
49
+ const bin = path.join(root, "bin");
50
+ mkdirSync(bin);
51
+ logFile = path.join(root, "argv.log");
52
+
53
+ // A real executable, so this goes through which(1), execFile and the whole
54
+ // wrapper rather than a module double.
55
+ const shim = path.join(bin, "claude");
56
+ writeFileSync(shim, `#!/bin/sh\necho "$@" >> "${logFile}"\nexit 0\n`);
57
+ chmodSync(shim, 0o755);
58
+
59
+ savedPath = process.env.PATH;
60
+ // The shim dir goes FIRST so it shadows a real claude, but the system dirs
61
+ // stay: `getClaudePath` resolves through which(1), which lives in /usr/bin.
62
+ process.env.PATH = [bin, "/usr/bin", "/bin"].join(path.delimiter);
63
+ });
64
+
65
+ afterEach(() => {
66
+ process.env.PATH = savedPath ?? "";
67
+ rmSync(root, { recursive: true, force: true });
68
+ });
69
+
70
+ /** Every argv the shim saw, one line per invocation. */
71
+ function calls(): string[] {
72
+ try {
73
+ return readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean);
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+
79
+ describe("which claude subcommand each operation runs", () => {
80
+ test("updatePlugin runs `plugin update` — NOT `plugin install`", async () => {
81
+ await updatePlugin("designer@magus", "project");
82
+ expect(calls()).toEqual(["plugin update designer@magus --scope project"]);
83
+ });
84
+
85
+ test("installPlugin runs `plugin install`", async () => {
86
+ await installPlugin("designer@magus", "project");
87
+ expect(calls()).toEqual(["plugin install designer@magus --scope project"]);
88
+ });
89
+
90
+ test("repairPlugin uninstalls then installs, in that order", async () => {
91
+ // Content drift under an unchanged version: `update` has no version bump
92
+ // to act on and `install` answers "already installed", so only the
93
+ // uninstall+install pair re-copies the files.
94
+ await repairPlugin("designer@magus", "project", root);
95
+ expect(calls()).toEqual([
96
+ "plugin uninstall designer@magus --scope project",
97
+ "plugin install designer@magus --scope project",
98
+ ]);
99
+ });
100
+
101
+ test("the scope reaches the CLI verbatim, at every scope", async () => {
102
+ await updatePlugin("dev@magus", "user");
103
+ await updatePlugin("dev@magus", "local");
104
+ expect(calls()).toEqual([
105
+ "plugin update dev@magus --scope user",
106
+ "plugin update dev@magus --scope local",
107
+ ]);
108
+ });
109
+ });
@@ -0,0 +1,371 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import type { PluginInfo } from "../services/plugin-manager.js";
3
+ import {
4
+ fetchPluginContents,
5
+ splitSkillDocument,
6
+ } from "../services/skills-manager.js";
7
+ import type { Marketplace } from "../types/index.js";
8
+ import {
9
+ buildPluginBrowserItems,
10
+ pluginContentsRepo,
11
+ } from "../ui/adapters/pluginsAdapter.js";
12
+ import type { PluginContentsEntry } from "../ui/state/types.js";
13
+
14
+ // ─── fetchPluginContents ─────────────────────────────────────────────────────
15
+
16
+ const realFetch = globalThis.fetch;
17
+
18
+ /**
19
+ * Serve one canned git-tree response. `fetchGitTree` caches per repo for the
20
+ * life of the process, so every test must use a repo name of its own.
21
+ */
22
+ function stubTree(paths: string[]): void {
23
+ globalThis.fetch = (async () =>
24
+ new Response(
25
+ JSON.stringify({
26
+ sha: "deadbeef",
27
+ truncated: false,
28
+ tree: paths.map((path) => ({
29
+ path,
30
+ mode: "100644",
31
+ type: "blob",
32
+ sha: `sha-${path}`,
33
+ })),
34
+ }),
35
+ { status: 200, headers: { ETag: '"x"' } },
36
+ )) as unknown as typeof fetch;
37
+ }
38
+
39
+ afterEach(() => {
40
+ globalThis.fetch = realFetch;
41
+ });
42
+
43
+ describe("fetchPluginContents", () => {
44
+ it("counts a grouped skills/ tree once per skill, not once per group", async () => {
45
+ stubTree([
46
+ "README.md",
47
+ "skills/engineering/code-review/SKILL.md",
48
+ "skills/engineering/tdd/SKILL.md",
49
+ "skills/writing/prd/SKILL.md",
50
+ ]);
51
+
52
+ const skills = await fetchPluginContents("fake/grouped", "./");
53
+
54
+ expect(skills.map((s) => s.name)).toEqual(["code-review", "tdd", "prd"]);
55
+ expect(skills.every((s) => s.kind === "skill")).toBe(true);
56
+ expect(skills[0].group).toBe("engineering");
57
+ expect(skills[2].group).toBe("writing");
58
+ });
59
+
60
+ it("ignores harness mirrors and translated docs trees", async () => {
61
+ // everything-claude-code's real shape: 286 skills under skills/, mirrored
62
+ // into four harness directories and six docs/<lang>/ trees for 898 files.
63
+ stubTree([
64
+ "skills/api-design/SKILL.md",
65
+ "skills/agent-sort/SKILL.md",
66
+ ".agents/skills/api-design/SKILL.md",
67
+ ".kiro/skills/api-design/SKILL.md",
68
+ ".cursor/skills/api-design/SKILL.md",
69
+ "docs/ja-JP/skills/api-design/SKILL.md",
70
+ "docs/zh-CN/skills/api-design/SKILL.md",
71
+ ]);
72
+
73
+ const skills = await fetchPluginContents("fake/mirrored", "./");
74
+
75
+ expect(skills.map((s) => s.name)).toEqual(["agent-sort", "api-design"]);
76
+ });
77
+
78
+ it("finds a plugin that is one skill at its root", async () => {
79
+ // blader/humanizer: a root SKILL.md and no skills/ directory at all.
80
+ stubTree([".claude-plugin/plugin.json", "SKILL.md", "README.md"]);
81
+
82
+ const skills = await fetchPluginContents("blader/humanizer", "./");
83
+
84
+ expect(skills).toHaveLength(1);
85
+ expect(skills[0].name).toBe("humanizer");
86
+ expect(skills[0].repoPath).toBe("SKILL.md");
87
+ });
88
+
89
+ it("scopes to one plugin's subtree in a multi-plugin marketplace", async () => {
90
+ stubTree([
91
+ "plugins/dev/skills/frontend/design-system/SKILL.md",
92
+ "plugins/dev/skills/backend/db-branching/SKILL.md",
93
+ "plugins/designer/skills/ui-analyse/SKILL.md",
94
+ ]);
95
+
96
+ const skills = await fetchPluginContents("fake/multi", "./plugins/dev");
97
+
98
+ expect(skills.map((s) => s.name)).toEqual([
99
+ "db-branching",
100
+ "design-system",
101
+ ]);
102
+ });
103
+
104
+ it("still lists commands and agents for a plugin with no skills", async () => {
105
+ stubTree(["agents/reviewer.md", "commands/go.md"]);
106
+
107
+ const found = await fetchPluginContents("fake/no-skills", "./");
108
+
109
+ expect(found.filter((c) => c.kind === "skill")).toEqual([]);
110
+ expect(found.map((c) => `${c.kind}:${c.name}`)).toEqual([
111
+ "command:go",
112
+ "agent:reviewer",
113
+ ]);
114
+ });
115
+ });
116
+
117
+ // ─── splitSkillDocument ──────────────────────────────────────────────────────
118
+
119
+ describe("splitSkillDocument", () => {
120
+ it("keeps frontmatter in file order so description stays near the top", () => {
121
+ const { frontmatter } = splitSkillDocument(
122
+ [
123
+ "---",
124
+ "name: code-review",
125
+ "description: Reviews a diff for correctness",
126
+ "allowed-tools: Read, Grep",
127
+ "---",
128
+ "",
129
+ "# Code review",
130
+ ].join("\n"),
131
+ );
132
+
133
+ expect(frontmatter.map((f) => f.key)).toEqual([
134
+ "name",
135
+ "description",
136
+ "allowed-tools",
137
+ ]);
138
+ expect(frontmatter[1].value).toBe("Reviews a diff for correctness");
139
+ });
140
+
141
+ it("flattens both list shapes to one display string", () => {
142
+ const { frontmatter } = splitSkillDocument(
143
+ [
144
+ "---",
145
+ 'tags: ["tdd", "testing"]',
146
+ "allowed-tools:",
147
+ " - Read",
148
+ " - Bash",
149
+ "---",
150
+ "body",
151
+ ].join("\n"),
152
+ );
153
+
154
+ expect(frontmatter[0]).toEqual({ key: "tags", value: "tdd, testing" });
155
+ expect(frontmatter[1]).toEqual({
156
+ key: "allowed-tools",
157
+ value: "Read, Bash",
158
+ });
159
+ });
160
+
161
+ it("separates the body and counts its lines", () => {
162
+ const doc = splitSkillDocument(
163
+ ["---", "name: x", "---", "", "# Title", "", "Body text.", ""].join("\n"),
164
+ );
165
+
166
+ expect(doc.body).toBe("# Title\n\nBody text.");
167
+ expect(doc.bodyLines).toBe(3);
168
+ });
169
+
170
+ it("treats a file with no frontmatter as all body", () => {
171
+ const doc = splitSkillDocument("# Just markdown\n\nNo frontmatter here.");
172
+
173
+ expect(doc.frontmatter).toEqual([]);
174
+ expect(doc.body).toBe("# Just markdown\n\nNo frontmatter here.");
175
+ });
176
+
177
+ it("reports an empty body rather than a phantom line", () => {
178
+ const doc = splitSkillDocument("---\nname: x\ndescription: y\n---\n");
179
+
180
+ expect(doc.body).toBe("");
181
+ expect(doc.bodyLines).toBe(0);
182
+ });
183
+ });
184
+
185
+ // ─── buildPluginBrowserItems ─────────────────────────────────────────────────
186
+
187
+ const marketplace: Marketplace = {
188
+ name: "mattpocock",
189
+ displayName: "Matt Pocock",
190
+ source: { source: "github", repo: "mattpocock/skills" },
191
+ description: "Agent skills for real engineering",
192
+ featured: true,
193
+ };
194
+
195
+ function makePlugin(overrides: Partial<PluginInfo> = {}): PluginInfo {
196
+ return {
197
+ id: "mattpocock-skills@mattpocock",
198
+ name: "mattpocock-skills",
199
+ version: "1.0.0",
200
+ description: "Matt Pocock's agent skills",
201
+ marketplace: "mattpocock",
202
+ marketplaceDisplay: "Matt Pocock",
203
+ enabled: false,
204
+ source: "./",
205
+ ...overrides,
206
+ };
207
+ }
208
+
209
+ function build(
210
+ plugin: PluginInfo,
211
+ expandedPlugins: Set<string>,
212
+ pluginContents: Map<string, PluginContentsEntry> = new Map(),
213
+ ) {
214
+ return buildPluginBrowserItems({
215
+ marketplaces: [marketplace],
216
+ plugins: [plugin],
217
+ collapsedMarketplaces: new Set(),
218
+ expandedPlugins,
219
+ pluginContents,
220
+ });
221
+ }
222
+
223
+ describe("buildPluginBrowserItems component rows", () => {
224
+ it("marks a plugin expandable and emits no component rows while collapsed", () => {
225
+ const items = build(makePlugin(), new Set());
226
+
227
+ expect(items.map((i) => i.kind)).toEqual(["category", "plugin"]);
228
+ const row = items[1];
229
+ expect(row.kind === "plugin" && row.isExpandable).toBe(true);
230
+ expect(row.kind === "plugin" && row.isExpanded).toBe(false);
231
+ });
232
+
233
+ it("emits one row per component when expanded, of every kind", () => {
234
+ const items = build(
235
+ makePlugin(),
236
+ new Set(["mattpocock-skills@mattpocock"]),
237
+ new Map([
238
+ [
239
+ "mattpocock-skills@mattpocock",
240
+ {
241
+ status: "success",
242
+ components: [
243
+ {
244
+ kind: "skill",
245
+ name: "code-review",
246
+ repoPath: "skills/engineering/code-review/SKILL.md",
247
+ group: "engineering",
248
+ },
249
+ { kind: "command", name: "go", repoPath: "commands/go.md" },
250
+ { kind: "agent", name: "critic", repoPath: "agents/critic.md" },
251
+ { kind: "mcp", name: "mnemex", repoPath: ".mcp.json" },
252
+ ],
253
+ } satisfies PluginContentsEntry,
254
+ ],
255
+ ]),
256
+ );
257
+
258
+ expect(items.map((i) => i.kind)).toEqual([
259
+ "category",
260
+ "plugin",
261
+ "component",
262
+ "component",
263
+ "component",
264
+ "component",
265
+ ]);
266
+ expect(items.slice(2).map((i) => i.label)).toEqual([
267
+ "code-review",
268
+ "go",
269
+ "critic",
270
+ "mnemex",
271
+ ]);
272
+ });
273
+
274
+ it("gives an MCP-only plugin a row instead of rendering it as empty", () => {
275
+ // mnemex: no skills, no commands, no agents, one server. It used to show
276
+ // nothing at all, which read as a plugin that ships nothing.
277
+ const items = build(
278
+ makePlugin({ id: "mnemex@magus", name: "mnemex" }),
279
+ new Set(["mnemex@magus"]),
280
+ new Map([
281
+ [
282
+ "mnemex@magus",
283
+ {
284
+ status: "success",
285
+ components: [
286
+ {
287
+ kind: "mcp",
288
+ name: "mnemex",
289
+ repoPath: ".mcp.json",
290
+ mcpCommand: "mnemex --mcp",
291
+ },
292
+ ],
293
+ } satisfies PluginContentsEntry,
294
+ ],
295
+ ]),
296
+ );
297
+
298
+ expect(items[2].kind).toBe("component");
299
+ expect(items[2].label).toBe("mnemex");
300
+ });
301
+
302
+ it("shows a loading row before the listing arrives", () => {
303
+ const items = build(
304
+ makePlugin(),
305
+ new Set(["mattpocock-skills@mattpocock"]),
306
+ );
307
+
308
+ expect(items[2].kind).toBe("contents-status");
309
+ expect(items[2].kind === "contents-status" && items[2].status).toBe(
310
+ "loading",
311
+ );
312
+ });
313
+
314
+ it("surfaces a failed listing instead of rendering an empty expansion", () => {
315
+ const items = build(
316
+ makePlugin(),
317
+ new Set(["mattpocock-skills@mattpocock"]),
318
+ new Map([
319
+ [
320
+ "mattpocock-skills@mattpocock",
321
+ {
322
+ status: "error",
323
+ error: "GitHub API rate limit exceeded",
324
+ } satisfies PluginContentsEntry,
325
+ ],
326
+ ]),
327
+ );
328
+
329
+ expect(items[2].kind === "contents-status" && items[2].status).toBe(
330
+ "error",
331
+ );
332
+ expect(items[2].kind === "contents-status" && items[2].error).toBe(
333
+ "GitHub API rate limit exceeded",
334
+ );
335
+ });
336
+
337
+ it("does not offer expansion for an orphaned plugin", () => {
338
+ const items = build(makePlugin({ isOrphaned: true }), new Set());
339
+
340
+ expect(items[1].kind === "plugin" && items[1].isExpandable).toBe(false);
341
+ });
342
+
343
+ it("does not offer expansion when the marketplace has no repo to read", () => {
344
+ const items = buildPluginBrowserItems({
345
+ marketplaces: [
346
+ { ...marketplace, source: { source: "directory", repo: undefined } },
347
+ ],
348
+ plugins: [makePlugin()],
349
+ collapsedMarketplaces: new Set(),
350
+ expandedPlugins: new Set(),
351
+ pluginContents: new Map(),
352
+ });
353
+
354
+ expect(items[1].kind === "plugin" && items[1].isExpandable).toBe(false);
355
+ });
356
+ });
357
+
358
+ describe("pluginContentsRepo", () => {
359
+ it("prefers a plugin's own repo over its marketplace's", () => {
360
+ const plugin = makePlugin({ sourceRepo: "addyosmani/agent-skills" });
361
+ expect(pluginContentsRepo(plugin, marketplace)).toBe(
362
+ "addyosmani/agent-skills",
363
+ );
364
+ });
365
+
366
+ it("falls back to the marketplace repo", () => {
367
+ expect(pluginContentsRepo(makePlugin(), marketplace)).toBe(
368
+ "mattpocock/skills",
369
+ );
370
+ });
371
+ });