magus-cli 0.0.0-placeholder → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +684 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +445 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +174 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +335 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/moved-marketplace.test.ts +86 -0
  96. package/src/__tests__/open-file.test.ts +59 -0
  97. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  98. package/src/__tests__/plugin-checkout.test.ts +66 -0
  99. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  100. package/src/__tests__/plugin-contents.test.ts +371 -0
  101. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  102. package/src/__tests__/plugin-releases.test.ts +98 -0
  103. package/src/__tests__/plugin-requires.test.ts +187 -0
  104. package/src/__tests__/plugin-row-text.test.ts +336 -0
  105. package/src/__tests__/plugin-setup.test.ts +1016 -0
  106. package/src/__tests__/profile-adopt.test.ts +177 -0
  107. package/src/__tests__/profile-command.test.ts +110 -0
  108. package/src/__tests__/profile-materializer.test.ts +238 -0
  109. package/src/__tests__/profile-sync.test.ts +154 -0
  110. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  111. package/src/__tests__/resolve-executable.test.ts +42 -0
  112. package/src/__tests__/resolver.test.ts +351 -0
  113. package/src/__tests__/scope-action.test.ts +134 -0
  114. package/src/__tests__/scope-squares.test.tsx +164 -0
  115. package/src/__tests__/selection-scope.test.ts +100 -0
  116. package/src/__tests__/settings-file.test.ts +179 -0
  117. package/src/__tests__/shell-script-callers.test.ts +92 -0
  118. package/src/__tests__/skill-install-files.test.ts +136 -0
  119. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  120. package/src/__tests__/style-wrap.test.ts +220 -0
  121. package/src/__tests__/styles-manager.test.ts +1098 -0
  122. package/src/__tests__/styles-origins.test.ts +414 -0
  123. package/src/__tests__/styles-screen-state.test.ts +457 -0
  124. package/src/__tests__/styles-status-line.test.ts +72 -0
  125. package/src/__tests__/styles-sync.test.ts +452 -0
  126. package/src/__tests__/symlink-manager.test.ts +180 -0
  127. package/src/__tests__/tabbar-layout.test.ts +100 -0
  128. package/src/__tests__/terminology-filler.test.ts +214 -0
  129. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  130. package/src/__tests__/theme-env.test.ts +281 -0
  131. package/src/__tests__/theme-resolve.test.ts +380 -0
  132. package/src/__tests__/tool-probe.test.ts +1400 -0
  133. package/src/__tests__/toolchain.test.ts +232 -0
  134. package/src/__tests__/ui-version-writers.test.ts +88 -0
  135. package/src/__tests__/update-apply.test.ts +637 -0
  136. package/src/__tests__/update-plan.test.ts +1373 -0
  137. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  138. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  139. package/src/__tests__/version-check-refresh.test.ts +45 -0
  140. package/src/__tests__/version-snapshot.test.ts +212 -0
  141. package/src/__tests__/version-unknown-state.test.ts +388 -0
  142. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  143. package/src/cli/ansi.ts +520 -0
  144. package/src/cli/bootstrap.ts +134 -0
  145. package/src/cli/doctor.ts +218 -0
  146. package/src/cli/hook.ts +129 -0
  147. package/src/cli/install.ts +382 -0
  148. package/src/cli/live.ts +374 -0
  149. package/src/cli/models.ts +297 -0
  150. package/src/cli/profile.ts +197 -0
  151. package/src/cli/prompt.ts +32 -0
  152. package/src/cli/router.ts +207 -0
  153. package/src/cli/update-view.ts +526 -0
  154. package/src/cli/update.ts +613 -0
  155. package/src/cli/upgrade.ts +97 -0
  156. package/src/data/alias-flags.ts +301 -0
  157. package/src/data/cli-tools.ts +146 -0
  158. package/src/data/community-styles.ts +531 -0
  159. package/src/data/gitignore-defaults.ts +33 -0
  160. package/src/data/gitignore-reasons.ts +93 -0
  161. package/src/data/gitignore-templates.ts +25 -0
  162. package/src/data/marketplaces.ts +278 -0
  163. package/src/data/mcp-servers.ts +51 -0
  164. package/src/data/models-presets.ts +329 -0
  165. package/src/data/predefined-profiles.ts +252 -0
  166. package/src/data/settings-catalog.ts +714 -0
  167. package/src/data/skill-repos.ts +91 -0
  168. package/src/data/styles/asd-ste100.md +38 -0
  169. package/src/data/styles/calibrated.md +24 -0
  170. package/src/data/styles/direct.md +23 -0
  171. package/src/data/styles/evidence-first.md +22 -0
  172. package/src/data/styles/explanatory.md +23 -0
  173. package/src/data/styles/index.ts +57 -0
  174. package/src/data/styles/no-slop.md +32 -0
  175. package/src/data/styles/plain-language.md +23 -0
  176. package/src/data/styles/structured.md +30 -0
  177. package/src/data/styles/terminology.md +28 -0
  178. package/src/data/styles/terse.md +20 -0
  179. package/src/main.tsx +110 -0
  180. package/src/markdown.d.ts +13 -0
  181. package/src/opentui.d.ts +203 -0
  182. package/src/services/active-closure.ts +117 -0
  183. package/src/services/alias-settings.ts +60 -0
  184. package/src/services/alias-shell-writer.ts +1244 -0
  185. package/src/services/alias-store.ts +172 -0
  186. package/src/services/binary-signing.ts +78 -0
  187. package/src/services/catalog-cache-store.ts +316 -0
  188. package/src/services/claude-cli.ts +304 -0
  189. package/src/services/claude-runner.ts +31 -0
  190. package/src/services/claude-settings.ts +1589 -0
  191. package/src/services/cli-tool-commands.ts +133 -0
  192. package/src/services/community-fetcher.ts +90 -0
  193. package/src/services/community-styles.ts +1194 -0
  194. package/src/services/content-drift.ts +208 -0
  195. package/src/services/conventions-manager.ts +945 -0
  196. package/src/services/doctor-bins.ts +63 -0
  197. package/src/services/doctor.ts +570 -0
  198. package/src/services/dotenv.ts +181 -0
  199. package/src/services/git-worktree.ts +129 -0
  200. package/src/services/github-budget.ts +274 -0
  201. package/src/services/gitignore-check.ts +63 -0
  202. package/src/services/gitignore-detector.ts +167 -0
  203. package/src/services/gitignore-fixer.ts +280 -0
  204. package/src/services/gitignore-resolver.ts +208 -0
  205. package/src/services/gitignore-service.ts +167 -0
  206. package/src/services/hook-registration.ts +218 -0
  207. package/src/services/install-plan.ts +107 -0
  208. package/src/services/local-marketplace.ts +587 -0
  209. package/src/services/manifest.ts +250 -0
  210. package/src/services/marketplace-catalog-git.ts +173 -0
  211. package/src/services/marketplace-catalog.ts +129 -0
  212. package/src/services/marketplace-fetcher.ts +400 -0
  213. package/src/services/marketplace-refresh.ts +253 -0
  214. package/src/services/marketplace-sync.ts +145 -0
  215. package/src/services/mate-availability.ts +133 -0
  216. package/src/services/mate-catalog.ts +265 -0
  217. package/src/services/mcp-registry.ts +171 -0
  218. package/src/services/models-core.ts +969 -0
  219. package/src/services/models-manager.ts +606 -0
  220. package/src/services/plugin-checkout.ts +186 -0
  221. package/src/services/plugin-manager.ts +878 -0
  222. package/src/services/plugin-mcp-config.ts +242 -0
  223. package/src/services/plugin-releases.ts +47 -0
  224. package/src/services/plugin-requires.ts +227 -0
  225. package/src/services/plugin-setup.ts +965 -0
  226. package/src/services/profile-adopt.ts +206 -0
  227. package/src/services/profile-materializer.ts +133 -0
  228. package/src/services/profile-sync.ts +158 -0
  229. package/src/services/profiles.ts +225 -0
  230. package/src/services/resolver.ts +344 -0
  231. package/src/services/settings-file.ts +69 -0
  232. package/src/services/settings-manager.ts +307 -0
  233. package/src/services/skills-manager.ts +1111 -0
  234. package/src/services/skillsmp-client.ts +193 -0
  235. package/src/services/styles-manager.ts +1294 -0
  236. package/src/services/symlink-manager.ts +189 -0
  237. package/src/services/terminology-filler.ts +266 -0
  238. package/src/services/tool-probe.ts +1637 -0
  239. package/src/services/toolchain.ts +431 -0
  240. package/src/services/update-engine.ts +691 -0
  241. package/src/services/update-plan.ts +863 -0
  242. package/src/services/version-check.ts +127 -0
  243. package/src/services/version-snapshot.ts +189 -0
  244. package/src/tui.tsx +112 -0
  245. package/src/types/bun.d.ts +27 -0
  246. package/src/types/gitignore.ts +68 -0
  247. package/src/types/index.ts +447 -0
  248. package/src/ui/App.tsx +538 -0
  249. package/src/ui/adapters/catalogNotice.ts +122 -0
  250. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  251. package/src/ui/adapters/modelsAdapter.ts +213 -0
  252. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  253. package/src/ui/adapters/settingsAdapter.ts +168 -0
  254. package/src/ui/adapters/skillsAdapter.ts +229 -0
  255. package/src/ui/adapters/stylesAdapter.ts +403 -0
  256. package/src/ui/components/CategoryHeader.tsx +64 -0
  257. package/src/ui/components/EmptyFilterState.tsx +32 -0
  258. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  259. package/src/ui/components/ScopeIndicator.tsx +63 -0
  260. package/src/ui/components/ScrollableList.tsx +95 -0
  261. package/src/ui/components/SearchInput.tsx +57 -0
  262. package/src/ui/components/StyledText.tsx +75 -0
  263. package/src/ui/components/TabBar.tsx +101 -0
  264. package/src/ui/components/layout/FooterHints.tsx +78 -0
  265. package/src/ui/components/layout/Panel.tsx +63 -0
  266. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  267. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  268. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  269. package/src/ui/components/layout/index.ts +9 -0
  270. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  271. package/src/ui/components/modals/InputModal.tsx +75 -0
  272. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  273. package/src/ui/components/modals/MessageModal.tsx +66 -0
  274. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  275. package/src/ui/components/modals/SelectModal.tsx +90 -0
  276. package/src/ui/components/modals/index.ts +6 -0
  277. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  278. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  279. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  280. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  281. package/src/ui/components/primitives/MetaText.tsx +40 -0
  282. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  283. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  284. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  285. package/src/ui/components/primitives/index.ts +9 -0
  286. package/src/ui/hooks/index.ts +2 -0
  287. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  288. package/src/ui/hooks/useKeyboard.ts +24 -0
  289. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  290. package/src/ui/registry.ts +33 -0
  291. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  292. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  293. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  294. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  295. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  296. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  297. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  298. package/src/ui/renderers/pluginRowText.ts +70 -0
  299. package/src/ui/renderers/pluginStatusText.ts +56 -0
  300. package/src/ui/renderers/profileRenderers.tsx +406 -0
  301. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  302. package/src/ui/renderers/skillRenderers.tsx +479 -0
  303. package/src/ui/renderers/styleRenderers.tsx +813 -0
  304. package/src/ui/screens/AliasScreen.tsx +1558 -0
  305. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  306. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  307. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  308. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  309. package/src/ui/screens/McpScreen.tsx +256 -0
  310. package/src/ui/screens/ModelsScreen.tsx +542 -0
  311. package/src/ui/screens/PluginsScreen.tsx +1464 -0
  312. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  313. package/src/ui/screens/SkillsScreen.tsx +853 -0
  314. package/src/ui/screens/StylesScreen.tsx +1064 -0
  315. package/src/ui/screens/index.ts +11 -0
  316. package/src/ui/state/AppContext.tsx +233 -0
  317. package/src/ui/state/DimensionsContext.tsx +154 -0
  318. package/src/ui/state/reducer.ts +810 -0
  319. package/src/ui/state/types.ts +503 -0
  320. package/src/ui/theme-mode.ts +352 -0
  321. package/src/ui/theme-resolve.ts +147 -0
  322. package/src/ui/theme.ts +175 -0
  323. package/src/utils/clipboard.ts +58 -0
  324. package/src/utils/command-utils.ts +144 -0
  325. package/src/utils/config-dir.ts +45 -0
  326. package/src/utils/file-locking.ts +146 -0
  327. package/src/utils/fuzzy-search.ts +138 -0
  328. package/src/utils/go-module.ts +36 -0
  329. package/src/utils/open-file.ts +84 -0
  330. package/src/utils/run.ts +67 -0
  331. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,127 @@
