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,70 @@
1
+ /**
2
+ * Everything a Plugins row shows after the plugin's name, as plain segments.
3
+ *
4
+ * `pluginRow` renders these segments verbatim — for the selected row, the
5
+ * unselected row, and an orphan alike — and holds no conditional on plugin
6
+ * state of its own. That is the point. The row used to carry three rules of
7
+ * its own, and each one was a way to disagree with `magus update`:
8
+ *
9
+ * - every status clause was gated on the installed version being displayable,
10
+ * so a plugin recorded as "0.0.0" showed no status and read as healthy;
11
+ * - its reason words ("? not comparable") were not the CLI's reason;
12
+ * - an orphan's text depended on the cursor — the selected row said
13
+ * "moved → X", the unselected one always said "deprecated".
14
+ *
15
+ * Pure and `.ts`, so a test can assert on exactly what the row prints.
16
+ */
17
+
18
+ import { isKnownVersion } from "../../services/version-snapshot.js";
19
+ import type { PluginPluginItem } from "../adapters/pluginsAdapter.js";
20
+ import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
21
+
22
+ export interface PluginRowSegment {
23
+ text: string;
24
+ tone: StatusTone | "text";
25
+ }
26
+
27
+ export interface PluginRowText {
28
+ /** After the name: the version, a history badge, then the status suffix. */
29
+ segments: PluginRowSegment[];
30
+ /** The segments joined — exactly what the row renders after the name. */
31
+ text: string;
32
+ }
33
+
34
+ export function pluginRowText(item: PluginPluginItem): PluginRowText {
35
+ const { plugin, status } = item;
36
+ const segments: PluginRowSegment[] = [];
37
+
38
+ // A version is shown only when it carries information. "0.0.0" means
39
+ // installed with an unknown version (Anthropic's official plugins record
40
+ // exactly that) — it is never printed, and nothing below depends on it.
41
+ const installed = status?.installed ?? plugin.installedVersion;
42
+ const shown = isKnownVersion(installed ?? undefined) ? installed : undefined;
43
+
44
+ // "What just changed" and "what state it is in now" are independent facts
45
+ // and both can be true at once: an external update can move a plugin
46
+ // 2.9.0 → 3.0.1 while the marketplace already offers 3.3.1. So the badge
47
+ // replaces only the version, and the status suffix is always appended.
48
+ if (plugin.recentlyUpdatedFrom && shown) {
49
+ // Show what actually happened, not just where it landed — the update may
50
+ // have come from Claude Code or a direct CLI call without telling us.
51
+ segments.push({
52
+ text: ` v${plugin.recentlyUpdatedFrom} → v${shown} updated`,
53
+ tone: "success",
54
+ });
55
+ } else if (plugin.recentlyInstalled) {
56
+ // Newly seen. A plugin can be genuinely installed with no version we can
57
+ // name, so the version is optional here.
58
+ segments.push({
59
+ text: shown ? ` v${shown} new` : " new",
60
+ tone: "success",
61
+ });
62
+ } else if (shown) {
63
+ segments.push({ text: ` v${shown}`, tone: "muted" });
64
+ }
65
+
66
+ const suffix = pluginStatusText(status);
67
+ if (suffix.suffix) segments.push({ text: suffix.suffix, tone: suffix.tone });
68
+
69
+ return { segments, text: segments.map((s) => s.text).join("") };
70
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A plugin's status, as the words and tone the Plugins screen shows.
3
+ *
4
+ * Pure, and a function of the classifier's answer ALONE. The row used to build
5
+ * this text from `PluginInfo` flags with its own rules, and every clause was
6
+ * gated on the installed version being displayable — so a plugin recorded as
7
+ * "0.0.0" rendered with no status at all and read as healthy, while
8
+ * `magus update` said `unknown`. Nothing here looks at the version.
9
+ *
10
+ * The note is shown VERBATIM: it is the exact string `magus update` prints,
11
+ * which is what makes the two front ends say the same thing.
12
+ */
13
+
14
+ import type { PluginUpdateItem } from "../../services/update-plan.js";
15
+
16
+ export type StatusTone = "muted" | "warning" | "danger" | "success" | "info";
17
+
18
+ export interface PluginStatusText {
19
+ /** Text after the version, with its leading space; "" for nothing to say. */
20
+ suffix: string;
21
+ tone: StatusTone;
22
+ }
23
+
24
+ export function pluginStatusText(
25
+ status: PluginUpdateItem | null,
26
+ ): PluginStatusText {
27
+ if (!status) return { suffix: "", tone: "muted" };
28
+ const withNote = status.note ? ` (${status.note})` : "";
29
+
30
+ switch (status.action) {
31
+ case "unknown":
32
+ // Amber, not red: "cannot tell" is not a failure.
33
+ return {
34
+ suffix: ` ? ${status.note ?? "cannot tell whether it is current"}`,
35
+ tone: "warning",
36
+ };
37
+ case "update":
38
+ return {
39
+ suffix: `${status.target ? ` → v${status.target} available` : " update available"}${withNote}`,
40
+ tone: "warning",
41
+ };
42
+ case "repair":
43
+ return { suffix: ` stale — reinstall${withNote}`, tone: "warning" };
44
+ case "install":
45
+ // `absent` only ever comes from a profile pin (see `pinForRow`), so it
46
+ // may say so. An undeclared row that is merely enabled must not.
47
+ return status.reason === "absent"
48
+ ? { suffix: " not installed — profile declares it", tone: "warning" }
49
+ : // Enabled with nothing on disk is a broken state; a missing
50
+ // declared plugin is routine work. Red against amber is the
51
+ // distinction that matters.
52
+ { suffix: ` not installed${withNote}`, tone: "danger" };
53
+ case "current":
54
+ return { suffix: withNote, tone: "muted" };
55
+ }
56
+ }
@@ -0,0 +1,406 @@
1
+ import type React from "react";
2
+ import type { PredefinedProfile } from "../../data/predefined-profiles.js";
3
+ import type { ProfileEntry } from "../../types/index.js";
4
+ import type { ItemRenderer } from "../registry.js";
5
+ import { type UiColor, theme } from "../theme.js";
6
+
7
+ // ─── List item types ───────────────────────────────────────────────────────────
8
+
9
+ export type ProfileListItem =
10
+ | { kind: "header"; label: string; color: UiColor; count: number }
11
+ | { kind: "predefined"; profile: PredefinedProfile }
12
+ | { kind: "saved"; entry: ProfileEntry };
13
+
14
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
15
+
16
+ function truncate(s: string, maxLen: number): string {
17
+ if (s.length <= maxLen) return s;
18
+ return `${s.slice(0, maxLen - 1)}\u2026`;
19
+ }
20
+
21
+ export function humanizeValue(_key: string, value: unknown): string {
22
+ if (typeof value === "boolean") return value ? "on" : "off";
23
+ if (typeof value !== "string") return String(value);
24
+ if (value === "claude-sonnet-4-6") return "Sonnet";
25
+ if (value === "claude-opus-4-6") return "Opus";
26
+ if (value === "claude-haiku-4-6") return "Haiku";
27
+ if (value.startsWith("claude-sonnet")) return "Sonnet";
28
+ if (value.startsWith("claude-opus")) return "Opus";
29
+ if (value.startsWith("claude-haiku")) return "Haiku";
30
+ return value;
31
+ }
32
+
33
+ export function humanizeKey(key: string): string {
34
+ const labels: Record<string, string> = {
35
+ effortLevel: "Effort",
36
+ model: "Model",
37
+ outputStyle: "Output",
38
+ alwaysThinkingEnabled: "Thinking",
39
+ includeGitInstructions: "Git Instructions",
40
+ respectGitignore: "Gitignore",
41
+ enableAllProjectMcpServers: "Auto MCP",
42
+ autoUpdatesChannel: "Updates",
43
+ language: "Language",
44
+ cleanupPeriodDays: "Cleanup",
45
+ };
46
+ return labels[key] ?? key;
47
+ }
48
+
49
+ export function stripSuffix(pluginName: string): string {
50
+ return pluginName
51
+ .replace(/@magus$/, "")
52
+ .replace(/@claude-plugins-official$/, "");
53
+ }
54
+
55
+ export function wrapNames(names: string[], lineMax = 40): string[] {
56
+ const lines: string[] = [];
57
+ let current = "";
58
+ for (const name of names) {
59
+ const add = current ? `, ${name}` : name;
60
+ if (current && current.length + add.length > lineMax) {
61
+ lines.push(current);
62
+ current = name;
63
+ } else {
64
+ current += current ? `, ${name}` : name;
65
+ }
66
+ }
67
+ if (current) lines.push(current);
68
+ return lines;
69
+ }
70
+
71
+ export function formatDate(iso: string): string {
72
+ try {
73
+ const d = new Date(iso);
74
+ return d.toLocaleDateString("en-US", {
75
+ month: "short",
76
+ day: "numeric",
77
+ year: "numeric",
78
+ });
79
+ } catch {
80
+ return iso;
81
+ }
82
+ }
83
+
84
+ // ─── Header renderer ───────────────────────────────────────────────────────────
85
+
86
+ const headerRenderer: ItemRenderer<{
87
+ kind: "header";
88
+ label: string;
89
+ color: UiColor;
90
+ count: number;
91
+ }> = {
92
+ renderRow: ({ item }) => (
93
+ <text bg={item.color} fg={theme.hints.fg}>
94
+ <strong>
95
+ {" "}
96
+ {item.label} ({item.count}){" "}
97
+ </strong>
98
+ </text>
99
+ ),
100
+ renderDetail: () => (
101
+ <text fg={theme.colors.muted}>Select a profile to see details</text>
102
+ ),
103
+ };
104
+
105
+ // ─── Predefined renderer ───────────────────────────────────────────────────────
106
+
107
+ const DIVIDER = "────────────────────────";
108
+
109
+ const predefinedRenderer: ItemRenderer<{
110
+ kind: "predefined";
111
+ profile: PredefinedProfile;
112
+ }> = {
113
+ renderRow: ({ item, isSelected }) => {
114
+ const { profile } = item;
115
+ const pluginCount =
116
+ profile.magusPlugins.length + profile.anthropicPlugins.length;
117
+ const skillCount = profile.skills.length;
118
+ const countStr = `${pluginCount}p ${skillCount}s`;
119
+
120
+ if (isSelected) {
121
+ return (
122
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
123
+ {" "}
124
+ {profile.name} {countStr}{" "}
125
+ </text>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <text fg={theme.colors.text}>
131
+ <span fg={theme.colors.text}>
132
+ {" "}
133
+ {profile.name}
134
+ </span>
135
+ <span fg={theme.colors.dim}> {countStr}</span>
136
+ </text>
137
+ );
138
+ },
139
+
140
+ renderDetail: ({ item }) => {
141
+ const { profile } = item;
142
+ const settingEntries = Object.entries(profile.settings).filter(
143
+ ([k]) => k !== "env",
144
+ );
145
+ const envMap =
146
+ (profile.settings.env as Record<string, string> | undefined) ?? {};
147
+ const tasksOn = envMap.CLAUDE_CODE_ENABLE_TASKS === "true";
148
+ const teamsOn = envMap.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "true";
149
+
150
+ return (
151
+ <box flexDirection="column">
152
+ <box>
153
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
154
+ <strong> {profile.name} </strong>
155
+ </text>
156
+ </box>
157
+ <box marginTop={1}>
158
+ <text fg={theme.colors.muted}>{profile.description}</text>
159
+ </box>
160
+
161
+ {/* Magus plugins */}
162
+ <box>
163
+ <text
164
+ fg={theme.colors.muted}
165
+ >{`\nMagus (${profile.magusPlugins.length})`}</text>
166
+ </box>
167
+ <box>
168
+ <text fg={theme.colors.info}>
169
+ {profile.magusPlugins.map((p) => ` ■ ${p}`).join("\n")}
170
+ </text>
171
+ </box>
172
+
173
+ {/* Anthropic plugins */}
174
+ <box>
175
+ <text
176
+ fg={theme.colors.muted}
177
+ >{`\nAnthropic (${profile.anthropicPlugins.length})`}</text>
178
+ </box>
179
+ <box>
180
+ <text fg={theme.colors.accent}>
181
+ {profile.anthropicPlugins.map((p) => ` ■ ${p}`).join("\n")}
182
+ </text>
183
+ </box>
184
+
185
+ {/* Skills */}
186
+ {profile.skills.length > 0 && (
187
+ <>
188
+ <box>
189
+ <text
190
+ fg={theme.colors.muted}
191
+ >{`\nSkills (${profile.skills.length})`}</text>
192
+ </box>
193
+ <box>
194
+ <text fg={theme.colors.warning}>
195
+ {profile.skills.map((s) => ` ■ ${s}`).join("\n")}
196
+ </text>
197
+ </box>
198
+ </>
199
+ )}
200
+
201
+ {/* Settings */}
202
+ <box>
203
+ <text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text>
204
+ </box>
205
+ <box>
206
+ <text fg={theme.colors.muted}>
207
+ {[
208
+ ...settingEntries.map(
209
+ ([k, v]) =>
210
+ ` ${humanizeKey(k).padEnd(18)}${humanizeValue(k, v)}`,
211
+ ),
212
+ ...(tasksOn ? [` ${"Tasks".padEnd(18)}on`] : []),
213
+ ...(teamsOn ? [` ${"Agent Teams".padEnd(18)}on`] : []),
214
+ ].join("\n")}
215
+ </text>
216
+ </box>
217
+
218
+ {/* Action */}
219
+ <box>
220
+ <text fg={theme.colors.dim}>{`\n${DIVIDER}`}</text>
221
+ </box>
222
+ <box>
223
+ <text fg={theme.colors.info}> Enter/a to apply</text>
224
+ </box>
225
+ </box>
226
+ );
227
+ },
228
+ };
229
+
230
+ // ─── Saved renderer ────────────────────────────────────────────────────────────
231
+
232
+ const savedRenderer: ItemRenderer<{ kind: "saved"; entry: ProfileEntry }> = {
233
+ renderRow: ({ item, isSelected }) => {
234
+ const { entry } = item;
235
+ const pluginCount = Object.keys(entry.plugins).length;
236
+ const dateStr = formatDate(entry.updatedAt);
237
+ const label = truncate(
238
+ `${entry.name} — ${pluginCount} plugin${pluginCount !== 1 ? "s" : ""} · ${dateStr}`,
239
+ 45,
240
+ );
241
+
242
+ if (isSelected) {
243
+ return (
244
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
245
+ {" "}
246
+ {label}{" "}
247
+ </text>
248
+ );
249
+ }
250
+
251
+ const scopeColor =
252
+ entry.scope === "user" ? theme.scopes.user : theme.scopes.project;
253
+ const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
254
+
255
+ return (
256
+ <text fg={theme.colors.text}>
257
+ <span fg={scopeColor}>{scopeLabel} </span>
258
+ <span fg={theme.colors.text}>{label}</span>
259
+ </text>
260
+ );
261
+ },
262
+
263
+ renderDetail: ({ item }) => {
264
+ const { entry: selectedProfile } = item;
265
+ const plugins = Object.keys(selectedProfile.plugins);
266
+ const cleanPlugins = plugins.map(stripSuffix);
267
+ const scopeColor =
268
+ selectedProfile.scope === "user"
269
+ ? theme.scopes.user
270
+ : theme.scopes.project;
271
+ const scopeLabel =
272
+ selectedProfile.scope === "user"
273
+ ? "User (~/.claude/profiles.json)"
274
+ : "Project (.claude/profiles.json — committed to git)";
275
+
276
+ return (
277
+ <box flexDirection="column">
278
+ <text fg={theme.colors.info}>
279
+ <strong>{selectedProfile.name}</strong>
280
+ </text>
281
+ <box marginTop={1}>
282
+ <text fg={theme.colors.muted}>Scope: </text>
283
+ <text fg={scopeColor}>{scopeLabel}</text>
284
+ </box>
285
+ <box marginTop={1}>
286
+ <text fg={theme.colors.muted}>
287
+ Created: {formatDate(selectedProfile.createdAt)} · Updated:{" "}
288
+ {formatDate(selectedProfile.updatedAt)}
289
+ </text>
290
+ </box>
291
+ <box marginTop={1} flexDirection="column">
292
+ <text fg={theme.colors.muted}>
293
+ Plugins ({plugins.length}
294
+ {plugins.length === 0 ? " — applying will disable all plugins" : ""}
295
+ ):
296
+ </text>
297
+ {cleanPlugins.length === 0 ? (
298
+ <text fg={theme.colors.warning}> (none)</text>
299
+ ) : (
300
+ wrapNames(cleanPlugins).map((line, i) => (
301
+ // biome-ignore lint/suspicious/noArrayIndexKey: word-wrapped lines of a static preview; lines carry no identity and can repeat, so the row number is the only stable key
302
+ <text key={i} fg={theme.colors.text}>
303
+ {" "}
304
+ {line}
305
+ </text>
306
+ ))
307
+ )}
308
+ </box>
309
+ <box marginTop={2} flexDirection="column">
310
+ <box>
311
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
312
+ {" "}
313
+ Enter/a{" "}
314
+ </text>
315
+ <text fg={theme.colors.muted}> Apply profile</text>
316
+ </box>
317
+ <box marginTop={1}>
318
+ <text bg={theme.colors.dim} fg={theme.hints.fg}>
319
+ {" "}
320
+ r{" "}
321
+ </text>
322
+ <text fg={theme.colors.muted}> Rename</text>
323
+ </box>
324
+ <box marginTop={1}>
325
+ <text bg={theme.colors.danger} fg={theme.hints.fg}>
326
+ {" "}
327
+ d{" "}
328
+ </text>
329
+ <text fg={theme.colors.muted}> Delete</text>
330
+ </box>
331
+ <box marginTop={1}>
332
+ <text bg={theme.colors.link} fg={theme.hints.fg}>
333
+ {" "}
334
+ c{" "}
335
+ </text>
336
+ <text fg={theme.colors.muted}> Copy JSON to clipboard</text>
337
+ </box>
338
+ <box marginTop={1}>
339
+ <text bg={theme.colors.success} fg={theme.hints.fg}>
340
+ {" "}
341
+ i{" "}
342
+ </text>
343
+ <text fg={theme.colors.muted}> Import from clipboard</text>
344
+ </box>
345
+ </box>
346
+ </box>
347
+ );
348
+ },
349
+ };
350
+
351
+ // ─── Dispatch helpers ──────────────────────────────────────────────────────────
352
+
353
+ export function renderProfileRow(
354
+ item: ProfileListItem,
355
+ _index: number,
356
+ isSelected: boolean,
357
+ ): React.ReactNode {
358
+ if (item.kind === "header") {
359
+ return headerRenderer.renderRow({ item, isSelected });
360
+ }
361
+ if (item.kind === "predefined") {
362
+ return predefinedRenderer.renderRow({ item, isSelected });
363
+ }
364
+ return savedRenderer.renderRow({ item, isSelected });
365
+ }
366
+
367
+ export function renderProfileDetail(
368
+ item: ProfileListItem | undefined,
369
+ loading: boolean,
370
+ error: string | undefined,
371
+ ): React.ReactNode {
372
+ if (loading) return <text fg={theme.colors.muted}>Loading profiles...</text>;
373
+ if (error) return <text fg={theme.colors.danger}>Error: {error}</text>;
374
+ if (!item || item.kind === "header")
375
+ return <text fg={theme.colors.muted}>Select a profile to see details</text>;
376
+ if (item.kind === "predefined") {
377
+ return predefinedRenderer.renderDetail({ item });
378
+ }
379
+ return savedRenderer.renderDetail({ item });
380
+ }
381
+
382
+ export function buildProfileListItems(
383
+ profileList: import("../../types/index.js").ProfileEntry[],
384
+ PREDEFINED_PROFILES: PredefinedProfile[],
385
+ ): ProfileListItem[] {
386
+ const items: ProfileListItem[] = [];
387
+ items.push({
388
+ kind: "header",
389
+ label: "Presets",
390
+ color: theme.colors.accent,
391
+ count: PREDEFINED_PROFILES.length,
392
+ });
393
+ for (const p of PREDEFINED_PROFILES) {
394
+ items.push({ kind: "predefined", profile: p });
395
+ }
396
+ items.push({
397
+ kind: "header",
398
+ label: "Your Profiles",
399
+ color: theme.colors.info,
400
+ count: profileList.length,
401
+ });
402
+ for (const e of profileList) {
403
+ items.push({ kind: "saved", entry: e });
404
+ }
405
+ return items;
406
+ }