magus-cli 0.0.0-placeholder → 7.1.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 (330) 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 +725 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +545 -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 +524 -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 +248 -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 +340 -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__/open-file.test.ts +59 -0
  96. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  97. package/src/__tests__/plugin-checkout.test.ts +66 -0
  98. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  99. package/src/__tests__/plugin-contents.test.ts +371 -0
  100. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  101. package/src/__tests__/plugin-releases.test.ts +98 -0
  102. package/src/__tests__/plugin-requires.test.ts +187 -0
  103. package/src/__tests__/plugin-row-text.test.ts +345 -0
  104. package/src/__tests__/plugin-setup.test.ts +1016 -0
  105. package/src/__tests__/profile-adopt.test.ts +177 -0
  106. package/src/__tests__/profile-command.test.ts +110 -0
  107. package/src/__tests__/profile-materializer.test.ts +238 -0
  108. package/src/__tests__/profile-sync.test.ts +154 -0
  109. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  110. package/src/__tests__/resolve-executable.test.ts +42 -0
  111. package/src/__tests__/resolver.test.ts +351 -0
  112. package/src/__tests__/scope-action.test.ts +134 -0
  113. package/src/__tests__/scope-squares.test.tsx +164 -0
  114. package/src/__tests__/selection-scope.test.ts +100 -0
  115. package/src/__tests__/settings-file.test.ts +179 -0
  116. package/src/__tests__/shell-script-callers.test.ts +92 -0
  117. package/src/__tests__/skill-install-files.test.ts +136 -0
  118. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  119. package/src/__tests__/style-wrap.test.ts +220 -0
  120. package/src/__tests__/styles-manager.test.ts +1098 -0
  121. package/src/__tests__/styles-origins.test.ts +414 -0
  122. package/src/__tests__/styles-screen-state.test.ts +457 -0
  123. package/src/__tests__/styles-status-line.test.ts +72 -0
  124. package/src/__tests__/styles-sync.test.ts +452 -0
  125. package/src/__tests__/symlink-manager.test.ts +180 -0
  126. package/src/__tests__/tabbar-layout.test.ts +100 -0
  127. package/src/__tests__/terminology-filler.test.ts +214 -0
  128. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  129. package/src/__tests__/theme-env.test.ts +310 -0
  130. package/src/__tests__/theme-resolve.test.ts +380 -0
  131. package/src/__tests__/tool-probe.test.ts +1400 -0
  132. package/src/__tests__/toolchain.test.ts +232 -0
  133. package/src/__tests__/ui-version-writers.test.ts +88 -0
  134. package/src/__tests__/update-apply.test.ts +637 -0
  135. package/src/__tests__/update-plan.test.ts +1396 -0
  136. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  137. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  138. package/src/__tests__/version-check-refresh.test.ts +45 -0
  139. package/src/__tests__/version-snapshot.test.ts +212 -0
  140. package/src/__tests__/version-unknown-state.test.ts +403 -0
  141. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  142. package/src/cli/ansi.ts +528 -0
  143. package/src/cli/bootstrap.ts +134 -0
  144. package/src/cli/doctor.ts +218 -0
  145. package/src/cli/hook.ts +129 -0
  146. package/src/cli/install.ts +382 -0
  147. package/src/cli/live.ts +374 -0
  148. package/src/cli/models.ts +297 -0
  149. package/src/cli/profile.ts +197 -0
  150. package/src/cli/prompt.ts +32 -0
  151. package/src/cli/router.ts +264 -0
  152. package/src/cli/update-view.ts +717 -0
  153. package/src/cli/update.ts +600 -0
  154. package/src/cli/upgrade.ts +97 -0
  155. package/src/data/alias-flags.ts +301 -0
  156. package/src/data/cli-tools.ts +146 -0
  157. package/src/data/community-styles.ts +531 -0
  158. package/src/data/gitignore-defaults.ts +33 -0
  159. package/src/data/gitignore-reasons.ts +93 -0
  160. package/src/data/gitignore-templates.ts +25 -0
  161. package/src/data/marketplaces.ts +278 -0
  162. package/src/data/mcp-servers.ts +51 -0
  163. package/src/data/models-presets.ts +329 -0
  164. package/src/data/predefined-profiles.ts +252 -0
  165. package/src/data/settings-catalog.ts +714 -0
  166. package/src/data/skill-repos.ts +91 -0
  167. package/src/data/styles/asd-ste100.md +38 -0
  168. package/src/data/styles/calibrated.md +24 -0
  169. package/src/data/styles/direct.md +23 -0
  170. package/src/data/styles/evidence-first.md +22 -0
  171. package/src/data/styles/explanatory.md +23 -0
  172. package/src/data/styles/index.ts +57 -0
  173. package/src/data/styles/no-slop.md +32 -0
  174. package/src/data/styles/plain-language.md +23 -0
  175. package/src/data/styles/structured.md +30 -0
  176. package/src/data/styles/terminology.md +28 -0
  177. package/src/data/styles/terse.md +20 -0
  178. package/src/main.tsx +110 -0
  179. package/src/markdown.d.ts +13 -0
  180. package/src/opentui.d.ts +203 -0
  181. package/src/services/active-closure.ts +117 -0
  182. package/src/services/alias-settings.ts +60 -0
  183. package/src/services/alias-shell-writer.ts +1244 -0
  184. package/src/services/alias-store.ts +172 -0
  185. package/src/services/binary-signing.ts +78 -0
  186. package/src/services/catalog-cache-store.ts +316 -0
  187. package/src/services/claude-cli.ts +304 -0
  188. package/src/services/claude-runner.ts +31 -0
  189. package/src/services/claude-settings.ts +1564 -0
  190. package/src/services/cli-tool-commands.ts +133 -0
  191. package/src/services/community-fetcher.ts +90 -0
  192. package/src/services/community-styles.ts +1194 -0
  193. package/src/services/content-drift.ts +208 -0
  194. package/src/services/conventions-manager.ts +945 -0
  195. package/src/services/doctor-bins.ts +63 -0
  196. package/src/services/doctor.ts +570 -0
  197. package/src/services/dotenv.ts +181 -0
  198. package/src/services/git-worktree.ts +129 -0
  199. package/src/services/github-budget.ts +274 -0
  200. package/src/services/gitignore-check.ts +63 -0
  201. package/src/services/gitignore-detector.ts +167 -0
  202. package/src/services/gitignore-fixer.ts +280 -0
  203. package/src/services/gitignore-resolver.ts +208 -0
  204. package/src/services/gitignore-service.ts +167 -0
  205. package/src/services/hook-registration.ts +218 -0
  206. package/src/services/install-plan.ts +107 -0
  207. package/src/services/local-marketplace.ts +657 -0
  208. package/src/services/manifest.ts +250 -0
  209. package/src/services/marketplace-catalog-git.ts +173 -0
  210. package/src/services/marketplace-catalog.ts +129 -0
  211. package/src/services/marketplace-fetcher.ts +400 -0
  212. package/src/services/marketplace-refresh.ts +245 -0
  213. package/src/services/marketplace-sync.ts +145 -0
  214. package/src/services/mate-availability.ts +133 -0
  215. package/src/services/mate-catalog.ts +265 -0
  216. package/src/services/mcp-registry.ts +171 -0
  217. package/src/services/models-core.ts +969 -0
  218. package/src/services/models-manager.ts +606 -0
  219. package/src/services/plugin-checkout.ts +186 -0
  220. package/src/services/plugin-manager.ts +847 -0
  221. package/src/services/plugin-mcp-config.ts +242 -0
  222. package/src/services/plugin-releases.ts +47 -0
  223. package/src/services/plugin-requires.ts +227 -0
  224. package/src/services/plugin-setup.ts +965 -0
  225. package/src/services/profile-adopt.ts +206 -0
  226. package/src/services/profile-materializer.ts +133 -0
  227. package/src/services/profile-sync.ts +158 -0
  228. package/src/services/profiles.ts +225 -0
  229. package/src/services/resolver.ts +377 -0
  230. package/src/services/settings-file.ts +69 -0
  231. package/src/services/settings-manager.ts +307 -0
  232. package/src/services/skills-manager.ts +1111 -0
  233. package/src/services/skillsmp-client.ts +193 -0
  234. package/src/services/styles-manager.ts +1294 -0
  235. package/src/services/symlink-manager.ts +189 -0
  236. package/src/services/terminology-filler.ts +266 -0
  237. package/src/services/tool-probe.ts +1637 -0
  238. package/src/services/toolchain.ts +431 -0
  239. package/src/services/update-engine.ts +691 -0
  240. package/src/services/update-plan.ts +820 -0
  241. package/src/services/version-check.ts +127 -0
  242. package/src/services/version-snapshot.ts +189 -0
  243. package/src/tui.tsx +112 -0
  244. package/src/types/bun.d.ts +27 -0
  245. package/src/types/gitignore.ts +68 -0
  246. package/src/types/index.ts +447 -0
  247. package/src/ui/App.tsx +535 -0
  248. package/src/ui/adapters/catalogNotice.ts +122 -0
  249. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  250. package/src/ui/adapters/modelsAdapter.ts +213 -0
  251. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  252. package/src/ui/adapters/settingsAdapter.ts +168 -0
  253. package/src/ui/adapters/skillsAdapter.ts +229 -0
  254. package/src/ui/adapters/stylesAdapter.ts +403 -0
  255. package/src/ui/components/CategoryHeader.tsx +64 -0
  256. package/src/ui/components/EmptyFilterState.tsx +32 -0
  257. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  258. package/src/ui/components/ScopeIndicator.tsx +63 -0
  259. package/src/ui/components/ScrollableList.tsx +95 -0
  260. package/src/ui/components/SearchInput.tsx +57 -0
  261. package/src/ui/components/StyledText.tsx +75 -0
  262. package/src/ui/components/TabBar.tsx +101 -0
  263. package/src/ui/components/layout/FooterHints.tsx +78 -0
  264. package/src/ui/components/layout/Panel.tsx +63 -0
  265. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  266. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  267. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  268. package/src/ui/components/layout/index.ts +9 -0
  269. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  270. package/src/ui/components/modals/InputModal.tsx +75 -0
  271. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  272. package/src/ui/components/modals/MessageModal.tsx +66 -0
  273. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  274. package/src/ui/components/modals/SelectModal.tsx +90 -0
  275. package/src/ui/components/modals/index.ts +6 -0
  276. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  277. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  278. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  279. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  280. package/src/ui/components/primitives/MetaText.tsx +40 -0
  281. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  282. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  283. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  284. package/src/ui/components/primitives/index.ts +9 -0
  285. package/src/ui/hooks/index.ts +2 -0
  286. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  287. package/src/ui/hooks/useKeyboard.ts +24 -0
  288. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  289. package/src/ui/registry.ts +33 -0
  290. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  291. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  292. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  293. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  294. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  295. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  296. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  297. package/src/ui/renderers/pluginRowText.ts +70 -0
  298. package/src/ui/renderers/pluginStatusText.ts +56 -0
  299. package/src/ui/renderers/profileRenderers.tsx +406 -0
  300. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  301. package/src/ui/renderers/skillRenderers.tsx +479 -0
  302. package/src/ui/renderers/styleRenderers.tsx +813 -0
  303. package/src/ui/screens/AliasScreen.tsx +1558 -0
  304. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  305. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  306. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  307. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  308. package/src/ui/screens/McpScreen.tsx +256 -0
  309. package/src/ui/screens/ModelsScreen.tsx +542 -0
  310. package/src/ui/screens/PluginsScreen.tsx +1423 -0
  311. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  312. package/src/ui/screens/SkillsScreen.tsx +853 -0
  313. package/src/ui/screens/StylesScreen.tsx +1064 -0
  314. package/src/ui/screens/index.ts +11 -0
  315. package/src/ui/state/AppContext.tsx +233 -0
  316. package/src/ui/state/DimensionsContext.tsx +154 -0
  317. package/src/ui/state/reducer.ts +810 -0
  318. package/src/ui/state/types.ts +503 -0
  319. package/src/ui/theme-mode.ts +440 -0
  320. package/src/ui/theme-resolve.ts +147 -0
  321. package/src/ui/theme.ts +175 -0
  322. package/src/utils/clipboard.ts +58 -0
  323. package/src/utils/command-utils.ts +149 -0
  324. package/src/utils/config-dir.ts +45 -0
  325. package/src/utils/file-locking.ts +146 -0
  326. package/src/utils/fuzzy-search.ts +138 -0
  327. package/src/utils/go-module.ts +36 -0
  328. package/src/utils/open-file.ts +84 -0
  329. package/src/utils/run.ts +67 -0
  330. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,413 @@
