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,95 @@
1
+ import { useCallback } from "react";
2
+ import { useApp, useNavigation } from "../state/AppContext.js";
3
+ import type { ModalState } from "../state/types.js";
4
+
5
+ /**
6
+ * Startup gitignore-violation modal: magus launches, detects a problem, and
7
+ * asks "add missing ignores / open the tab / dismiss".
8
+ *
9
+ * Pure builder is exported separately for tests.
10
+ */
11
+
12
+ export interface BuildModalArgs {
13
+ violationCount: number;
14
+ safeCount: number;
15
+ onFixSafe: () => void;
16
+ onOpenTab: () => void;
17
+ onDismiss: () => void;
18
+ }
19
+
20
+ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
21
+ const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
22
+ const options = [
23
+ {
24
+ label:
25
+ safeCount > 0
26
+ ? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
27
+ : "(no missing .gitignore entries)",
28
+ value: "fix-safe",
29
+ },
30
+ {
31
+ label: "Open Git State tab to review",
32
+ value: "open-tab",
33
+ },
34
+ {
35
+ label: "Dismiss",
36
+ value: "dismiss",
37
+ },
38
+ ];
39
+ return {
40
+ type: "select",
41
+ title: "Gitignore violations detected",
42
+ message: `Found ${violationCount} managed gitignore item(s) that do not match the current repo state.\n\nMissing .gitignore entries can be added now. Items that change git tracking are reviewed and fixed from the Gitignore tab.`,
43
+ options,
44
+ onSelect: (value: string) => {
45
+ if (value === "fix-safe") onFixSafe();
46
+ else if (value === "open-tab") onOpenTab();
47
+ else onDismiss();
48
+ },
49
+ onCancel: onDismiss,
50
+ };
51
+ }
52
+
53
+ export function useGitignoreModal() {
54
+ const { dispatch } = useApp();
55
+ const { navigateToScreen } = useNavigation();
56
+
57
+ const show = useCallback(
58
+ (args: {
59
+ violationCount: number;
60
+ safeCount: number;
61
+ onFixSafe: () => Promise<void> | void;
62
+ }) => {
63
+ if (args.violationCount === 0) return;
64
+ const dismiss = () => dispatch({ type: "HIDE_MODAL" });
65
+ const modal = buildGitignoreModal({
66
+ violationCount: args.violationCount,
67
+ safeCount: args.safeCount,
68
+ onFixSafe: () => {
69
+ dispatch({ type: "HIDE_MODAL" });
70
+ void Promise.resolve(args.onFixSafe()).catch((err) => {
71
+ dispatch({
72
+ type: "SHOW_MODAL",
73
+ modal: {
74
+ type: "message",
75
+ title: "Safe fixes failed",
76
+ message: err instanceof Error ? err.message : String(err),
77
+ variant: "error",
78
+ onDismiss: dismiss,
79
+ },
80
+ });
81
+ });
82
+ },
83
+ onOpenTab: () => {
84
+ dispatch({ type: "HIDE_MODAL" });
85
+ navigateToScreen("gitignore");
86
+ },
87
+ onDismiss: dismiss,
88
+ });
89
+ dispatch({ type: "SHOW_MODAL", modal });
90
+ },
91
+ [dispatch, navigateToScreen],
92
+ );
93
+
94
+ return { show };
95
+ }
@@ -0,0 +1,24 @@
1
+ import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
+
3
+ /**
4
+ * OpenTUI keyboard event type
5
+ */
6
+ export interface KeyEvent {
7
+ name: string;
8
+ ctrl?: boolean;
9
+ shift?: boolean;
10
+ meta?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Simple re-export of OpenTUI's useKeyboard hook
15
+ * Used by modals for direct keyboard event handling
16
+ *
17
+ * Key names in OpenTUI:
18
+ * - Single characters: "a", "1", etc.
19
+ * - Arrow keys: "up", "down", "left", "right"
20
+ * - Special: "enter", "escape", "tab", "backspace", "delete"
21
+ */
22
+ export function useKeyboard(handler: (event: KeyEvent) => void): void {
23
+ useOpenTUIKeyboard(handler);
24
+ }
@@ -0,0 +1,63 @@
1
+ import { useKeyboard as useOpenTUIKeyboard } from "@opentui/react";
2
+
3
+ /**
4
+ * Wrapper hook for OpenTUI's useKeyboard that provides an Ink-like API
5
+ * Maps Ink key names to OpenTUI key names for easier migration
6
+ */
7
+
8
+ export interface InkLikeKey {
9
+ name: string;
10
+ ctrl: boolean;
11
+ shift: boolean;
12
+ meta: boolean;
13
+ return?: boolean;
14
+ upArrow?: boolean;
15
+ downArrow?: boolean;
16
+ leftArrow?: boolean;
17
+ rightArrow?: boolean;
18
+ escape?: boolean;
19
+ tab?: boolean;
20
+ }
21
+
22
+ type KeyHandler = (input: string, key: InkLikeKey) => void;
23
+
24
+ /**
25
+ * useKeyboardHandler hook
26
+ * Provides Ink-like API for keyboard events
27
+ *
28
+ * @param handler - Callback function with Ink-compatible signature
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * useKeyboardHandler((input, key) => {
33
+ * if (key.return) handleEnter();
34
+ * if (key.upArrow) handleUp();
35
+ * if (key.escape) handleEscape();
36
+ * if (input === 'q') handleQuit();
37
+ * });
38
+ * ```
39
+ */
40
+ export function useKeyboardHandler(handler: KeyHandler): void {
41
+ useOpenTUIKeyboard((event) => {
42
+ // Map OpenTUI key names to Ink-like key names
43
+ const key: InkLikeKey = {
44
+ name: event.name,
45
+ ctrl: event.ctrl || false,
46
+ shift: event.shift || false,
47
+ meta: event.meta || false,
48
+ return: event.name === "enter" || event.name === "return",
49
+ upArrow: event.name === "up",
50
+ downArrow: event.name === "down",
51
+ leftArrow: event.name === "left",
52
+ rightArrow: event.name === "right",
53
+ escape: event.name === "escape",
54
+ tab: event.name === "tab",
55
+ };
56
+
57
+ // For printable characters, pass the character as input
58
+ // For special keys, pass empty string
59
+ const input = event.name.length === 1 ? event.name : "";
60
+
61
+ handler(input, key);
62
+ });
63
+ }
@@ -0,0 +1,33 @@
1
+ import type React from "react";
2
+
3
+ export interface RowRenderProps<T> {
4
+ item: T;
5
+ isSelected: boolean;
6
+ width?: number;
7
+ }
8
+
9
+ export interface DetailRenderProps<T> {
10
+ item: T;
11
+ /**
12
+ * Usable columns in the detail panel. Optional — renderers that only emit
13
+ * short lines can ignore it. Renderers that print wrapped body text need it,
14
+ * because OpenTUI does not reflow: an unwrapped line is clipped, not folded.
15
+ */
16
+ width?: number;
17
+ }
18
+
19
+ export interface Hint {
20
+ key: string;
21
+ label: string;
22
+ tone?: "default" | "primary" | "danger";
23
+ }
24
+
25
+ export interface ItemRenderer<T> {
26
+ renderRow: (props: RowRenderProps<T>) => React.ReactNode;
27
+ renderDetail: (props: DetailRenderProps<T>) => React.ReactNode;
28
+ getActionHints?: (item: T) => Hint[];
29
+ }
30
+
31
+ export type RendererRegistry<TItem extends { kind: string }> = {
32
+ [K in TItem["kind"]]: ItemRenderer<Extract<TItem, { kind: K }>>;
33
+ };
@@ -0,0 +1,326 @@
1
+ import type React from "react";
2
+ import type { CliTool } from "../../data/cli-tools.js";
3
+ import {
4
+ type InstallMethod,
5
+ cliToolUninstall,
6
+ } from "../../services/cli-tool-commands.js";
7
+ import { formatCommand } from "../../utils/command-utils.js";
8
+ import {
9
+ type CliToolRow,
10
+ cliToolStatusText,
11
+ displayVersion,
12
+ isInstalled,
13
+ } from "../adapters/cliToolsAdapter.js";
14
+ import { type UiColor, theme } from "../theme.js";
15
+ import type { StatusTone } from "./pluginStatusText.js";
16
+
17
+ // ─── Helpers ────────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * The uninstall command as a line the reader can copy.
21
+ *
22
+ * This used to be a second, hand-written copy of the screen's own uninstall
23
+ * switch — same shape, and already drifted (`pip uninstall` here,
24
+ * `pip uninstall -y` there). Both spelled a bare `pip`, which on many machines
25
+ * is not an executable. Now the renderer and the screen ask the same function,
26
+ * so what is shown is what would run.
27
+ *
28
+ * Empty string when there is nothing to offer, which is what the caller
29
+ * interpolates into the conflict list.
30
+ */
31
+ function uninstallHint(
32
+ tool: CliTool,
33
+ method: InstallMethod,
34
+ brewFormula?: string,
35
+ ): string {
36
+ const cmd = cliToolUninstall(tool, method, brewFormula);
37
+ return cmd ? formatCommand(cmd) : "";
38
+ }
39
+
40
+ function toneColor(tone: StatusTone): UiColor {
41
+ switch (tone) {
42
+ case "warning":
43
+ return theme.colors.warning;
44
+ case "danger":
45
+ return theme.colors.danger;
46
+ case "success":
47
+ return theme.colors.success;
48
+ case "info":
49
+ return theme.colors.info;
50
+ case "muted":
51
+ return theme.colors.muted;
52
+ }
53
+ }
54
+
55
+ // ─── Row renderer ──────────────────────────────────────────────────────────────
56
+
57
+ export function renderCliToolRow(
58
+ row: CliToolRow,
59
+ _index: number,
60
+ isSelected: boolean,
61
+ ): React.ReactNode {
62
+ const { tool, status, install } = row;
63
+ const installed = isInstalled(status);
64
+ const allMethods = install?.all ?? [];
65
+ const hasConflict = allMethods.length > 1;
66
+
67
+ let icon: string;
68
+ let iconColor: UiColor;
69
+ if (!installed) {
70
+ icon = "○";
71
+ iconColor = theme.colors.muted;
72
+ } else if (hasConflict) {
73
+ icon = "!";
74
+ iconColor = theme.colors.danger;
75
+ } else if (status?.state === "upgrade") {
76
+ icon = "*";
77
+ iconColor = theme.colors.warning;
78
+ } else if (status?.state === "unknown") {
79
+ icon = "?";
80
+ iconColor = theme.colors.warning;
81
+ } else {
82
+ icon = "●";
83
+ iconColor = theme.colors.success;
84
+ }
85
+
86
+ const versionText = status?.installed
87
+ ? ` ${displayVersion(status.installed)}`
88
+ : "";
89
+ const methodTag =
90
+ installed && allMethods.length ? ` ${allMethods.join("+")}` : "";
91
+ const { suffix, tone } = cliToolStatusText(row);
92
+
93
+ if (isSelected) {
94
+ return (
95
+ <text bg={theme.selection.bg} fg={theme.selection.fg}>
96
+ {" "}
97
+ {icon} {tool.displayName}
98
+ {versionText}
99
+ {methodTag}
100
+ {suffix}{" "}
101
+ </text>
102
+ );
103
+ }
104
+
105
+ return (
106
+ <text fg={theme.colors.text}>
107
+ <span fg={iconColor}> {icon}</span>
108
+ <span fg={theme.colors.text}> {tool.displayName}</span>
109
+ {versionText ? (
110
+ <span fg={theme.colors.success}>{versionText}</span>
111
+ ) : null}
112
+ {methodTag ? (
113
+ <span fg={hasConflict ? theme.colors.danger : theme.colors.dim}>
114
+ {methodTag}
115
+ </span>
116
+ ) : null}
117
+ {suffix ? <span fg={toneColor(tone)}>{suffix}</span> : null}
118
+ </text>
119
+ );
120
+ }
121
+
122
+ // ─── Detail renderer ───────────────────────────────────────────────────────────
123
+
124
+ export function renderCliToolDetail(
125
+ row: CliToolRow | undefined,
126
+ ): React.ReactNode {
127
+ if (!row) {
128
+ return (
129
+ <box
130
+ flexDirection="column"
131
+ alignItems="center"
132
+ justifyContent="center"
133
+ flexGrow={1}
134
+ >
135
+ <text fg={theme.colors.muted}>Select a tool to see details</text>
136
+ </box>
137
+ );
138
+ }
139
+
140
+ const { tool, status, checking, install, updateCommand, installCommand } =
141
+ row;
142
+ const installed = isInstalled(status);
143
+ const allMethods = install?.all ?? [];
144
+ const brewFormula = install?.brewFormula;
145
+ const hasConflict = allMethods.length > 1;
146
+ const state = status?.state;
147
+
148
+ return (
149
+ <box flexDirection="column">
150
+ <box marginBottom={1}>
151
+ <text fg={theme.colors.info}>
152
+ <strong>
153
+ {"⚙ "}
154
+ {tool.displayName}
155
+ </strong>
156
+ </text>
157
+ {state === "upgrade" ? <text fg={theme.colors.warning}> ⬆</text> : null}
158
+ {hasConflict ? <text fg={theme.colors.danger}> !</text> : null}
159
+ </box>
160
+
161
+ <text fg={theme.colors.muted}>{tool.description}</text>
162
+
163
+ <box marginTop={1} flexDirection="column">
164
+ <box>
165
+ <text fg={theme.colors.muted}>{"Status "}</text>
166
+ {checking || !status ? (
167
+ <text fg={theme.colors.muted}>{"● Checking..."}</text>
168
+ ) : state === "install" ? (
169
+ <text fg={theme.colors.muted}>{"○ Not installed"}</text>
170
+ ) : state === "upgrade" ? (
171
+ <text fg={theme.colors.warning}>{"● Update available"}</text>
172
+ ) : state === "unknown" ? (
173
+ <text fg={theme.colors.warning}>{"? Unverified"}</text>
174
+ ) : (
175
+ <text fg={theme.colors.success}>{"● Up to date"}</text>
176
+ )}
177
+ </box>
178
+ {state === "unknown" && status?.note ? (
179
+ <box>
180
+ <text fg={theme.colors.muted}>{"Why "}</text>
181
+ <text fg={theme.colors.warning}>{status.note}</text>
182
+ </box>
183
+ ) : null}
184
+ {/* An upgrade that also takes the tool over from another installer. */}
185
+ {status?.adopted ? (
186
+ <box>
187
+ <text fg={theme.colors.muted}>{"Note "}</text>
188
+ <text fg={theme.colors.warning}>{status.adopted}</text>
189
+ </box>
190
+ ) : null}
191
+ {status?.installed ? (
192
+ <box>
193
+ <text fg={theme.colors.muted}>{"Version "}</text>
194
+ <text fg={theme.colors.text}>
195
+ <span fg={theme.colors.success}>
196
+ {displayVersion(status.installed)}
197
+ </span>
198
+ {state === "upgrade" && status.target ? (
199
+ <span fg={theme.colors.warning}>
200
+ {" "}
201
+ → {displayVersion(status.target)}
202
+ </span>
203
+ ) : null}
204
+ </text>
205
+ </box>
206
+ ) : status?.latest ? (
207
+ <box>
208
+ <text fg={theme.colors.muted}>{"Latest "}</text>
209
+ <text fg={theme.colors.text}>{displayVersion(status.latest)}</text>
210
+ </box>
211
+ ) : null}
212
+ {status?.pin ? (
213
+ <box>
214
+ <text fg={theme.colors.muted}>{"Pinned "}</text>
215
+ <text fg={theme.colors.text}>{displayVersion(status.pin)}</text>
216
+ </box>
217
+ ) : null}
218
+ {installed && updateCommand ? (
219
+ <box>
220
+ <text fg={theme.colors.muted}>{"Update "}</text>
221
+ <text fg={theme.colors.accent}>{formatCommand(updateCommand)}</text>
222
+ </box>
223
+ ) : !installed && !checking ? (
224
+ <box>
225
+ <text fg={theme.colors.muted}>{"Install "}</text>
226
+ <text fg={theme.colors.accent}>
227
+ {formatCommand(installCommand)}
228
+ </text>
229
+ </box>
230
+ ) : null}
231
+ <box>
232
+ <text fg={theme.colors.muted}>{"Website "}</text>
233
+ <text fg={theme.colors.link}>{tool.website}</text>
234
+ </box>
235
+ </box>
236
+
237
+ {/* Conflict warning */}
238
+ {hasConflict ? (
239
+ <box marginTop={1} flexDirection="column">
240
+ <box>
241
+ <text bg={theme.colors.danger} fg={theme.hints.fg}>
242
+ <strong>
243
+ {" "}
244
+ Conflict: installed via {allMethods.join(" + ")}{" "}
245
+ </strong>
246
+ </text>
247
+ </box>
248
+ <box marginTop={1}>
249
+ <text fg={theme.colors.muted}>
250
+ Multiple installs can cause version mismatches.
251
+ {"\n"}Keep one, remove the rest:
252
+ </text>
253
+ </box>
254
+ {allMethods.map((method, i) => (
255
+ <box key={method}>
256
+ <text fg={theme.colors.text}>
257
+ <span fg={i === 0 ? theme.colors.success : theme.colors.danger}>
258
+ {i === 0 ? " ● keep " : " ○ remove "}
259
+ </span>
260
+ <span fg={theme.colors.warning}>{method}</span>
261
+ {i > 0 ? (
262
+ <span
263
+ fg={theme.colors.dim}
264
+ >{` ${uninstallHint(tool, method, brewFormula)}`}</span>
265
+ ) : (
266
+ <span fg={theme.colors.dim}> (active in PATH)</span>
267
+ )}
268
+ </text>
269
+ </box>
270
+ ))}
271
+ <box marginTop={1}>
272
+ <text bg={theme.colors.danger} fg={theme.hints.fg}>
273
+ {" "}
274
+ c{" "}
275
+ </text>
276
+ <text fg={theme.colors.muted}> Resolve — pick which to keep</text>
277
+ </box>
278
+ </box>
279
+ ) : null}
280
+
281
+ {/* Actions */}
282
+ <box marginTop={2} flexDirection="column">
283
+ {checking || !status ? null : !installed ? (
284
+ <box>
285
+ <text bg={theme.colors.success} fg={theme.hints.fg}>
286
+ {" "}
287
+ Enter{" "}
288
+ </text>
289
+ <text fg={theme.colors.muted}> Install</text>
290
+ <text fg={theme.colors.dim}> {formatCommand(installCommand)}</text>
291
+ </box>
292
+ ) : state === "upgrade" ? (
293
+ <box>
294
+ <text bg={theme.colors.warning} fg={theme.hints.fg}>
295
+ {" "}
296
+ Enter{" "}
297
+ </text>
298
+ <text fg={theme.colors.muted}>
299
+ {" "}
300
+ Update
301
+ {status.target ? ` to ${displayVersion(status.target)}` : ""}
302
+ </text>
303
+ </box>
304
+ ) : (
305
+ <box flexDirection="column">
306
+ <box>
307
+ <text bg={theme.colors.muted} fg={theme.hints.fg}>
308
+ {" "}
309
+ Enter{" "}
310
+ </text>
311
+ <text fg={theme.colors.muted}>
312
+ {state === "unknown" ? " Update anyway" : " Reinstall"}
313
+ </text>
314
+ </box>
315
+ {/* The status warns that this command may not replace the
316
+ running binary; the menu still offers it, as an explicit
317
+ user action, and says so beside it. */}
318
+ {status.reason === "not-managed" && status.note ? (
319
+ <text fg={theme.colors.warning}>{` ${status.note}`}</text>
320
+ ) : null}
321
+ </box>
322
+ )}
323
+ </box>
324
+ </box>
325
+ );
326
+ }
@@ -0,0 +1,80 @@
1
+ import type React from "react";
2
+ import type { ResolvedRule, Violation } from "../../types/gitignore.js";
3
+ import { theme } from "../theme.js";
4
+
5
+ /**
6
+ * A unified row for the Gitignore screen — represents one resolved rule,
7
+ * annotated with every violation it has against the working tree.
8
+ */
9
+ export interface RuleRow {
10
+ rule: ResolvedRule;
11
+ violations: Violation[];
12
+ }
13
+
14
+ // Two color channels that never collide:
15
+ // • Badge color = direction (where the item ends up): green = tracked by git,
16
+ // reddish = ignored/dropped from git. Same on clean AND violation rows.
17
+ // • Filename color = severity: dim gray when fine, red when it's a violation.
18
+ // No row background tints — red is reserved entirely for "needs attention".
19
+ const TRACK_FG = theme.colors.success; // green — item ends up tracked by git
20
+ const IGNORE_FG = theme.colors.danger; // reddish — item ends up ignored / dropped
21
+ const VIOLATION_PATH_FG = theme.colors.danger; // bright red filename = look here
22
+ const CLEAN_PATH_FG = theme.colors.muted;
23
+ const SELECTED_BG = theme.surface.bg; // faint slate highlight for the cursor row
24
+
25
+ // Violation badges are solid chips so the required direction reads at a glance:
26
+ // green chip = will be tracked, red chip = will be ignored/dropped.
27
+ const TRACK_CHIP_BG = theme.colors.success;
28
+ const IGNORE_CHIP_BG = theme.colors.danger;
29
+ const CHIP_FG = theme.colors.text;
30
+
31
+ // The only marker is the cursor: ▶ on the selected row, blank otherwise.
32
+ // Action (track/ignore) is read from the badge word and, for clean items,
33
+ // from the group the row sits under — so no per-row dot is needed.
34
+ const MARKER_SELECTED = "▶";
35
+ const MARKER_NONE = " ";
36
+
37
+ // Layout: " M action path" — keep the prefix compact so the narrow terminal
38
+ // case still has room for the path without wrapping.
39
+ const MAX_PATH_LEN = 22;
40
+
41
+ function truncatePath(p: string): string {
42
+ return p.length > MAX_PATH_LEN ? `${p.slice(0, MAX_PATH_LEN - 1)}…` : p;
43
+ }
44
+
45
+ export function renderRuleRow(
46
+ row: RuleRow,
47
+ isSelected: boolean,
48
+ ): React.ReactNode {
49
+ const { rule, violations } = row;
50
+ const path = truncatePath(rule.pattern);
51
+ const key = `${rule.action}:${rule.pattern}`;
52
+ // Badge text padded to a fixed width so paths line up in a column.
53
+ const badge = rule.action === "ignore" ? "ignore" : "track ";
54
+ const badgeFg = rule.action === "ignore" ? IGNORE_FG : TRACK_FG;
55
+ const suffix = violations.length > 1 ? ` +${violations.length - 1}` : "";
56
+ const rowBg = isSelected ? SELECTED_BG : undefined;
57
+ const marker = isSelected ? MARKER_SELECTED : MARKER_NONE;
58
+ const isViolation = violations.length > 0;
59
+
60
+ if (isViolation) {
61
+ // Solid badge chip + red filename. The chip color is the fix direction.
62
+ const chipBg = rule.action === "ignore" ? IGNORE_CHIP_BG : TRACK_CHIP_BG;
63
+ return (
64
+ <text key={key} bg={rowBg}>
65
+ <span fg={VIOLATION_PATH_FG}>{` ${marker} `}</span>
66
+ <span bg={chipBg} fg={CHIP_FG}>{` ${badge} `}</span>
67
+ <span fg={VIOLATION_PATH_FG}>{` ${path}${suffix}`}</span>
68
+ </text>
69
+ );
70
+ }
71
+
72
+ // Clean row: flat colored badge word, dim path, no chip.
73
+ return (
74
+ <text key={key} bg={rowBg}>
75
+ <span fg={badgeFg}>{` ${marker} `}</span>
76
+ <span fg={badgeFg}>{badge}</span>
77
+ <span fg={CLEAN_PATH_FG}>{` ${path}`}</span>
78
+ </text>
79
+ );
80
+ }