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,138 @@
1
+ /**
2
+ * Simple fuzzy search utility for filtering lists
3
+ * Uses a scoring algorithm that rewards:
4
+ * - Consecutive character matches
5
+ * - Matches at word boundaries
6
+ * - Matches at start of string
7
+ */
8
+
9
+ export interface FuzzyMatch<T> {
10
+ item: T;
11
+ score: number;
12
+ matches: number[]; // indices of matched characters
13
+ }
14
+
15
+ /**
16
+ * Calculate fuzzy match score for a query against a target string
17
+ * Returns null if no match, otherwise returns score and match indices
18
+ */
19
+ export function fuzzyMatch(
20
+ query: string,
21
+ target: string,
22
+ ): { score: number; matches: number[] } | null {
23
+ if (!query) return { score: 1, matches: [] };
24
+
25
+ const queryLower = query.toLowerCase();
26
+ const targetLower = target.toLowerCase();
27
+
28
+ let queryIdx = 0;
29
+ let score = 0;
30
+ const matches: number[] = [];
31
+ let prevMatchIdx = -1;
32
+
33
+ for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
34
+ if (targetLower[i] === queryLower[queryIdx]) {
35
+ matches.push(i);
36
+
37
+ // Bonus for consecutive matches
38
+ if (prevMatchIdx === i - 1) {
39
+ score += 5;
40
+ }
41
+
42
+ // Bonus for matching at word boundary
43
+ if (i === 0 || /[\s\-_.]/.test(target[i - 1])) {
44
+ score += 10;
45
+ }
46
+
47
+ // Bonus for matching at start
48
+ if (i === 0) {
49
+ score += 15;
50
+ }
51
+
52
+ // Base score for each match
53
+ score += 1;
54
+
55
+ prevMatchIdx = i;
56
+ queryIdx++;
57
+ }
58
+ }
59
+
60
+ // All query characters must be found
61
+ if (queryIdx !== queryLower.length) {
62
+ return null;
63
+ }
64
+
65
+ // Penalty for longer strings (prefer shorter, more precise matches)
66
+ score -= Math.floor(target.length / 10);
67
+
68
+ return { score, matches };
69
+ }
70
+
71
+ /**
72
+ * Filter and sort items by fuzzy match score
73
+ */
74
+ export function fuzzyFilter<T>(
75
+ items: T[],
76
+ query: string,
77
+ getText: (item: T) => string,
78
+ ): FuzzyMatch<T>[] {
79
+ if (!query.trim()) {
80
+ return items.map((item) => ({ item, score: 1, matches: [] }));
81
+ }
82
+
83
+ const results: FuzzyMatch<T>[] = [];
84
+
85
+ for (const item of items) {
86
+ const text = getText(item);
87
+ const match = fuzzyMatch(query, text);
88
+
89
+ if (match) {
90
+ results.push({
91
+ item,
92
+ score: match.score,
93
+ matches: match.matches,
94
+ });
95
+ }
96
+ }
97
+
98
+ // Sort by score descending
99
+ return results.sort((a, b) => b.score - a.score);
100
+ }
101
+
102
+ /**
103
+ * Highlight matched characters in a string
104
+ * Returns array of { text, highlighted } segments
105
+ */
106
+ export function highlightMatches(
107
+ text: string,
108
+ matches: number[],
109
+ ): Array<{ text: string; highlighted: boolean }> {
110
+ if (matches.length === 0) {
111
+ return [{ text, highlighted: false }];
112
+ }
113
+
114
+ const matchSet = new Set(matches);
115
+ const segments: Array<{ text: string; highlighted: boolean }> = [];
116
+ let currentSegment = "";
117
+ let isHighlighted = matchSet.has(0);
118
+
119
+ for (let i = 0; i < text.length; i++) {
120
+ const shouldHighlight = matchSet.has(i);
121
+
122
+ if (shouldHighlight !== isHighlighted) {
123
+ if (currentSegment) {
124
+ segments.push({ text: currentSegment, highlighted: isHighlighted });
125
+ }
126
+ currentSegment = text[i];
127
+ isHighlighted = shouldHighlight;
128
+ } else {
129
+ currentSegment += text[i];
130
+ }
131
+ }
132
+
133
+ if (currentSegment) {
134
+ segments.push({ text: currentSegment, highlighted: isHighlighted });
135
+ }
136
+
137
+ return segments;
138
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Go module path → installed binary name.
3
+ *
4
+ * `go install` names the binary after the last path element of the module
5
+ * path, EXCEPT when that element is a major-version suffix: the module
6
+ * `github.com/MadAppGang/tmux-mcp/v2` installs `tmux-mcp`, not `v2`. Taking a
7
+ * plain basename therefore points every later check at a binary that never
8
+ * exists — presence probes fail, so the module is reinstalled on every run,
9
+ * doctor reports it missing, and the pin comparison never runs at all.
10
+ *
11
+ * Only the Go rule lives here. npm and cargo share the `name@version` syntax
12
+ * but not the suffix convention — an npm scoped package can legitimately be
13
+ * called `@scope/v2` — so this must not be folded into the generic spec parser.
14
+ */
15
+
16
+ /**
17
+ * Trailing major-version suffix per the Go modules spec: `/vN` with N >= 2.
18
+ * Anchored to the END of the path with nothing after `vN`, so a segment that
19
+ * merely starts with "v2" (`github.com/x/v2tool`) is a real name, not a suffix.
20
+ * v0 and v1 are never written as a path suffix. The alternation, not a plain
21
+ * `[2-9][0-9]*`, because that shorthand rejects v10–v19 — a first digit of 1
22
+ * is only excluded when it is the ONLY digit.
23
+ */
24
+ const MAJOR_VERSION_SUFFIX = /\/v(?:[2-9]|[1-9][0-9]+)$/;
25
+
26
+ /**
27
+ * The binary `go install <modulePath>` produces.
28
+ *
29
+ * Takes the MODULE PATH — the part before any `@version` — so callers that
30
+ * hold a full `path@version` spec split it first. An empty or single-segment
31
+ * input is returned unchanged.
32
+ */
33
+ export function goBinaryName(modulePath: string): string {
34
+ const withoutSuffix = modulePath.replace(MAJOR_VERSION_SUFFIX, "");
35
+ return withoutSuffix.split("/").pop() || withoutSuffix;
36
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Hand a file or URL to the operating system's default application.
3
+ *
4
+ * Why this exists rather than launching `$EDITOR`: magus owns the terminal.
5
+ * Suspending an OpenTUI app to give the TTY to a full-screen editor and then
6
+ * restoring raw mode, the alternate screen buffer and the cursor is a reliable
7
+ * way to leave the terminal wedged if anything goes wrong in between. Handing
8
+ * the path to the OS launches a separate process that never touches our TTY.
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+
13
+ export interface Opener {
14
+ command: string;
15
+ /** Arguments that precede the target path. */
16
+ args: string[];
17
+ }
18
+
19
+ /**
20
+ * The platform's "open this with whatever handles it" command.
21
+ *
22
+ * magus ships darwin-arm64, darwin-x64 and linux-x64, so macOS and Linux
23
+ * both have to work — the existing `open "${url}"` call in the Skills screen is
24
+ * macOS-only and silently does nothing on Linux.
25
+ */
26
+ export function openerFor(platform: NodeJS.Platform): Opener {
27
+ if (platform === "darwin") return { command: "open", args: [] };
28
+ // `start` is a cmd.exe builtin, and its first quoted argument is the window
29
+ // title — hence the empty string, or a quoted path would be consumed as one.
30
+ if (platform === "win32")
31
+ return { command: "cmd", args: ["/c", "start", ""] };
32
+ return { command: "xdg-open", args: [] };
33
+ }
34
+
35
+ /**
36
+ * Open `target` in the default application, without blocking the TUI.
37
+ *
38
+ * The child is spawned WITHOUT a shell and the target is passed as its own
39
+ * argv entry, so a filename containing a space, quote or `$(…)` is opened
40
+ * rather than interpreted. Resolves once the process has been spawned, not
41
+ * when it exits — the viewer outlives us by design.
42
+ */
43
+ export function openInDefaultApp(
44
+ target: string,
45
+ platform: NodeJS.Platform = process.platform,
46
+ /**
47
+ * Test seam. The default is the platform's real opener; a test passes a
48
+ * command that cannot exist, so the missing-opener path is exercised
49
+ * deterministically on every OS — CI runners ship `xdg-open`, so "assume
50
+ * the opener is absent" fails exactly there, and "spawn the real one"
51
+ * launches applications on a build machine.
52
+ */
53
+ opener: Opener = openerFor(platform),
54
+ ): Promise<void> {
55
+ const { command, args } = opener;
56
+
57
+ return new Promise((resolve, reject) => {
58
+ let settled = false;
59
+ const child = spawn(command, [...args, target], {
60
+ detached: true,
61
+ stdio: "ignore",
62
+ });
63
+
64
+ child.once("error", (error: NodeJS.ErrnoException) => {
65
+ if (settled) return;
66
+ settled = true;
67
+ reject(
68
+ error.code === "ENOENT"
69
+ ? new Error(
70
+ `No way to open files here — "${command}" is not on PATH.`,
71
+ )
72
+ : error,
73
+ );
74
+ });
75
+
76
+ child.once("spawn", () => {
77
+ if (settled) return;
78
+ settled = true;
79
+ // Detach so magus exiting does not take the editor with it.
80
+ child.unref();
81
+ resolve();
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Running an installer, with the shell split out from the argv.
3
+ *
4
+ * Lives in `utils/` rather than `cli/prompt.ts` because the update engine needs
5
+ * it and a service must not import from the CLI layer — `cli/` and `ui/` are
6
+ * sibling front ends over `services/`, and an arrow from a service back up into
7
+ * one of them is how the two front ends ended up with different update logic in
8
+ * the first place.
9
+ *
10
+ * Both functions live in ONE file on purpose: {@link runShellScript}'s doc only
11
+ * makes sense next to {@link runCommand}, because most of what it has to say is
12
+ * "you almost certainly want the other one".
13
+ */
14
+
15
+ import { spawn } from "node:child_process";
16
+ import type { Command } from "./command-utils.js";
17
+
18
+ /**
19
+ * Run a built command as argv, streaming its output; true on exit 0.
20
+ *
21
+ * **This is the one every installer uses.** No `shell` option, so:
22
+ *
23
+ * - `cmd` is resolved on PATH as an executable. There is no profile to source
24
+ * and therefore no way for a shell alias to appear to work — the reason the
25
+ * shipped `pip install` bug could exist at all was that `/bin/sh` was asked
26
+ * to resolve a word that only interactive zsh knew.
27
+ * - Every argument stays exactly one argument. Package names reach here from
28
+ * `.claude/profiles.json` and plugin `requires.bin` — hand-authored data
29
+ * committed by teammates and fetched from marketplaces — and under a shell a
30
+ * space or a `$(…)` in one of them would have been executed. Nothing
31
+ * validates those names, and after this nothing has to.
32
+ *
33
+ * stdio is inherited: the installer owns the terminal while it runs, which is
34
+ * what makes brew's and bun's own progress output visible. A caller painting an
35
+ * animated region must pause it around this (see `ApplyReporter.detach`).
36
+ */
37
+ export async function runCommand(c: Command): Promise<boolean> {
38
+ return new Promise((resolve) => {
39
+ const child = spawn(c.cmd, [...c.args], { stdio: "inherit" });
40
+ child.on("exit", (code) => resolve(code === 0));
41
+ child.on("error", () => resolve(false));
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Run a shell SCRIPT through `/bin/sh`, streaming its output; true on exit 0.
47
+ *
48
+ * **Not for installer commands — use {@link runCommand}.** This exists for
49
+ * exactly one input: the values of `TOOLCHAIN_BOOTSTRAP`, which are the
50
+ * upstream-published one-liners for bun and Homebrew. Those genuinely are shell
51
+ * text — one contains a pipe, the other a command substitution — so they cannot
52
+ * be expressed as argv without wrapping them in a fake `/bin/sh -c` at the one
53
+ * place where a shell is actually correct.
54
+ *
55
+ * The distinction is who wrote the string. A bootstrap line is a constant in
56
+ * this repo, copied from bun.sh and brew.sh. An installer command is assembled
57
+ * from names that arrive with a profile. Passing the second kind here is the
58
+ * defect that motivated splitting the two, so
59
+ * `src/__tests__/shell-script-callers.test.ts` pins the caller set to one file.
60
+ */
61
+ export async function runShellScript(script: string): Promise<boolean> {
62
+ return new Promise((resolve) => {
63
+ const child = spawn(script, { stdio: "inherit", shell: true });
64
+ child.on("exit", (code) => resolve(code === 0));
65
+ child.on("error", () => resolve(false));
66
+ });
67
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shared string utilities for magus
3
+ */
4
+
5
+ /**
6
+ * Format marketplace name for display
7
+ * e.g., "magus" -> "Magus"
8
+ */
9
+ export function formatMarketplaceName(name: string): string {
10
+ return name
11
+ .split(/[-_]/)
12
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
13
+ .join(" ");
14
+ }
15
+
16
+ /**
17
+ * Validate GitHub repository format (org/repo)
18
+ * Prevents SSRF via malicious repo strings
19
+ */
20
+ export function isValidGitHubRepo(repo: string): boolean {
21
+ if (!repo || typeof repo !== "string") {
22
+ return false;
23
+ }
24
+
25
+ // Must match GitHub username/repo format
26
+ // Username: alphanumeric, hyphens (no consecutive, not start/end)
27
+ // Repo: alphanumeric, hyphens, underscores, dots
28
+ const GITHUB_REPO_PATTERN =
29
+ /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\/[a-zA-Z0-9_.-]+$/;
30
+
31
+ // Allow single-char usernames too
32
+ const GITHUB_REPO_PATTERN_SHORT = /^[a-zA-Z0-9]\/[a-zA-Z0-9_.-]+$/;
33
+
34
+ if (
35
+ !GITHUB_REPO_PATTERN.test(repo) &&
36
+ !GITHUB_REPO_PATTERN_SHORT.test(repo)
37
+ ) {
38
+ return false;
39
+ }
40
+
41
+ // Blocklist suspicious patterns
42
+ const BLOCKLIST = [
43
+ /\.\./, // Path traversal
44
+ /@/, // URL authority injection
45
+ /:/, // Port specification
46
+ /\s/, // Whitespace
47
+ /%/, // URL encoding
48
+ /\\/, // Backslash
49
+ ];
50
+
51
+ return !BLOCKLIST.some((pattern) => pattern.test(repo));
52
+ }
53
+
54
+ /**
55
+ * Parse plugin ID into components
56
+ * Format: pluginName@marketplaceName
57
+ */
58
+ export interface ParsedPluginId {
59
+ pluginName: string;
60
+ marketplace: string;
61
+ raw: string;
62
+ }
63
+
64
+ export function parsePluginId(pluginId: string): ParsedPluginId | null {
65
+ if (!pluginId || typeof pluginId !== "string") {
66
+ return null;
67
+ }
68
+
69
+ const trimmed = pluginId.trim();
70
+ const atIndex = trimmed.lastIndexOf("@");
71
+
72
+ if (atIndex <= 0) {
73
+ return null;
74
+ }
75
+
76
+ const pluginName = trimmed.slice(0, atIndex).trim();
77
+ const marketplace = trimmed.slice(atIndex + 1).trim();
78
+
79
+ if (!pluginName || !marketplace) {
80
+ return null;
81
+ }
82
+
83
+ return {
84
+ pluginName,
85
+ marketplace,
86
+ raw: trimmed,
87
+ };
88
+ }