1
+ import type { PluginInfo } from "../../services/plugin-manager.js";
2
+ import type { PluginComponent } from "../../services/skills-manager.js";
3
+ import type { ApplyReporter } from "../../services/update-engine.js";
4
+ import type { PluginUpdateItem } from "../../services/update-plan.js";
5
+ import type { Marketplace } from "../../types/index.js";
6
+ import type { PluginContentsEntry } from "../state/types.js";
7
+
8
+ // Virtual marketplace name for the community sub-section of claude-plugins-official
9
+ export const COMMUNITY_VIRTUAL_MARKETPLACE =
10
+ "claude-plugins-official:community";
11
+ // The marketplace that gets split into Anthropic Official + Community sections
12
+ export const SPLIT_MARKETPLACE = "claude-plugins-official";
13
+
14
+ // ─── Item types ───────────────────────────────────────────────────────────────
15
+
16
+ export interface PluginCategoryItem {
17
+ id: string;
18
+ kind: "category";
19
+ label: string;
20
+ marketplace: Marketplace;
21
+ marketplaceEnabled: boolean;
22
+ pluginCount: number;
23
+ isExpanded: boolean;
24
+ isCommunitySection?: boolean;
25
+ /** Visual tone for the category row */
26
+ tone: "yellow" | "gray" | "green" | "red" | "purple" | "teal" | "byline";
27
+ /** Badge text shown on category row (e.g. "★ Official") */
28
+ badge?: string;
29
+ }
30
+
31
+ export interface PluginPluginItem {
32
+ id: string;
33
+ kind: "plugin";
34
+ label: string;
35
+ plugin: PluginInfo;
36
+ /** Fuzzy match highlight indices, if search is active */
37
+ matches?: number[];
38
+ /** True when this plugin's contents can be opened with ← / →. */
39
+ isExpandable: boolean;
40
+ /** True when its component rows follow below. */
41
+ isExpanded: boolean;
42
+ /**
43
+ * The classifier's answer for this plugin — the same item `magus update`
44
+ * plans with. Null for a catalog entry the user is only browsing (neither
45
+ * declared by the profile, installed, nor enabled): it has no state to claim.
46
+ */
47
+ status: PluginUpdateItem | null;
48
+ }
49
+
50
+ /**
51
+ * One component inside an expanded plugin — display only.
52
+ *
53
+ * Claude Code installs plugins, not individual pieces, so these rows carry no
54
+ * action. They exist because a plugin row alone says nothing about what
55
+ * installing it brings: a marketplace that publishes its whole repo as one
56
+ * plugin (mattpocock, ecc, ponytail, addy-agent-skills, humanizer and
57
+ * opendesign all do) rendered as a single unexplained line, and a plugin whose
58
+ * only content is an MCP server — mnemex — looked like it shipped nothing at
59
+ * all.
60
+ */
61
+ export interface PluginComponentItem {
62
+ id: string;
63
+ kind: "component";
64
+ label: string;
65
+ /** The plugin this belongs to, for the detail panel. */
66
+ plugin: PluginInfo;
67
+ component: PluginComponent;
68
+ /** Repo the component's file is read from. */
69
+ repo: string;
70
+ /** Cache key for this component's content. */
71
+ detailKey: string;
72
+ }
73
+
74
+ /** Placeholder row shown while a plugin's contents load, or when they cannot. */
75
+ export interface PluginContentsStatusItem {
76
+ id: string;
77
+ kind: "contents-status";
78
+ label: string;
79
+ plugin: PluginInfo;
80
+ status: "loading" | "error" | "empty";
81
+ error?: string;
82
+ }
83
+
84
+ export type PluginBrowserItem =
85
+ | PluginCategoryItem
86
+ | PluginPluginItem
87
+ | PluginComponentItem
88
+ | PluginContentsStatusItem;
89
+
90
+ // ─── Adapter ─────────────────────────────────────────────────────────────────
91
+
92
+ export interface BuildPluginBrowserItemsArgs {
93
+ marketplaces: Marketplace[];
94
+ plugins: PluginInfo[];
95
+ collapsedMarketplaces: Set<string>;
96
+ expandedPlugins?: Set<string>;
97
+ pluginContents?: Map<string, PluginContentsEntry>;
98
+ /** `classifyPluginRows` output, by plugin id. A plugin absent from it gets no status. */
99
+ statuses?: ReadonlyMap<string, PluginUpdateItem>;
100
+ }
101
+
102
+ /**
103
+ * A plugin's contents are listed from its repo tree, so a plugin with no repo
104
+ * to read (a local-path marketplace, or a plugin whose marketplace has no git
105
+ * remote) cannot be expanded. Offering the arrow there would open a row that
106
+ * can only ever say "could not load".
107
+ */
108
+ export function pluginContentsRepo(
109
+ plugin: PluginInfo,
110
+ marketplace: Marketplace,
111
+ ): string | undefined {
112
+ return plugin.sourceRepo || marketplace.source.repo || undefined;
113
+ }
114
+
115
+ /**
116
+ * Appends a plugin row, plus its component rows when expanded.
117
+ */
118
+ function pushPluginItems(
119
+ items: PluginBrowserItem[],
120
+ plugin: PluginInfo,
121
+ marketplace: Marketplace,
122
+ expandedPlugins: Set<string>,
123
+ pluginContents: Map<string, PluginContentsEntry>,
124
+ statuses: ReadonlyMap<string, PluginUpdateItem>,
125
+ ): void {
126
+ const repo = pluginContentsRepo(plugin, marketplace);
127
+ const isExpandable = !plugin.isOrphaned && !!repo;
128
+ const isExpanded = isExpandable && expandedPlugins.has(plugin.id);
129
+
130
+ items.push({
131
+ id: `pl:${plugin.id}`,
132
+ kind: "plugin",
133
+ label: plugin.name,
134
+ plugin,
135
+ isExpandable,
136
+ isExpanded,
137
+ status: statuses.get(plugin.id) ?? null,
138
+ });
139
+
140
+ if (!isExpanded) return;
141
+
142
+ const entry = pluginContents.get(plugin.id);
143
+ if (!entry || entry.status === "loading") {
144
+ items.push({
145
+ id: `c:${plugin.id}:loading`,
146
+ kind: "contents-status",
147
+ label: "Reading the plugin…",
148
+ plugin,
149
+ status: "loading",
150
+ });
151
+ return;
152
+ }
153
+ if (entry.status === "error") {
154
+ items.push({
155
+ id: `c:${plugin.id}:error`,
156
+ kind: "contents-status",
157
+ label: "Could not read the plugin",
158
+ plugin,
159
+ status: "error",
160
+ error: entry.error,
161
+ });
162
+ return;
163
+ }
164
+ const components = entry.components ?? [];
165
+ if (components.length === 0) {
166
+ items.push({
167
+ id: `c:${plugin.id}:empty`,
168
+ kind: "contents-status",
169
+ label: "Ships no skills, commands, agents or MCP servers",
170
+ plugin,
171
+ status: "empty",
172
+ });
173
+ return;
174
+ }
175
+ for (const component of components) {
176
+ items.push({
177
+ id: `c:${plugin.id}:${component.kind}:${component.repoPath}:${component.name}`,
178
+ kind: "component",
179
+ label: component.name,
180
+ plugin,
181
+ component,
182
+ // `repo` is non-null here: without one the plugin is not expandable and
183
+ // this loop is unreachable.
184
+ repo: repo as string,
185
+ detailKey: `${repo}/${component.repoPath}#${component.name}`,
186
+ });
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Derives tone and badge for a category item based on marketplace identity.
192
+ */
193
+ function categoryStyling(
194
+ mp: Marketplace,
195
+ isCommunitySection: boolean,
196
+ marketplaceEnabled: boolean,
197
+ ): { tone: PluginCategoryItem["tone"]; badge?: string } {
198
+ if (!marketplaceEnabled) {
199
+ return { tone: "gray" };
200
+ }
201
+ if (isCommunitySection) {
202
+ return { tone: "gray", badge: "3rd Party" };
203
+ }
204
+ if (mp.name === "claude-plugins-official") {
205
+ return { tone: "yellow", badge: "★ Official" };
206
+ }
207
+ if (mp.name === "claude-code-plugins") {
208
+ return { tone: "red", badge: "⚠ Deprecated" };
209
+ }
210
+ if (mp.official) {
211
+ return { tone: "yellow", badge: "★ Official" };
212
+ }
213
+ if (isMadAppGang(mp)) {
214
+ return { tone: "byline", badge: "by MadAppGang" };
215
+ }
216
+ return { tone: "green", badge: "✓ Added" };
217
+ }
218
+
219
+ /**
220
+ * Our own marketplaces (magus, magus-marketing, magus-alpha) get the byline
221
+ * instead of the generic "Added". Matched on the repo owner rather than a list
222
+ * of names, so a new channel picks it up without a code change. Same idiom the
223
+ * marketplace sort uses to float MadAppGang entries to the top.
224
+ */
225
+ function isMadAppGang(mp: Marketplace): boolean {
226
+ return (mp.source.repo ?? "").toLowerCase().startsWith("madappgang/");
227
+ }
228
+
229
+ // ─── "Ran, but nothing moved" ─────────────────────────────────────────────────
230
+
231
+ /**
232
+ * Collect the engine's no-move reasons so the screen can name them.
233
+ *
234
+ * `ApplyResult.unchanged` is the whole point of the read-back: an item that
235
+ * ran, exited 0, and left the machine exactly as it was. The CLI prints
236
+ * `N item(s) ran but did not change:` with each cause; the TUI used to branch
237
+ * on `failed.length` alone, show a success, and refetch a row that had not
238
+ * moved — "press U, press U, forever", which is the experience the read-back
239
+ * was added to remove.
240
+ *
241
+ * The cause itself only ever reaches the reporter (`applyPlugins` formats it as
242
+ * `no change — <why>`), so this captures it there, exactly as `cli/update.ts`
243
+ * does with `finishedDetail`.
244
+ */
245
+ export function noMoveReasons(): {
246
+ reporter: ApplyReporter;
247
+ /** One line per unchanged item, with its cause when one was reported. */
248
+ lines(unchanged: readonly string[]): string;
249
+ } {
250
+ const why = new Map<string, string>();
251
+ return {
252
+ reporter: {
253
+ begin: () => {},
254
+ finish: (_ok, name, detail) => {
255
+ const reason = detail.replace(/^no change — /, "");
256
+ if (reason !== detail) why.set(name, reason);
257
+ },
258
+ detach: (work) => work(),
259
+ },
260
+ lines: (unchanged) =>
261
+ unchanged
262
+ .map((name) => {
263
+ const reason = why.get(name);
264
+ return reason ? `${name}: ${reason}` : name;
265
+ })
266
+ .join("\n"),
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Builds the flat list of items for the PluginsScreen list panel.
272
+ * Extracted from PluginsScreen so it can be tested independently.
273
+ */
274
+ export function buildPluginBrowserItems({
275
+ marketplaces,
276
+ plugins,
277
+ collapsedMarketplaces,
278
+ expandedPlugins = new Set(),
279
+ pluginContents = new Map(),
280
+ statuses = new Map(),
281
+ }: BuildPluginBrowserItemsArgs): PluginBrowserItem[] {
282
+ const pluginsByMarketplace = new Map<string, PluginInfo[]>();
283
+ for (const plugin of plugins) {
284
+ const existing = pluginsByMarketplace.get(plugin.marketplace) || [];
285
+ existing.push(plugin);
286
+ pluginsByMarketplace.set(plugin.marketplace, existing);
287
+ }
288
+
289
+ // Sort marketplaces: our own (magus*) first, deprecated last, others between.
290
+ const rank = (m: Marketplace) =>
291
+ m.owned ? 0 : m.deprecated || m.name === "claude-code-plugins" ? 2 : 1;
292
+ const sortedMarketplaces = [...marketplaces].sort(
293
+ (a, b) => rank(a) - rank(b),
294
+ );
295
+
296
+ const items: PluginBrowserItem[] = [];
297
+
298
+ for (const marketplace of sortedMarketplaces) {
299
+ const marketplacePlugins = pluginsByMarketplace.get(marketplace.name) || [];
300
+ const isCollapsed = collapsedMarketplaces.has(marketplace.name);
301
+ const isEnabled = marketplacePlugins.length > 0 || !!marketplace.official;
302
+ const hasPlugins = marketplacePlugins.length > 0;
303
+
304
+ // Special handling: split claude-plugins-official into two sub-sections
305
+ if (marketplace.name === SPLIT_MARKETPLACE && hasPlugins) {
306
+ const anthropicPlugins = marketplacePlugins.filter(
307
+ (p) => p.author?.name?.toLowerCase() === "anthropic",
308
+ );
309
+ const communityPlugins = marketplacePlugins.filter(
310
+ (p) => p.author?.name?.toLowerCase() !== "anthropic",
311
+ );
312
+
313
+ // Sub-section 1: Anthropic Official (plugins by Anthropic)
314
+ const anthropicCollapsed = collapsedMarketplaces.has(marketplace.name);
315
+ const anthropicHasPlugins = anthropicPlugins.length > 0;
316
+ const anthropicStyle = categoryStyling(marketplace, false, isEnabled);
317
+ items.push({
318
+ id: `mp:${marketplace.name}`,
319
+ kind: "category",
320
+ label: marketplace.displayName,
321
+ marketplace,
322
+ marketplaceEnabled: isEnabled,
323
+ pluginCount: anthropicPlugins.length,
324
+ isExpanded: !anthropicCollapsed && anthropicHasPlugins,
325
+ tone: anthropicStyle.tone,
326
+ badge: anthropicStyle.badge,
327
+ });
328
+ if (isEnabled && anthropicHasPlugins && !anthropicCollapsed) {
329
+ for (const plugin of anthropicPlugins) {
330
+ pushPluginItems(
331
+ items,
332
+ plugin,
333
+ marketplace,
334
+ expandedPlugins,
335
+ pluginContents,
336
+ statuses,
337
+ );
338
+ }
339
+ }
340
+
341
+ // Sub-section 2: Community (third-party plugins in same marketplace)
342
+ if (communityPlugins.length > 0) {
343
+ const communityVirtualMp: Marketplace = {
344
+ name: COMMUNITY_VIRTUAL_MARKETPLACE,
345
+ displayName: "Anthropic Official — 3rd Party",
346
+ source: marketplace.source,
347
+ description:
348
+ "Third-party plugins in the Anthropic Official marketplace",
349
+ };
350
+ const communityCollapsed = collapsedMarketplaces.has(
351
+ COMMUNITY_VIRTUAL_MARKETPLACE,
352
+ );
353
+ const communityStyle = categoryStyling(communityVirtualMp, true, true);
354
+ items.push({
355
+ id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
356
+ kind: "category",
357
+ label: "Anthropic Official — 3rd Party",
358
+ marketplace: communityVirtualMp,
359
+ marketplaceEnabled: true,
360
+ pluginCount: communityPlugins.length,
361
+ isExpanded: !communityCollapsed,
362
+ isCommunitySection: true,
363
+ tone: communityStyle.tone,
364
+ badge: communityStyle.badge,
365
+ });
366
+ if (!communityCollapsed) {
367
+ for (const plugin of communityPlugins) {
368
+ pushPluginItems(
369
+ items,
370
+ plugin,
371
+ communityVirtualMp,
372
+ expandedPlugins,
373
+ pluginContents,
374
+ statuses,
375
+ );
376
+ }
377
+ }
378
+ }
379
+
380
+ continue;
381
+ }
382
+
383
+ // Category header (marketplace)
384
+ const style = categoryStyling(marketplace, false, isEnabled);
385
+ items.push({
386
+ id: `mp:${marketplace.name}`,
387
+ kind: "category",
388
+ label: marketplace.displayName,
389
+ marketplace,
390
+ marketplaceEnabled: isEnabled,
391
+ pluginCount: marketplacePlugins.length,
392
+ isExpanded: !isCollapsed && hasPlugins,
393
+ tone: style.tone,
394
+ badge: style.badge,
395
+ });
396
+
397
+ // Plugins under this marketplace (if expanded)
398
+ if (isEnabled && hasPlugins && !isCollapsed) {
399
+ for (const plugin of marketplacePlugins) {
400
+ pushPluginItems(
401
+ items,
402
+ plugin,
403
+ marketplace,
404
+ expandedPlugins,
405
+ pluginContents,
406
+ statuses,
407
+ );
408
+ }
409
+ }
410
+ }
411
+
412
+ return items;
413
+ }
@@ -0,0 +1,168 @@
1
+ import {
2
+ SETTINGS_CATALOG,
3
+ type SettingCategory,
4
+ type SettingDefinition,
5
+ } from "../../data/settings-catalog.js";
6
+ import type { ScopedSettingValues } from "../../services/settings-manager.js";
7
+ import { type UiColor, theme } from "../theme.js";
8
+
9
+ // ─── Item types ────────────────────────────────────────────────────────────────
10
+
11
+ export interface SettingsCategoryItem {
12
+ id: string;
13
+ kind: "category";
14
+ label: string;
15
+ category: SettingCategory;
16
+ isDefault: true;
17
+ }
18
+
19
+ export interface SettingsSettingItem {
20
+ id: string;
21
+ kind: "setting";
22
+ label: string;
23
+ category: SettingCategory;
24
+ setting: SettingDefinition;
25
+ scopedValues: ScopedSettingValues;
26
+ effectiveValue: string | undefined;
27
+ isDefault: boolean;
28
+ }
29
+
30
+ export type SettingsBrowserItem = SettingsCategoryItem | SettingsSettingItem;
31
+
32
+ // ─── Constants ─────────────────────────────────────────────────────────────────
33
+
34
+ export const CATEGORY_LABELS: Record<SettingCategory, string> = {
35
+ recommended: "Recommended",
36
+ agents: "Agents & Teams",
37
+ models: "Models & Thinking",
38
+ workflow: "Workflow",
39
+ terminal: "Terminal & UI",
40
+ performance: "Performance",
41
+ advanced: "Advanced",
42
+ };
43
+
44
+ export const CATEGORY_ORDER: SettingCategory[] = [
45
+ "recommended",
46
+ "agents",
47
+ "models",
48
+ "workflow",
49
+ "terminal",
50
+ "performance",
51
+ "advanced",
52
+ ];
53
+
54
+ export const CATEGORY_BG: Record<SettingCategory, UiColor> = {
55
+ recommended: theme.colors.success,
56
+ agents: theme.colors.info,
57
+ models: theme.colors.accent,
58
+ workflow: theme.colors.link,
59
+ terminal: theme.colors.warning,
60
+ performance: theme.colors.accent,
61
+ advanced: theme.colors.warning,
62
+ };
63
+
64
+ export const CATEGORY_COLOR: Record<SettingCategory, UiColor> = {
65
+ recommended: theme.colors.success,
66
+ agents: theme.colors.info,
67
+ models: theme.colors.info,
68
+ workflow: theme.colors.link,
69
+ terminal: theme.colors.link,
70
+ performance: "magentaBright",
71
+ advanced: theme.colors.warning,
72
+ };
73
+
74
+ export const CATEGORY_DESCRIPTIONS: Record<SettingCategory, string> = {
75
+ recommended: "Most impactful settings every user should know.",
76
+ agents: "Agent teams, task lists, and subagent configuration.",
77
+ models: "Model selection, extended thinking, and effort.",
78
+ workflow: "Git, plans, permissions, output style, and languages.",
79
+ terminal: "Shell, spinners, progress bars, voice, and UI behavior.",
80
+ performance: "Compaction, token limits, timeouts, and caching.",
81
+ advanced: "Telemetry, updates, debugging, and internal controls.",
82
+ };
83
+
84
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
85
+
86
+ /** Get the effective value (project overrides user) */
87
+ export function getEffectiveValue(
88
+ scoped: ScopedSettingValues,
89
+ ): string | undefined {
90
+ return scoped.project !== undefined ? scoped.project : scoped.user;
91
+ }
92
+
93
+ export function formatValue(
94
+ setting: SettingDefinition,
95
+ value: string | undefined,
96
+ ): string {
97
+ if (value === undefined || value === "") {
98
+ if (setting.type === "boolean" && setting.defaultValue !== undefined) {
99
+ return setting.defaultValue === "true" ? "on" : "off";
100
+ }
101
+ if (setting.type === "select" && setting.defaultValue !== undefined) {
102
+ const opt = setting.options?.find(
103
+ (o) => o.value === setting.defaultValue,
104
+ );
105
+ return opt ? opt.label : setting.defaultValue;
106
+ }
107
+ return "—";
108
+ }
109
+
110
+ if (setting.type === "boolean") {
111
+ return value === "true" || value === "1" ? "on" : "off";
112
+ }
113
+
114
+ if (setting.type === "select" && setting.options) {
115
+ const opt = setting.options.find((o) => o.value === value);
116
+ return opt ? opt.label : value;
117
+ }
118
+
119
+ if (value.length > 12) {
120
+ return `${value.slice(0, 12)}…`;
121
+ }
122
+ return value;
123
+ }
124
+
125
+ // ─── Adapter ───────────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Builds the flat list of items for the SettingsScreen list panel.
129
+ * Extracted from SettingsScreen so it can be tested independently.
130
+ */
131
+ export function buildSettingsBrowserItems(
132
+ values: Map<string, ScopedSettingValues>,
133
+ ): SettingsBrowserItem[] {
134
+ const items: SettingsBrowserItem[] = [];
135
+
136
+ for (const category of CATEGORY_ORDER) {
137
+ items.push({
138
+ id: `cat:${category}`,
139
+ kind: "category",
140
+ label: CATEGORY_LABELS[category],
141
+ category,
142
+ isDefault: true,
143
+ });
144
+
145
+ const categorySettings = SETTINGS_CATALOG.filter(
146
+ (s) => s.category === category,
147
+ );
148
+ for (const setting of categorySettings) {
149
+ const scoped = values.get(setting.id) || {
150
+ user: undefined,
151
+ project: undefined,
152
+ };
153
+ const effective = getEffectiveValue(scoped);
154
+ items.push({
155
+ id: `setting:${setting.id}`,
156
+ kind: "setting",
157
+ label: setting.name,
158
+ category,
159
+ setting,
160
+ scopedValues: scoped,
161
+ effectiveValue: effective,
162
+ isDefault: effective === undefined || effective === "",
163
+ });
164
+ }
165
+ }
166
+
167
+ return items;
168
+ }