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
package/src/ui/App.tsx ADDED
@@ -0,0 +1,538 @@
1
+ import fs from "node:fs";
2
+ import { useRenderer } from "@opentui/react";
3
+ import React, { useEffect, useState } from "react";
4
+ import { isClaudeAvailable } from "../services/claude-cli.js";
5
+ import {
6
+ migrateMarketplaceRename,
7
+ recoverMarketplaceSettings,
8
+ } from "../services/claude-settings.js";
9
+ import { checkGitignore } from "../services/gitignore-check.js";
10
+ import {
11
+ applyAllSafeFixes,
12
+ loadGitignoreState,
13
+ } from "../services/gitignore-service.js";
14
+ import { repairAllMarketplaces } from "../services/local-marketplace.js";
15
+ import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
16
+ import {
17
+ type VersionCheckResult,
18
+ checkForUpdates,
19
+ getCurrentVersion,
20
+ } from "../services/version-check.js";
21
+ import { ProgressBar } from "./components/layout/ProgressBar.js";
22
+ import { ModalContainer } from "./components/modals/index.js";
23
+ import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
24
+ import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
25
+ import {
26
+ AliasScreen,
27
+ CliToolsScreen,
28
+ GitignoreScreen,
29
+ McpRegistryScreen,
30
+ McpScreen,
31
+ ModelsScreen,
32
+ PluginsScreen,
33
+ ProfilesScreen,
34
+ SettingsScreen,
35
+ SkillsScreen,
36
+ StylesScreen,
37
+ } from "./screens/index.js";
38
+ import {
39
+ AppProvider,
40
+ useApp,
41
+ useModal,
42
+ useNavigation,
43
+ } from "./state/AppContext.js";
44
+ import {
45
+ DimensionsProvider,
46
+ useDimensions,
47
+ } from "./state/DimensionsContext.js";
48
+ import type { Screen } from "./state/types.js";
49
+ import { theme } from "./theme.js";
50
+
51
+ export const VERSION = getCurrentVersion();
52
+
53
+ /**
54
+ * Router Component
55
+ * Renders the current screen based on route
56
+ */
57
+ function Router() {
58
+ const { state } = useApp();
59
+ const { currentRoute } = state;
60
+
61
+ switch (currentRoute.screen) {
62
+ case "plugins":
63
+ return <PluginsScreen />;
64
+ case "mcp":
65
+ return <McpScreen />;
66
+ case "mcp-registry":
67
+ return <McpRegistryScreen />;
68
+ case "settings":
69
+ return <SettingsScreen />;
70
+ case "cli-tools":
71
+ return <CliToolsScreen />;
72
+ case "profiles":
73
+ return <ProfilesScreen />;
74
+ case "skills":
75
+ return <SkillsScreen />;
76
+ case "styles":
77
+ return <StylesScreen />;
78
+ case "gitignore":
79
+ return <GitignoreScreen />;
80
+ case "alias":
81
+ return <AliasScreen />;
82
+ case "models":
83
+ return <ModelsScreen />;
84
+ default:
85
+ return <PluginsScreen />;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * GlobalKeyHandler Component
91
+ * Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
92
+ * Does not render anything (returns null)
93
+ */
94
+ function GlobalKeyHandler({
95
+ onDebugToggle,
96
+ onExit,
97
+ }: { onDebugToggle: () => void; onExit: () => void }): null {
98
+ const { state } = useApp();
99
+ const { navigateToScreen } = useNavigation();
100
+ const modal = useModal();
101
+ const renderer = useRenderer();
102
+
103
+ useKeyboardHandler((input, key) => {
104
+ // Debug key (Shift+D) - always available
105
+ if (input === "D" && key.shift) {
106
+ onDebugToggle();
107
+ // Write debug info to file
108
+ const debugInfo = {
109
+ timestamp: new Date().toISOString(),
110
+ terminal: { rows: renderer.height, columns: renderer.width },
111
+ state: {
112
+ currentRoute: state.currentRoute,
113
+ isSearching: state.isSearching,
114
+ modal: state.modal ? { type: state.modal.type } : null,
115
+ plugins: {
116
+ scope: state.plugins.scope,
117
+ selectedIndex: state.plugins.selectedIndex,
118
+ searchQuery: state.plugins.searchQuery,
119
+ marketplacesStatus: state.plugins.marketplaces.status,
120
+ pluginsStatus: state.plugins.plugins.status,
121
+ },
122
+ },
123
+ };
124
+ fs.writeFileSync(
125
+ "/tmp/magus-debug.json",
126
+ JSON.stringify(debugInfo, null, 2),
127
+ );
128
+ return;
129
+ }
130
+
131
+ // Don't handle keys when modal is open or searching
132
+ if (state.modal || state.isSearching) return;
133
+
134
+ // Global navigation shortcuts (1-9, then 0) - include mcp-registry as it's
135
+ // a sub-screen of mcp
136
+ const isTopLevel = [
137
+ "plugins",
138
+ "mcp",
139
+ "mcp-registry",
140
+ "settings",
141
+ "cli-tools",
142
+ "profiles",
143
+ "skills",
144
+ "styles",
145
+ "gitignore",
146
+ "alias",
147
+ "models",
148
+ ].includes(state.currentRoute.screen);
149
+
150
+ if (isTopLevel) {
151
+ if (input === "1") navigateToScreen("plugins");
152
+ else if (input === "2") navigateToScreen("skills");
153
+ else if (input === "3") navigateToScreen("mcp");
154
+ else if (input === "4") navigateToScreen("settings");
155
+ else if (input === "5") navigateToScreen("profiles");
156
+ else if (input === "6") navigateToScreen("cli-tools");
157
+ else if (input === "7") navigateToScreen("gitignore");
158
+ else if (input === "8") navigateToScreen("alias");
159
+ else if (input === "9") navigateToScreen("styles");
160
+ else if (input === "0") navigateToScreen("models");
161
+
162
+ // Tab navigation cycling
163
+ if (key.tab) {
164
+ const screens: Screen[] = [
165
+ "plugins",
166
+ "skills",
167
+ "mcp",
168
+ "settings",
169
+ "profiles",
170
+ "cli-tools",
171
+ "gitignore",
172
+ "alias",
173
+ "styles",
174
+ "models",
175
+ ];
176
+ const currentIndex = screens.indexOf(
177
+ state.currentRoute.screen as Screen,
178
+ );
179
+ if (currentIndex !== -1) {
180
+ const nextIndex = key.shift
181
+ ? (currentIndex - 1 + screens.length) % screens.length
182
+ : (currentIndex + 1) % screens.length;
183
+ navigateToScreen(screens[nextIndex]);
184
+ }
185
+ }
186
+ }
187
+
188
+ // q or Ctrl+C quits from anywhere — no need to bounce to home first.
189
+ if (input === "q" || (key.ctrl && input === "c")) {
190
+ onExit();
191
+ return;
192
+ }
193
+
194
+ // Escape goes back: registry → mcp, any other tab → plugins home,
195
+ // already-home → exit. But when a screen has a focused sub-view open
196
+ // (e.g. the Alias flag-detail editor), that sub-view owns Escape and
197
+ // backs out to itself — don't let the global handler jump to home.
198
+ // Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
199
+ if (key.escape) {
200
+ if (state.detailActive) return;
201
+ if (state.currentRoute.screen === "plugins") {
202
+ onExit();
203
+ } else if (state.currentRoute.screen === "mcp-registry") {
204
+ navigateToScreen("mcp");
205
+ } else {
206
+ navigateToScreen("plugins");
207
+ }
208
+ }
209
+
210
+ // ? for help
211
+ if (input === "?") {
212
+ modal.message(
213
+ "magus Help",
214
+ `Navigation
215
+ ↑/↓ or j/k Move selection
216
+ Enter Select / Toggle
217
+ Escape Back (to home)
218
+ q / Ctrl+C Quit (from anywhere)
219
+ ? This help
220
+
221
+ Quick Navigation
222
+ 1 Plugins 4 Settings 7 Git State 0 Models
223
+ 2 Skills 5 Profiles 8 Alias
224
+ 3 MCP Servers 6 CLI Tools 9 Styles
225
+
226
+ Plugin Actions
227
+ u Update d Uninstall
228
+ a Update All r Refresh
229
+
230
+ MCP Servers
231
+ / Search local + remote
232
+ r Browse MCP registry
233
+
234
+ Styles
235
+ Space Tick / untick a style
236
+ a Apply the selection to this project
237
+ x Reset the selection to what is live
238
+ c Clear the active output style
239
+
240
+ Models
241
+ a / Enter Route this project through the selected preset
242
+ c Turn model tiers off
243
+ r Re-read the config`,
244
+ "info",
245
+ );
246
+ }
247
+ });
248
+
249
+ return null;
250
+ }
251
+
252
+ /**
253
+ * UpdateBanner Component
254
+ * Shows version update notification
255
+ */
256
+ function UpdateBanner({ result }: { result: VersionCheckResult }) {
257
+ if (!result.updateAvailable) return null;
258
+
259
+ return (
260
+ <box paddingLeft={1} paddingRight={1}>
261
+ <text bg={theme.colors.warning} fg={theme.hints.fg}>
262
+ <strong> UPDATE </strong>
263
+ </text>
264
+ <text fg={theme.colors.warning}>
265
+ {" "}
266
+ v{result.currentVersion} → v{result.latestVersion}
267
+ </text>
268
+ <text fg={theme.colors.muted}> Run: </text>
269
+ <text fg={theme.colors.info}>magus upgrade</text>
270
+ </box>
271
+ );
272
+ }
273
+
274
+ /**
275
+ * ProgressIndicator Component
276
+ * Shows progress bar when operations are running
277
+ */
278
+ function ProgressIndicator({
279
+ message,
280
+ current,
281
+ total,
282
+ }: {
283
+ message: string;
284
+ current?: number;
285
+ total?: number;
286
+ }) {
287
+ return (
288
+ <box paddingLeft={1} paddingRight={1}>
289
+ <ProgressBar message={message} current={current} total={total} />
290
+ </box>
291
+ );
292
+ }
293
+
294
+ /**
295
+ * AppContentInner Component
296
+ * Main app layout with all UI elements
297
+ */
298
+ interface AppContentInnerProps {
299
+ showDebug: boolean;
300
+ onDebugToggle: () => void;
301
+ updateInfo: VersionCheckResult | null;
302
+ onExit: () => void;
303
+ recoveryReport: string | null;
304
+ }
305
+
306
+ function AppContentInner({
307
+ showDebug,
308
+ onDebugToggle,
309
+ updateInfo,
310
+ onExit,
311
+ recoveryReport,
312
+ }: AppContentInnerProps) {
313
+ const { state } = useApp();
314
+ const { progress } = state;
315
+ const dimensions = useDimensions();
316
+
317
+ return (
318
+ <box flexDirection="column" height={dimensions.terminalHeight}>
319
+ {updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
320
+ {recoveryReport && (
321
+ <box paddingLeft={1} paddingRight={1}>
322
+ <text fg={theme.colors.success}>✓ Fixed: {recoveryReport}</text>
323
+ </box>
324
+ )}
325
+ {showDebug && (
326
+ <box paddingLeft={1} paddingRight={1}>
327
+ <text fg={theme.colors.muted}>
328
+ DEBUG: {dimensions.terminalWidth}x{dimensions.terminalHeight} |
329
+ content={dimensions.contentHeight} | screen=
330
+ {state.currentRoute.screen}
331
+ </text>
332
+ </box>
333
+ )}
334
+ {progress && <ProgressIndicator {...progress} />}
335
+ <box
336
+ flexDirection="column"
337
+ height={dimensions.contentHeight}
338
+ paddingLeft={1}
339
+ paddingRight={1}
340
+ >
341
+ <Router />
342
+ </box>
343
+ <GlobalKeyHandler onDebugToggle={onDebugToggle} onExit={onExit} />
344
+ <ModalContainer />
345
+ </box>
346
+ );
347
+ }
348
+
349
+ /**
350
+ * AppContent Component
351
+ * Wraps app with DimensionsProvider and manages state for debug/updates
352
+ */
353
+ interface AppContentProps {
354
+ onExit: () => void;
355
+ }
356
+
357
+ function AppContent({ onExit }: AppContentProps) {
358
+ const { state, dispatch } = useApp();
359
+ const { progress } = state;
360
+ const [showDebug, setShowDebug] = useState(false);
361
+ const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
362
+ const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
363
+ const [gitignoreData, setGitignoreData] = useState<{
364
+ violationCount: number;
365
+ safeCount: number;
366
+ } | null>(null);
367
+ const gitignoreModal = useGitignoreModal();
368
+
369
+ // Check for updates on startup, then re-check periodically (force-bypassing
370
+ // the session cache) so the banner reflects a release that lands while
371
+ // magus is left open — otherwise it shows the stale startup value.
372
+ useEffect(() => {
373
+ checkForUpdates()
374
+ .then(setUpdateInfo)
375
+ .catch(() => {});
376
+ const id = setInterval(
377
+ () => {
378
+ checkForUpdates(true)
379
+ .then(setUpdateInfo)
380
+ .catch(() => {});
381
+ },
382
+ 15 * 60 * 1000, // 15 min
383
+ );
384
+ return () => clearInterval(id);
385
+ }, []);
386
+
387
+ // Auto-dismiss recovery banner after 5 seconds
388
+ useEffect(() => {
389
+ if (!recoveryReport) return;
390
+ const timer = setTimeout(() => setRecoveryReport(null), 5000);
391
+ return () => clearTimeout(timer);
392
+ }, [recoveryReport]);
393
+
394
+ // Show gitignore modal — wait for the modal slot to be free so we don't
395
+ // stomp on another modal that may have just been shown.
396
+ useEffect(() => {
397
+ if (!gitignoreData) return;
398
+ if (state.modal) return;
399
+ gitignoreModal.show({
400
+ violationCount: gitignoreData.violationCount,
401
+ safeCount: gitignoreData.safeCount,
402
+ onFixSafe: async () => {
403
+ const s = await loadGitignoreState(state.projectPath);
404
+ await applyAllSafeFixes(state.projectPath, s.violations);
405
+ },
406
+ });
407
+ setGitignoreData(null);
408
+ }, [gitignoreData, gitignoreModal, state.modal, state.projectPath]);
409
+
410
+ // Auto-refresh marketplaces on startup
411
+ useEffect(() => {
412
+ const noRefresh = process.argv.includes("--no-refresh");
413
+ if (noRefresh) return;
414
+
415
+ dispatch({
416
+ type: "SHOW_PROGRESS",
417
+ state: { message: "Scanning marketplaces..." },
418
+ });
419
+
420
+ // Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
421
+ migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
422
+
423
+ // Auto-add marketplaces referenced by enabledPlugins but missing from
424
+ // the local registry. Fixes the "fresh clone of a project whose
425
+ // .claude/settings.json enables @magus plugins but this machine has
426
+ // never run `claude plugin marketplace add`" case.
427
+ (async () => {
428
+ try {
429
+ if (!(await isClaudeAvailable())) return;
430
+ const added = await autoAddMissingMarketplaces(process.cwd());
431
+ if (added.length > 0) {
432
+ setRecoveryReport(`added marketplace(s): ${added.join(", ")}`);
433
+ }
434
+ } catch {
435
+ // non-fatal: user can still install marketplaces manually
436
+ }
437
+ })();
438
+
439
+ // Recover stale marketplace registry entries (e.g. "directory" -> "github")
440
+ recoverMarketplaceSettings()
441
+ .then(async (recovery) => {
442
+ const parts: string[] = [];
443
+ if (recovery.reregistered.length > 0) {
444
+ // Update the marketplace clone now that the source is fixed
445
+ const { updateMarketplace } = await import(
446
+ "../services/claude-cli.js"
447
+ );
448
+ for (const mp of recovery.reregistered) {
449
+ try {
450
+ await updateMarketplace(mp);
451
+ parts.push(`${mp} refreshed`);
452
+ } catch {
453
+ parts.push(`${mp} (update failed)`);
454
+ }
455
+ }
456
+ }
457
+ if (recovery.enabledAutoUpdate.length > 0) {
458
+ parts.push(`auto-update: ${recovery.enabledAutoUpdate.join(", ")}`);
459
+ }
460
+ if (recovery.removed.length > 0) {
461
+ parts.push(`removed: ${recovery.removed.join(", ")}`);
462
+ }
463
+ if (parts.length > 0) {
464
+ setRecoveryReport(parts.join(" | "));
465
+ }
466
+ })
467
+ .catch(() => {}); // non-fatal
468
+
469
+ // Check for gitignore manifest violations and surface a popup if any.
470
+ // Loads twice (once for count, once when user picks "fix safe") because
471
+ // the second load happens after edits — keeps detection cheap on startup.
472
+ (async () => {
473
+ try {
474
+ const result = await checkGitignore(process.cwd());
475
+ if (result.violationCount > 0) {
476
+ const s = await loadGitignoreState(process.cwd());
477
+ const safeCount = s.violations.filter(
478
+ (v) => v.severity === "safe",
479
+ ).length;
480
+ setGitignoreData({
481
+ violationCount: result.violationCount,
482
+ safeCount,
483
+ });
484
+ }
485
+ } catch {
486
+ // non-fatal
487
+ }
488
+ })();
489
+
490
+ repairAllMarketplaces()
491
+ .then(async () => {
492
+ dispatch({ type: "HIDE_PROGRESS" });
493
+ dispatch({ type: "DATA_REFRESH_COMPLETE" });
494
+ })
495
+ .catch(() => {
496
+ dispatch({ type: "HIDE_PROGRESS" });
497
+ });
498
+ }, [dispatch]);
499
+
500
+ // Count transient banners for dimension calculation
501
+ const transientBannerCount = recoveryReport ? 1 : 0;
502
+
503
+ return (
504
+ <DimensionsProvider
505
+ showProgress={!!progress}
506
+ showDebug={showDebug}
507
+ showUpdateBanner={!!updateInfo?.updateAvailable}
508
+ transientBannerCount={transientBannerCount}
509
+ >
510
+ <AppContentInner
511
+ showDebug={showDebug}
512
+ onDebugToggle={() => setShowDebug((s) => !s)}
513
+ updateInfo={updateInfo}
514
+ onExit={onExit}
515
+ recoveryReport={recoveryReport}
516
+ />
517
+ </DimensionsProvider>
518
+ );
519
+ }
520
+
521
+ /**
522
+ * App Component (Root)
523
+ * Entry point for the OpenTUI application
524
+ * Wraps everything with AppProvider
525
+ */
526
+ interface AppProps {
527
+ onExit: () => void;
528
+ }
529
+
530
+ export function App({ onExit }: AppProps) {
531
+ return (
532
+ <AppProvider>
533
+ <AppContent onExit={onExit} />
534
+ </AppProvider>
535
+ );
536
+ }
537
+
538
+ export default App;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * catalogNotice.ts — the text of the Plugins screen's warning line.
3
+ *
4
+ * Extracted from PluginsScreen for the same reason as pluginsAdapter: so it can
5
+ * be tested without importing a React/OpenTUI screen. That is not merely tidier —
6
+ * importing the screen into a test pulls the renderer into the shared test process
7
+ * and broke fifteen unrelated tests.
8
+ *
9
+ * What this does NOT warn about, deliberately
10
+ * -------------------------------------------
11
+ * `autoUpdate: false` and a marketplace clone sitting behind its remote are both
12
+ * NORMAL here. Plugins are not auto-updated by policy; the UI's job is to show
13
+ * that an update exists, not to advance the install source. An earlier version of
14
+ * this banner reported both as problems and suggested
15
+ * `claude plugin marketplace update`, which would have defeated the policy it was
16
+ * complaining about. A warning that fires on intended behaviour trains the user to
17
+ * ignore the line.
18
+ *
19
+ * The one thing worth warning about is an update check that did not happen —
20
+ * because the alternative is claiming a plugin is current when nobody looked.
21
+ */
22
+
23
+ import { formatWait } from "../../services/github-budget.js";
24
+ import type { MarketplaceFetchFailure } from "../../services/marketplace-fetcher.js";
25
+
26
+ export interface CatalogNoticeInput {
27
+ /** Catalogs that could not be read from any authoritative source. */
28
+ failures: MarketplaceFetchFailure[];
29
+ /** Installed plugins whose version could not be verified against upstream. */
30
+ unverifiedPlugins: number;
31
+ /** Terminal columns available for the line. */
32
+ width: number;
33
+ /** For the countdown; injected so tests are not clock-dependent. */
34
+ now?: number;
35
+ }
36
+
37
+ /**
38
+ * One line naming why some versions on screen could not be verified — or `null`
39
+ * when everything was actually checked.
40
+ *
41
+ * Returns `null` when there is nothing wrong: a banner that is always present is
42
+ * a banner nobody reads.
43
+ *
44
+ * Segments are added in priority order and dropped from the end when they do not
45
+ * fit `width`. Listing every failing marketplace overflowed an 80-column pane and
46
+ * took the retry hint with it — a warning truncated mid-sentence is barely better
47
+ * than no warning.
48
+ */
49
+ export function buildCatalogNoticeText({
50
+ failures,
51
+ unverifiedPlugins,
52
+ width,
53
+ now = Date.now(),
54
+ }: CatalogNoticeInput): string | null {
55
+ if (failures.length === 0) return null;
56
+
57
+ const segments: string[] = [];
58
+
59
+ // Lead with the reason, not the names: "rate limit" tells the user to wait,
60
+ // "timed out" tells them to check the network. That distinction is the whole
61
+ // value of classifying the failure, and it costs few characters.
62
+ const kinds = new Set(failures.map((f) => f.kind));
63
+ const rateLimited = kinds.has("rate-limited");
64
+ const reason = rateLimited
65
+ ? "GitHub rate limit"
66
+ : kinds.has("timeout")
67
+ ? "GitHub timed out"
68
+ : (failures[0]?.detail ?? "fetch failed");
69
+
70
+ const noun = failures.length === 1 ? "catalog" : "catalogs";
71
+ // Naming them only pays off when there are one or two; past that the count
72
+ // carries the same information in a fraction of the width.
73
+ const named =
74
+ failures.length <= 2
75
+ ? `: ${failures.map((f) => f.marketplace).join(", ")}`
76
+ : "";
77
+ segments.push(`${failures.length} ${noun} unchecked (${reason})${named}`);
78
+
79
+ if (unverifiedPlugins > 0) {
80
+ segments.push(
81
+ `${unverifiedPlugins} version${unverifiedPlugins === 1 ? "" : "s"} unverified`,
82
+ );
83
+ }
84
+
85
+ // The countdown replaces the bare retry hint when we know when the window
86
+ // reopens. Answering "when can I try again" is the point: "try later" leaves
87
+ // the user to guess, and guessing wrong spends another request against the
88
+ // limit. Soonest retry across all failures, since that is when anything at all
89
+ // becomes possible again.
90
+ const retryAt = failures
91
+ .map((f) => f.retryAt)
92
+ .filter((t): t is number => typeof t === "number" && t > now)
93
+ .sort((a, b) => a - b)[0];
94
+
95
+ const hint = retryAt
96
+ ? ` · retrying in ${formatWait(retryAt - now)}`
97
+ : " · r to retry";
98
+
99
+ // Column budget. `⚠` renders double-width in most terminals while
100
+ // `String.length` counts it as one, so one column is held back for it.
101
+ const maxColumns = Math.max(1, width - 1);
102
+
103
+ // The retry affordance is reserved out of the budget rather than appended: it
104
+ // is the only action the line offers, so it is the last thing that may be cut.
105
+ // No floor on what remains — a floor is what let a 40-column pane overflow by
106
+ // two, which is the whole failure mode this budget exists to prevent.
107
+ const messageBudget = maxColumns - hint.length;
108
+
109
+ let line = `⚠ ${segments[0]}`;
110
+ for (const segment of segments.slice(1)) {
111
+ const candidate = `${line} · ${segment}`;
112
+ if (candidate.length > messageBudget) break;
113
+ line = candidate;
114
+ }
115
+ if (line.length > messageBudget) {
116
+ line = messageBudget > 1 ? `${line.slice(0, messageBudget - 1)}…` : "";
117
+ }
118
+
119
+ // Absurdly narrow pane: the hint alone does not fit. Truncate it rather than
120
+ // return a line that overflows and corrupts the layout.
121
+ return `${line}${hint}`.slice(0, maxColumns);
122
+ }