1
+ import semver from "semver";
2
+ import pkg from "../../package.json";
3
+
4
+ // Cache the result for the session
5
+ let cachedResult: VersionCheckResult | null = null;
6
+
7
+ export interface VersionCheckResult {
8
+ currentVersion: string;
9
+ latestVersion: string;
10
+ updateAvailable: boolean;
11
+ updateType?: "major" | "minor" | "patch";
12
+ }
13
+
14
+ /**
15
+ * Get the current version from package.json.
16
+ * Static import so it resolves inside a `bun build --compile` binary — a
17
+ * dynamic require("../../package.json") returns no `.version` there.
18
+ */
19
+ export function getCurrentVersion(): string {
20
+ return (pkg as { version?: string }).version ?? "0.0.0";
21
+ }
22
+
23
+ /**
24
+ * Fetch the latest version from npm registry
25
+ * Uses a short timeout to avoid blocking startup
26
+ */
27
+ async function fetchLatestVersion(): Promise<string | null> {
28
+ try {
29
+ const controller = new AbortController();
30
+ const timeout = setTimeout(() => controller.abort(), 3000); // 3s timeout
31
+
32
+ const response = await fetch("https://registry.npmjs.org/magus/latest", {
33
+ signal: controller.signal,
34
+ headers: {
35
+ Accept: "application/json",
36
+ // Always revalidate — a CDN/HTTP-cached response serves a stale
37
+ // "latest" right after a release (the 4.25-vs-4.26 flicker).
38
+ "Cache-Control": "no-cache",
39
+ },
40
+ });
41
+
42
+ clearTimeout(timeout);
43
+
44
+ if (!response.ok) {
45
+ return null;
46
+ }
47
+
48
+ const data = (await response.json()) as { version?: string };
49
+ return data.version || null;
50
+ } catch {
51
+ // Network error, timeout, or parse error - silently fail
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Determine the type of update (major, minor, patch)
58
+ */
59
+ function getUpdateType(
60
+ current: string,
61
+ latest: string,
62
+ ): "major" | "minor" | "patch" | undefined {
63
+ const currentParsed = semver.parse(current);
64
+ const latestParsed = semver.parse(latest);
65
+
66
+ if (!currentParsed || !latestParsed) return undefined;
67
+
68
+ if (latestParsed.major > currentParsed.major) return "major";
69
+ if (latestParsed.minor > currentParsed.minor) return "minor";
70
+ if (latestParsed.patch > currentParsed.patch) return "patch";
71
+
72
+ return undefined;
73
+ }
74
+
75
+ /**
76
+ * Check if a new version is available.
77
+ * Returns the session-cached result unless `force` requests a fresh check.
78
+ */
79
+ export async function checkForUpdates(
80
+ force = false,
81
+ ): Promise<VersionCheckResult> {
82
+ // Return the session-cached result unless a fresh check is forced — a periodic
83
+ // re-check passes force:true so a release landing mid-session updates the banner.
84
+ if (!force && cachedResult) {
85
+ return cachedResult;
86
+ }
87
+
88
+ const currentVersion = getCurrentVersion();
89
+ const latestVersion = await fetchLatestVersion();
90
+
91
+ if (!latestVersion) {
92
+ // Couldn't fetch, assume no update
93
+ cachedResult = {
94
+ currentVersion,
95
+ latestVersion: currentVersion,
96
+ updateAvailable: false,
97
+ };
98
+ return cachedResult;
99
+ }
100
+
101
+ // Guard: never let a malformed version crash the update check.
102
+ const updateAvailable =
103
+ semver.valid(latestVersion) != null &&
104
+ semver.valid(currentVersion) != null &&
105
+ semver.gt(latestVersion, currentVersion);
106
+
107
+ cachedResult = {
108
+ currentVersion,
109
+ latestVersion,
110
+ updateAvailable,
111
+ updateType: updateAvailable
112
+ ? getUpdateType(currentVersion, latestVersion)
113
+ : undefined,
114
+ };
115
+
116
+ return cachedResult;
117
+ }
118
+
119
+ /**
120
+ * Format update message for display
121
+ */
122
+ export function formatUpdateMessage(result: VersionCheckResult): string {
123
+ if (!result.updateAvailable) return "";
124
+
125
+ const typeLabel = result.updateType ? ` (${result.updateType})` : "";
126
+ return `Update available: v${result.currentVersion} → v${result.latestVersion}${typeLabel}. Run: npm i -g magus-cli`;
127
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * version-snapshot.ts — detect plugin changes that happened outside magus.
3
+ *
4
+ * Plugins get updated by things magus does not control: Claude Code's own
5
+ * `plugin install`/`update`, or a direct CLI
6
+ * call. All of them write `installed_plugins.json`; none of them tell the user
7
+ * anything the next time magus opens, so versions move underfoot silently.
8
+ *
9
+ * Rather than log what magus itself did — which misses every other source —
10
+ * this records what magus last *saw* installed and diffs against reality on
11
+ * load. Any change is reported, regardless of who made it.
12
+ *
13
+ * The snapshot is advisory UI state, kept separate from the update-check cache
14
+ * so a TTL expiry or a corrupt cache can never make plugins look changed.
15
+ */
16
+
17
+ import { promises as fs } from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+
21
+ /**
22
+ * Sentinel recorded by Claude Code for a plugin that is installed but whose
23
+ * version is unknown — every plugin in the official Anthropic marketplace uses
24
+ * it. It is not a real version and must never be rendered as one.
25
+ */
26
+ export const UNKNOWN_VERSION = "0.0.0";
27
+
28
+ /** True when a version string carries no usable information. */
29
+ export function isKnownVersion(v: string | undefined): v is string {
30
+ return !!v && v !== UNKNOWN_VERSION;
31
+ }
32
+
33
+ export type PluginChangeKind = "installed" | "updated";
34
+
35
+ export interface PluginChange {
36
+ pluginId: string;
37
+ kind: PluginChangeKind;
38
+ /** Previous version. Only set for "updated", and always a known version. */
39
+ from?: string;
40
+ /** Current version. May be UNKNOWN_VERSION — callers must not render it raw. */
41
+ to: string;
42
+ }
43
+
44
+ interface SnapshotFile {
45
+ /**
46
+ * Legacy flat baseline: pluginId -> version, with no record of which project
47
+ * it was observed from. Kept only so an existing file can be migrated.
48
+ */
49
+ seen?: Record<string, string>;
50
+ /**
51
+ * projectPath -> (pluginId -> installedVersion) as of the last time magus
52
+ * rendered that project.
53
+ *
54
+ * Versions are per-project (installed_plugins.json keys installs by
55
+ * projectPath), but this file is global. A single flat map therefore let two
56
+ * projects legitimately on different versions overwrite each other's
57
+ * baseline, and every differing plugin was badged "updated" on the next
58
+ * render — a change that never happened. Keying by project removes the
59
+ * cross-talk entirely.
60
+ */
61
+ byProject?: Record<string, Record<string, string>>;
62
+ /** Set on the run that first created the baseline. */
63
+ seededAt: string;
64
+ updatedAt: string;
65
+ }
66
+
67
+ /**
68
+ * Honour CLAUDE_CONFIG_DIR — the same override Claude Code itself uses.
69
+ *
70
+ * `os.homedir()` reads the passwd database on macOS and ignores $HOME, so a
71
+ * test that only overrides HOME writes to the operator's real snapshot file.
72
+ * That is not hypothetical: it happened while building the tests for this fix.
73
+ */
74
+ function snapshotPath(): string {
75
+ const configDir =
76
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
77
+ return path.join(configDir, "magus-version-snapshot.json");
78
+ }
79
+
80
+ async function read(): Promise<SnapshotFile | null> {
81
+ try {
82
+ const raw = await fs.readFile(snapshotPath(), "utf-8");
83
+ const data = JSON.parse(raw) as SnapshotFile;
84
+ if (!data) return null;
85
+ const hasFlat = typeof data.seen === "object" && data.seen !== null;
86
+ const hasByProject =
87
+ typeof data.byProject === "object" && data.byProject !== null;
88
+ if (!hasFlat && !hasByProject) return null;
89
+ return data;
90
+ } catch {
91
+ return null; // absent or corrupt — treated as "no baseline yet"
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Diff what is installed now against the previous snapshot.
97
+ *
98
+ * Three cases are deliberately silent, because each reflects magus's own
99
+ * knowledge changing rather than anything happening to the user's plugins:
100
+ *
101
+ * 1. No baseline (first run). Every plugin would otherwise be reported as
102
+ * newly installed on the very first launch — a guaranteed false flood that
103
+ * trains the user to ignore the badge. The first run seeds and says nothing.
104
+ *
105
+ * 2. A version moving to or from UNKNOWN_VERSION. That is magus learning
106
+ * (or losing) a version it could not previously resolve — notably after
107
+ * v4.22.0 taught it to read the registry — not the plugin changing.
108
+ *
109
+ * 3. A plugin disappearing. Uninstalls are visible elsewhere; nothing to badge.
110
+ *
111
+ * A downgrade IS reported: rolling back is a real change worth surfacing.
112
+ */
113
+ export function diffVersions(
114
+ previous: Record<string, string> | null,
115
+ current: Record<string, string>,
116
+ ): PluginChange[] {
117
+ if (!previous) return [];
118
+
119
+ const changes: PluginChange[] = [];
120
+ for (const [pluginId, to] of Object.entries(current)) {
121
+ const from = previous[pluginId];
122
+
123
+ // Never seen before → newly installed, not an update. There is no
124
+ // meaningful "from", and `to` may legitimately be UNKNOWN_VERSION.
125
+ if (from === undefined) {
126
+ changes.push({ pluginId, kind: "installed", to });
127
+ continue;
128
+ }
129
+
130
+ if (from === to) continue;
131
+
132
+ // One side carries no real version: we learned or lost it, nothing moved.
133
+ if (!isKnownVersion(from) || !isKnownVersion(to)) continue;
134
+
135
+ changes.push({ pluginId, kind: "updated", from, to });
136
+ }
137
+ return changes;
138
+ }
139
+
140
+ /**
141
+ * Load the baseline for one project, or null when that project has none.
142
+ *
143
+ * A project seen for the first time returns null, so `diffVersions` stays
144
+ * silent — the same rule as a first ever run. That is deliberate: opening
145
+ * magus in a new project must not badge every plugin as "updated" just
146
+ * because this file has never seen that path before.
147
+ *
148
+ * A pre-migration flat file is adopted as the baseline for whichever project
149
+ * asks first, which is the closest thing to the truth it contains.
150
+ */
151
+ export async function loadSeenVersions(
152
+ projectPath: string,
153
+ ): Promise<Record<string, string> | null> {
154
+ const data = await read();
155
+ if (!data) return null;
156
+ const scoped = data.byProject?.[projectPath];
157
+ if (scoped) return scoped;
158
+ // Legacy flat baseline, project unknown — use it once, then it is replaced
159
+ // by per-project entries on save.
160
+ if (data.seen && !data.byProject) return data.seen;
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Persist what is installed now, so the same change is not reported twice.
166
+ * Best-effort: a failure here must never break the UI — it only means the badge
167
+ * shows again next launch.
168
+ */
169
+ export async function saveSeenVersions(
170
+ projectPath: string,
171
+ current: Record<string, string>,
172
+ ): Promise<void> {
173
+ try {
174
+ const file = snapshotPath();
175
+ await fs.mkdir(path.dirname(file), { recursive: true });
176
+ const existing = await read();
177
+ const now = new Date().toISOString();
178
+ const payload: SnapshotFile = {
179
+ // Other projects' baselines are preserved — writing only this project's
180
+ // slot is the entire point of the fix.
181
+ byProject: { ...(existing?.byProject ?? {}), [projectPath]: current },
182
+ seededAt: existing?.seededAt ?? now,
183
+ updatedAt: now,
184
+ };
185
+ await fs.writeFile(file, JSON.stringify(payload, null, 2), "utf-8");
186
+ } catch {
187
+ // ignore
188
+ }
189
+ }
package/src/tui.tsx ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The interactive TUI: renderer creation, theme resolution, and the React root.
3
+ *
4
+ * Split out of `main.tsx` so that nothing on a non-interactive path — above all
5
+ * `magus hook …`, which Claude Code runs on every matching tool call — has
6
+ * to load `@opentui/*` or `src/ui/` to reach its own code. `main.tsx` imports
7
+ * this module dynamically, at the last possible moment.
8
+ */
9
+
10
+ import { RGBA, type ThemeMode, createCliRenderer } from "@opentui/core";
11
+ import { createRoot } from "@opentui/react";
12
+ import { App } from "./ui/App.js";
13
+ import { setThemeMode } from "./ui/theme-mode.js";
14
+ import { type ThemeEnv, resolveTheme } from "./ui/theme-resolve.js";
15
+
16
+ export interface StartTuiOptions {
17
+ /** From `--theme`, already validated by the router. */
18
+ theme?: ThemeMode;
19
+ /** The theme variables, snapshotted in main.tsx BEFORE any .env merge. */
20
+ env: ThemeEnv;
21
+ /** stdin AND stdout are TTYs. Decided by the caller. */
22
+ isInteractive: boolean;
23
+ }
24
+
25
+ /**
26
+ * Start the TUI. Never resolves — the app runs until the user exits.
27
+ */
28
+ export async function startTui({
29
+ theme,
30
+ env,
31
+ isInteractive,
32
+ }: StartTuiOptions): Promise<void> {
33
+ // Create OpenTUI renderer (handles alternate screen buffer automatically).
34
+ //
35
+ // The background is the terminal's own, not a colour of ours: painting an
36
+ // absolute fill would box the UI into whatever theme we guessed. OpenTUI's
37
+ // built-in default for unstyled cells is truecolor white, which is why every
38
+ // element now names an adaptive colour explicitly — see src/ui/theme.ts.
39
+ // The mouse is ON, everywhere — it carries real features now: the wheel
40
+ // scrolls the pane under the cursor, and a drag selects text from ONE pane
41
+ // at a time (terminal-native selection is a screen-wide rectangle that
42
+ // grabs both columns), OSC 52-copied on release below.
43
+ //
44
+ // This deliberately includes tmux. Two earlier revisions got this wrong in
45
+ // opposite directions: one disabled the mouse everywhere on the theory that
46
+ // capture breaks tmux-level pane clicking, the next kept it off only inside
47
+ // tmux. Both dated from when magus had no mouse features, so disabling
48
+ // cost nothing. MEASURED on a heavily tmux'd machine: tmux forwards mouse
49
+ // to a pane application that asks for it (htop in the same session took
50
+ // clicks and wheel fine while magus sat inert), and tmux's own `mouse`
51
+ // option keeps governing pane management. Terminal-native selection stays
52
+ // reachable via Shift+drag in most terminals.
53
+ //
54
+ // enableMouseMovement (mode-1003 "report all motion") stays off — selection
55
+ // needs only button-held drag events, which button reporting delivers.
56
+ const renderer = await createCliRenderer({
57
+ backgroundColor: RGBA.defaultBackground(),
58
+ useMouse: true,
59
+ enableMouseMovement: false,
60
+ });
61
+
62
+ // Mouse selection → system clipboard, on release. OSC 52 survives SSH;
63
+ // terminals that block it simply ignore the sequence. Never fires where the
64
+ // mouse is off, so the tmux path pays nothing.
65
+ renderer.on("selection", (selection: { getSelectedText(): string }) => {
66
+ const text = selection?.getSelectedText() ?? "";
67
+ if (text.length > 0) renderer.copyToClipboardOSC52(text);
68
+ });
69
+
70
+ // Resolve light vs dark once: --theme, MAGUS_THEME, TERM_THEME, the OSC
71
+ // probe, COLORFGBG, dark — see src/ui/theme-resolve.ts. Only the disabled-row
72
+ // tint and the chips need this (src/ui/theme-mode.ts explains why that one
73
+ // case cannot be solved the way the rest of the palette is).
74
+ //
75
+ // The renderer sends OSC 10/11 at creation regardless: @opentui/core 0.1.107
76
+ // has no option to suppress it. So the rule "TERM_THEME skips the probe" is
77
+ // satisfied by NOT AWAITING — when steps 1-3 answer, `probe` is never called,
78
+ // `waitForThemeMode(250)` never runs, first paint is not delayed, and
79
+ // `renderer.themeMode` is never read anywhere in magus. Any late reply the
80
+ // terminal sends is consumed by OpenTUI's own handleSequence and ignored here.
81
+ const { mode } = await resolveTheme({
82
+ flag: theme,
83
+ env,
84
+ isInteractive,
85
+ probe: () => renderer.waitForThemeMode(250).catch(() => null),
86
+ });
87
+ setThemeMode(mode);
88
+
89
+ const root = createRoot(renderer);
90
+
91
+ // Cleanup function to restore terminal
92
+ const cleanup = () => {
93
+ root.unmount();
94
+ renderer.destroy(); // CRITICAL: Never use process.exit() directly
95
+ };
96
+
97
+ // Handle cleanup on exit signals
98
+ const handleExit = () => {
99
+ cleanup();
100
+ // Exit after cleanup completes
101
+ process.exit(0);
102
+ };
103
+
104
+ process.on("SIGINT", handleExit);
105
+ process.on("SIGTERM", handleExit);
106
+
107
+ // Render the OpenTUI app with exit handler
108
+ root.render(<App onExit={handleExit} />);
109
+
110
+ // Wait indefinitely (app runs until user exits)
111
+ await new Promise(() => {});
112
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The sliver of Bun's global this project actually uses.
3
+ *
4
+ * `@types/bun` is deliberately NOT a dependency — `utils/command-utils.ts` says
5
+ * so where it reaches for Node's `fs` instead of `Bun.which` — and the whole
6
+ * package would be pulled in to type two properties. `cli/hook.ts` reads its
7
+ * payload from `Bun.stdin.stream()` because that is the fastest path on the
8
+ * hottest code magus has (a PreToolUse hook runs on every Agent call), so
9
+ * the global is declared here rather than the call being rewritten.
10
+ * `services/tool-probe.ts` reads uv's `uv-receipt.toml` with `Bun.TOML.parse`
11
+ * (the receipt decides whether uv's silence may be read as "current"); a
12
+ * missing parser degrades to "cannot tell", so the member is optional.
13
+ *
14
+ * Keep this MINIMAL. Every member added here is a member nobody checks against
15
+ * the real runtime; a full mirror of Bun's API would be a second, worse copy of
16
+ * `@types/bun` that drifts silently. If this grows past a few members, take the
17
+ * dependency instead.
18
+ */
19
+
20
+ declare namespace Bun {
21
+ const stdin: {
22
+ stream(): AsyncIterable<Uint8Array>;
23
+ };
24
+ const TOML: {
25
+ parse(text: string): unknown;
26
+ };
27
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Types for the gitignore manager: a three-tier (built-in → global → project)
3
+ * manifest system that declares which paths must be ignored and which must
4
+ * stay tracked. Last-wins on conflict across tiers; intra-tier conflicts throw.
5
+ */
6
+
7
+ export type RuleAction = "ignore" | "track";
8
+
9
+ export type Tier = "builtin" | "global" | "project" | "profile";
10
+
11
+ /** A user-authored manifest, deserialized from JSON. */
12
+ export interface GitignoreManifest {
13
+ /** Paths or globs that must be excluded from git. */
14
+ ignore: string[];
15
+ /** Paths that must remain tracked even if a parent is in ignore. */
16
+ track: string[];
17
+ /** Optional template names to merge in (e.g. "common-dev"). */
18
+ extends?: string[];
19
+ }
20
+
21
+ /** A single rule after tier resolution. */
22
+ export interface ResolvedRule {
23
+ pattern: string;
24
+ action: RuleAction;
25
+ source: Tier;
26
+ }
27
+
28
+ /** When two tiers disagree on a pattern, the higher-precedence tier wins. */
29
+ export interface ResolvedConflict {
30
+ pattern: string;
31
+ winner: Tier;
32
+ losers: Array<{ tier: Tier; action: RuleAction }>;
33
+ }
34
+
35
+ export interface ResolvedManifest {
36
+ rules: ResolvedRule[];
37
+ conflicts: ResolvedConflict[];
38
+ }
39
+
40
+ /**
41
+ * Categorized violation state. The detector inspects the working tree against
42
+ * the resolved manifest and emits one of these per offending path.
43
+ *
44
+ * - tracked-but-should-ignore: path is in git index, manifest says ignore
45
+ * - ignored-but-should-track: path matches .gitignore, manifest says track
46
+ * - untracked-and-should-track: path exists but neither tracked nor ignored;
47
+ * manifest says it should be tracked
48
+ * - missing-from-gitignore: path is in manifest.ignore but no .gitignore line
49
+ * covers it (and the path isn't currently tracked)
50
+ */
51
+ export type ViolationKind =
52
+ | "tracked-but-should-ignore"
53
+ | "ignored-but-should-track"
54
+ | "untracked-and-should-track"
55
+ | "missing-from-gitignore";
56
+
57
+ export interface Violation {
58
+ kind: ViolationKind;
59
+ path: string;
60
+ /** Which tier asserted the rule that's being violated. */
61
+ source: Tier;
62
+ /**
63
+ * "safe" — fix is purely additive (append a .gitignore line)
64
+ * "destructive" — fix mutates git index (`git rm --cached`, `git add`,
65
+ * removing existing .gitignore lines)
66
+ */
67
+ severity: "safe" | "destructive";
68
+ }