magus-cli 0.0.0-placeholder → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +684 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +445 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +174 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +335 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/moved-marketplace.test.ts +86 -0
  96. package/src/__tests__/open-file.test.ts +59 -0
  97. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  98. package/src/__tests__/plugin-checkout.test.ts +66 -0
  99. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  100. package/src/__tests__/plugin-contents.test.ts +371 -0
  101. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  102. package/src/__tests__/plugin-releases.test.ts +98 -0
  103. package/src/__tests__/plugin-requires.test.ts +187 -0
  104. package/src/__tests__/plugin-row-text.test.ts +336 -0
  105. package/src/__tests__/plugin-setup.test.ts +1016 -0
  106. package/src/__tests__/profile-adopt.test.ts +177 -0
  107. package/src/__tests__/profile-command.test.ts +110 -0
  108. package/src/__tests__/profile-materializer.test.ts +238 -0
  109. package/src/__tests__/profile-sync.test.ts +154 -0
  110. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  111. package/src/__tests__/resolve-executable.test.ts +42 -0
  112. package/src/__tests__/resolver.test.ts +351 -0
  113. package/src/__tests__/scope-action.test.ts +134 -0
  114. package/src/__tests__/scope-squares.test.tsx +164 -0
  115. package/src/__tests__/selection-scope.test.ts +100 -0
  116. package/src/__tests__/settings-file.test.ts +179 -0
  117. package/src/__tests__/shell-script-callers.test.ts +92 -0
  118. package/src/__tests__/skill-install-files.test.ts +136 -0
  119. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  120. package/src/__tests__/style-wrap.test.ts +220 -0
  121. package/src/__tests__/styles-manager.test.ts +1098 -0
  122. package/src/__tests__/styles-origins.test.ts +414 -0
  123. package/src/__tests__/styles-screen-state.test.ts +457 -0
  124. package/src/__tests__/styles-status-line.test.ts +72 -0
  125. package/src/__tests__/styles-sync.test.ts +452 -0
  126. package/src/__tests__/symlink-manager.test.ts +180 -0
  127. package/src/__tests__/tabbar-layout.test.ts +100 -0
  128. package/src/__tests__/terminology-filler.test.ts +214 -0
  129. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  130. package/src/__tests__/theme-env.test.ts +281 -0
  131. package/src/__tests__/theme-resolve.test.ts +380 -0
  132. package/src/__tests__/tool-probe.test.ts +1400 -0
  133. package/src/__tests__/toolchain.test.ts +232 -0
  134. package/src/__tests__/ui-version-writers.test.ts +88 -0
  135. package/src/__tests__/update-apply.test.ts +637 -0
  136. package/src/__tests__/update-plan.test.ts +1373 -0
  137. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  138. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  139. package/src/__tests__/version-check-refresh.test.ts +45 -0
  140. package/src/__tests__/version-snapshot.test.ts +212 -0
  141. package/src/__tests__/version-unknown-state.test.ts +388 -0
  142. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  143. package/src/cli/ansi.ts +520 -0
  144. package/src/cli/bootstrap.ts +134 -0
  145. package/src/cli/doctor.ts +218 -0
  146. package/src/cli/hook.ts +129 -0
  147. package/src/cli/install.ts +382 -0
  148. package/src/cli/live.ts +374 -0
  149. package/src/cli/models.ts +297 -0
  150. package/src/cli/profile.ts +197 -0
  151. package/src/cli/prompt.ts +32 -0
  152. package/src/cli/router.ts +207 -0
  153. package/src/cli/update-view.ts +526 -0
  154. package/src/cli/update.ts +613 -0
  155. package/src/cli/upgrade.ts +97 -0
  156. package/src/data/alias-flags.ts +301 -0
  157. package/src/data/cli-tools.ts +146 -0
  158. package/src/data/community-styles.ts +531 -0
  159. package/src/data/gitignore-defaults.ts +33 -0
  160. package/src/data/gitignore-reasons.ts +93 -0
  161. package/src/data/gitignore-templates.ts +25 -0
  162. package/src/data/marketplaces.ts +278 -0
  163. package/src/data/mcp-servers.ts +51 -0
  164. package/src/data/models-presets.ts +329 -0
  165. package/src/data/predefined-profiles.ts +252 -0
  166. package/src/data/settings-catalog.ts +714 -0
  167. package/src/data/skill-repos.ts +91 -0
  168. package/src/data/styles/asd-ste100.md +38 -0
  169. package/src/data/styles/calibrated.md +24 -0
  170. package/src/data/styles/direct.md +23 -0
  171. package/src/data/styles/evidence-first.md +22 -0
  172. package/src/data/styles/explanatory.md +23 -0
  173. package/src/data/styles/index.ts +57 -0
  174. package/src/data/styles/no-slop.md +32 -0
  175. package/src/data/styles/plain-language.md +23 -0
  176. package/src/data/styles/structured.md +30 -0
  177. package/src/data/styles/terminology.md +28 -0
  178. package/src/data/styles/terse.md +20 -0
  179. package/src/main.tsx +110 -0
  180. package/src/markdown.d.ts +13 -0
  181. package/src/opentui.d.ts +203 -0
  182. package/src/services/active-closure.ts +117 -0
  183. package/src/services/alias-settings.ts +60 -0
  184. package/src/services/alias-shell-writer.ts +1244 -0
  185. package/src/services/alias-store.ts +172 -0
  186. package/src/services/binary-signing.ts +78 -0
  187. package/src/services/catalog-cache-store.ts +316 -0
  188. package/src/services/claude-cli.ts +304 -0
  189. package/src/services/claude-runner.ts +31 -0
  190. package/src/services/claude-settings.ts +1589 -0
  191. package/src/services/cli-tool-commands.ts +133 -0
  192. package/src/services/community-fetcher.ts +90 -0
  193. package/src/services/community-styles.ts +1194 -0
  194. package/src/services/content-drift.ts +208 -0
  195. package/src/services/conventions-manager.ts +945 -0
  196. package/src/services/doctor-bins.ts +63 -0
  197. package/src/services/doctor.ts +570 -0
  198. package/src/services/dotenv.ts +181 -0
  199. package/src/services/git-worktree.ts +129 -0
  200. package/src/services/github-budget.ts +274 -0
  201. package/src/services/gitignore-check.ts +63 -0
  202. package/src/services/gitignore-detector.ts +167 -0
  203. package/src/services/gitignore-fixer.ts +280 -0
  204. package/src/services/gitignore-resolver.ts +208 -0
  205. package/src/services/gitignore-service.ts +167 -0
  206. package/src/services/hook-registration.ts +218 -0
  207. package/src/services/install-plan.ts +107 -0
  208. package/src/services/local-marketplace.ts +587 -0
  209. package/src/services/manifest.ts +250 -0
  210. package/src/services/marketplace-catalog-git.ts +173 -0
  211. package/src/services/marketplace-catalog.ts +129 -0
  212. package/src/services/marketplace-fetcher.ts +400 -0
  213. package/src/services/marketplace-refresh.ts +253 -0
  214. package/src/services/marketplace-sync.ts +145 -0
  215. package/src/services/mate-availability.ts +133 -0
  216. package/src/services/mate-catalog.ts +265 -0
  217. package/src/services/mcp-registry.ts +171 -0
  218. package/src/services/models-core.ts +969 -0
  219. package/src/services/models-manager.ts +606 -0
  220. package/src/services/plugin-checkout.ts +186 -0
  221. package/src/services/plugin-manager.ts +878 -0
  222. package/src/services/plugin-mcp-config.ts +242 -0
  223. package/src/services/plugin-releases.ts +47 -0
  224. package/src/services/plugin-requires.ts +227 -0
  225. package/src/services/plugin-setup.ts +965 -0
  226. package/src/services/profile-adopt.ts +206 -0
  227. package/src/services/profile-materializer.ts +133 -0
  228. package/src/services/profile-sync.ts +158 -0
  229. package/src/services/profiles.ts +225 -0
  230. package/src/services/resolver.ts +344 -0
  231. package/src/services/settings-file.ts +69 -0
  232. package/src/services/settings-manager.ts +307 -0
  233. package/src/services/skills-manager.ts +1111 -0
  234. package/src/services/skillsmp-client.ts +193 -0
  235. package/src/services/styles-manager.ts +1294 -0
  236. package/src/services/symlink-manager.ts +189 -0
  237. package/src/services/terminology-filler.ts +266 -0
  238. package/src/services/tool-probe.ts +1637 -0
  239. package/src/services/toolchain.ts +431 -0
  240. package/src/services/update-engine.ts +691 -0
  241. package/src/services/update-plan.ts +863 -0
  242. package/src/services/version-check.ts +127 -0
  243. package/src/services/version-snapshot.ts +189 -0
  244. package/src/tui.tsx +112 -0
  245. package/src/types/bun.d.ts +27 -0
  246. package/src/types/gitignore.ts +68 -0
  247. package/src/types/index.ts +447 -0
  248. package/src/ui/App.tsx +538 -0
  249. package/src/ui/adapters/catalogNotice.ts +122 -0
  250. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  251. package/src/ui/adapters/modelsAdapter.ts +213 -0
  252. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  253. package/src/ui/adapters/settingsAdapter.ts +168 -0
  254. package/src/ui/adapters/skillsAdapter.ts +229 -0
  255. package/src/ui/adapters/stylesAdapter.ts +403 -0
  256. package/src/ui/components/CategoryHeader.tsx +64 -0
  257. package/src/ui/components/EmptyFilterState.tsx +32 -0
  258. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  259. package/src/ui/components/ScopeIndicator.tsx +63 -0
  260. package/src/ui/components/ScrollableList.tsx +95 -0
  261. package/src/ui/components/SearchInput.tsx +57 -0
  262. package/src/ui/components/StyledText.tsx +75 -0
  263. package/src/ui/components/TabBar.tsx +101 -0
  264. package/src/ui/components/layout/FooterHints.tsx +78 -0
  265. package/src/ui/components/layout/Panel.tsx +63 -0
  266. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  267. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  268. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  269. package/src/ui/components/layout/index.ts +9 -0
  270. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  271. package/src/ui/components/modals/InputModal.tsx +75 -0
  272. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  273. package/src/ui/components/modals/MessageModal.tsx +66 -0
  274. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  275. package/src/ui/components/modals/SelectModal.tsx +90 -0
  276. package/src/ui/components/modals/index.ts +6 -0
  277. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  278. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  279. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  280. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  281. package/src/ui/components/primitives/MetaText.tsx +40 -0
  282. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  283. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  284. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  285. package/src/ui/components/primitives/index.ts +9 -0
  286. package/src/ui/hooks/index.ts +2 -0
  287. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  288. package/src/ui/hooks/useKeyboard.ts +24 -0
  289. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  290. package/src/ui/registry.ts +33 -0
  291. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  292. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  293. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  294. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  295. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  296. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  297. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  298. package/src/ui/renderers/pluginRowText.ts +70 -0
  299. package/src/ui/renderers/pluginStatusText.ts +56 -0
  300. package/src/ui/renderers/profileRenderers.tsx +406 -0
  301. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  302. package/src/ui/renderers/skillRenderers.tsx +479 -0
  303. package/src/ui/renderers/styleRenderers.tsx +813 -0
  304. package/src/ui/screens/AliasScreen.tsx +1558 -0
  305. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  306. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  307. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  308. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  309. package/src/ui/screens/McpScreen.tsx +256 -0
  310. package/src/ui/screens/ModelsScreen.tsx +542 -0
  311. package/src/ui/screens/PluginsScreen.tsx +1464 -0
  312. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  313. package/src/ui/screens/SkillsScreen.tsx +853 -0
  314. package/src/ui/screens/StylesScreen.tsx +1064 -0
  315. package/src/ui/screens/index.ts +11 -0
  316. package/src/ui/state/AppContext.tsx +233 -0
  317. package/src/ui/state/DimensionsContext.tsx +154 -0
  318. package/src/ui/state/reducer.ts +810 -0
  319. package/src/ui/state/types.ts +503 -0
  320. package/src/ui/theme-mode.ts +352 -0
  321. package/src/ui/theme-resolve.ts +147 -0
  322. package/src/ui/theme.ts +175 -0
  323. package/src/utils/clipboard.ts +58 -0
  324. package/src/utils/command-utils.ts +144 -0
  325. package/src/utils/config-dir.ts +45 -0
  326. package/src/utils/file-locking.ts +146 -0
  327. package/src/utils/fuzzy-search.ts +138 -0
  328. package/src/utils/go-module.ts +36 -0
  329. package/src/utils/open-file.ts +84 -0
  330. package/src/utils/run.ts +67 -0
  331. package/src/utils/string-utils.ts +88 -0
package/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # magus
2
+
3
+ Manage a Claude Code setup — plugins, MCP servers, skills, CLI tools and the
4
+ binaries plugins depend on — from a TUI, a CLI, or a committed team manifest.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ bun add -g magus-cli # recommended
10
+ npm install -g magus-cli # also works
11
+ ```
12
+
13
+ Updates go through magus itself, not the package manager:
14
+
15
+ ```bash
16
+ magus upgrade
17
+ ```
18
+
19
+ `upgrade` advances **magus**. `update` advances **your dependencies** — the
20
+ plugins, CLI tools and skills your profile declares. Same split as every package
21
+ manager, so the two are never confused.
22
+
23
+ You get a self-contained binary for your platform (an `optionalDependencies`
24
+ package gated on `os`/`cpu`). On a platform without a prebuilt binary the
25
+ launcher falls back to running from source under Bun, so the install still works.
26
+
27
+ ## Two ways to use it
28
+
29
+ **Interactive** — run `magus` with no arguments for the TUI. Good for
30
+ exploring what is available and toggling things on one machine.
31
+
32
+ **Declarative** — commit a `.claude/profiles.json` and run `magus install`.
33
+ Good for making a teammate's machine match yours. This is the
34
+ [team configuration guide](docs/team-configuration.md).
35
+
36
+ ## Commands
37
+
38
+ ```
39
+ magus Open the interactive TUI
40
+
41
+ install [profile] Install a profile's plugins, binaries, skills and
42
+ env at the manifest's PINNED versions, then activate
43
+ it. No argument = every profile.
44
+ --check Report drift only, write nothing (CI gate)
45
+ --yes, -y Skip the confirmation prompt
46
+ --force Discard unsynced local edits instead of refusing
47
+ update [profile] Bring the ACTIVE profile up to date: install what it
48
+ declares but this machine lacks, and advance every
49
+ "latest" pin to the newest published version.
50
+ --check Report only, write nothing (exits 1 if anything is
51
+ missing or known to be behind)
52
+ --yes, -y Skip the confirmation prompt
53
+ profile list Show every profile; ● marks the active one
54
+ profile show <name> Print a profile's fully resolved closure
55
+ profile switch <name> Repoint the active profile — offline, no reinstall
56
+ profile init Write .claude/profiles.json from this project's setup
57
+ profile sync Promote local edits back into .claude/profiles.json
58
+ doctor [--fix] Check binary deps, profile symlinks, conventions
59
+
60
+ claude [args...] Check for plugin updates (1h cache), then run claude
61
+ --force, -f Force the update check
62
+ upgrade Update magus itself
63
+ --version, -v Version + update check
64
+ --help, -h This list
65
+ --theme light|dark Force the palette. Otherwise: MAGUS_THEME, then
66
+ TERM_THEME, then a 250 ms terminal query, then
67
+ COLORFGBG, then dark. Only the exact words
68
+ "light"/"dark" count.
69
+ ```
70
+
71
+ ### Light or dark
72
+
73
+ magus decides once at startup, first answer wins: `--theme`, then the
74
+ `MAGUS_THEME` variable, then `TERM_THEME`, then a bounded (250 ms) wait for
75
+ the terminal's OSC 10/11 reply — only when stdin and stdout are both TTYs — then
76
+ `COLORFGBG`, then dark. A word that is not exactly `light` or `dark` (so `auto`,
77
+ `Light`, an empty value) is no opinion and the next step runs; when the flag is
78
+ given a bad word, that is an error naming both accepted values.
79
+
80
+ `TERM_THEME` and `MAGUS_THEME` are read from the process environment, never
81
+ from a project's `.env`. Running from source needs `--no-env-file`, because Bun
82
+ otherwise autoloads the cwd's `.env` before any code runs; the package scripts
83
+ and the `bin/magus.js` source fallback already pass it, and the compiled
84
+ binary never autoloads. The OSC 10/11 query itself is emitted by OpenTUI
85
+ (0.1.107) when the renderer is created, regardless of the outcome; when an
86
+ earlier step answered, magus never awaits or uses the reply, so first paint
87
+ is not delayed and the terminal's answer has no influence.
88
+
89
+ ### install vs update
90
+
91
+ They differ in one thing: whether the pins are allowed to move.
92
+
93
+ | | `install` | `update` |
94
+ |---|---|---|
95
+ | Versions | exactly what the manifest pins | advances every `"latest"` pin |
96
+ | Scope | every profile (or the one named) | the **active** profile |
97
+ | Also does | materializes + activates the profile | nothing to the manifest |
98
+
99
+ Both install whatever the profile declares and this machine lacks, so `update`
100
+ covers the "I ticked it but never installed it" case on its own.
101
+
102
+ **This is dependency management, not a package manager.** There is no resolver,
103
+ no lockfile and no version negotiation. The job is: everything the profile
104
+ declares is installed, and current.
105
+
106
+ That matters for exact pins. `claude plugin install` takes no version argument —
107
+ it installs whatever the marketplace publishes now — so a pin naming anything
108
+ else cannot be satisfied. `update` plans against the version it can actually
109
+ get, notes the shortfall, and moves on:
110
+
111
+ ```
112
+ dev@magus = 9.9.9 (manifest pins 4.6.1; the marketplace offers only 9.9.9)
113
+ ```
114
+
115
+ It records what actually landed, never the pin, and it does **not** fail over
116
+ the difference — an unsatisfiable pin is a standing property of the manifest,
117
+ not something a given run did wrong. Pin drift is gated where it always was:
118
+ `magus install --check` with `strictVersions` in the manifest.
119
+
120
+ **`update` only ever moves forward.** Nothing here can downgrade — the installer
121
+ takes no version — so an install that is already ahead of what the marketplace
122
+ publishes is left alone rather than dragged back. A plan that tried would emit a
123
+ step that runs, reports success, changes nothing, and reappears on every
124
+ subsequent run.
125
+
126
+ `--check` fails on things that are **missing or known to be behind**. It
127
+ deliberately does not fail on the two items that get re-fetched every run
128
+ regardless: an already-installed skill (a manifest skill ref carries no content
129
+ hash, so there is nothing to compare) and a present unpinned binary (no version
130
+ probe exists, only present/absent). A gate that can never pass gets switched
131
+ off, so it only fires on real drift.
132
+
133
+ Neither command needs a manifest to exist first. In a repo without
134
+ `.claude/profiles.json`, both offer to write one describing the project as it
135
+ stands — that is `magus profile init`, which you can also run on its own.
136
+ Adoption reads the project's own `.claude/settings.json`; if that enables
137
+ nothing it falls back to your user-scope settings and says so, because the file
138
+ is meant to be committed and one machine's global set is not a team contract.
139
+ Machine-only keys are stripped, and any settings value naming a path under your
140
+ home directory is flagged for review.
141
+
142
+ `--yes` alone will **not** create a manifest. It means "don't ask me about the
143
+ command I ran", and someone running `magus update --yes` in CI did not ask to
144
+ author their team's committed contract. Creating it needs a human at the prompt,
145
+ or `magus profile init`.
146
+
147
+ `magus doctor` exits non-zero when it finds a problem, so it works as a CI
148
+ check. So does `magus install --check`.
149
+
150
+ ## What the TUI covers
151
+
152
+ | Screen | What it manages |
153
+ |---|---|
154
+ | Plugins | Install, enable, disable; shows version changes and newly installed plugins |
155
+ | Skills | Browse and install skills from configured skill repos |
156
+ | MCP | Add MCP servers from a curated catalog |
157
+ | Settings | Claude Code settings, from a catalog of known keys |
158
+ | Profiles | Saved plugin sets (the TUI's own, older profile concept) |
159
+ | CLI Tools | Install claudish, mnemex and friends via the right package manager |
160
+ | Git State | Gitignore conventions and repo hygiene |
161
+ | Alias | Shell alias + flag management for launching `claude` |
162
+ | Styles | Communication style presets, composed into one native output style |
163
+
164
+ Navigate with `↑/↓` or `j/k`, `Enter` to select, `r` to refresh, `?` for help,
165
+ `q`/`Escape` to go back. Number keys `1`–`9` jump straight to a screen.
166
+
167
+ ## Styles
168
+
169
+ Claude Code activates exactly one output style at a time. The Styles screen (`9`)
170
+ composes several rule blocks into that one style, so choices that would otherwise
171
+ compete for the single slot end up in one file.
172
+
173
+ One **verbosity axis** — pick exactly one of `direct`, `explanatory`, or `terse` —
174
+ plus any number of free modifiers:
175
+
176
+ | Preset | Effect |
177
+ |---|---|
178
+ | `direct` | Lead with the answer, minimal preamble |
179
+ | `explanatory` | Teach as you go, more context per step |
180
+ | `terse` | Shortest useful output |
181
+ | `no-slop` | Bans filler, hype, and AI vocabulary |
182
+ | `asd-ste100` | ASD-STE100 Simplified Technical English: sentences that survive one read |
183
+ | `evidence-first` | Claims must carry a citation, command output, or file reference |
184
+ | `calibrated` | State confidence honestly; no false certainty |
185
+ | `plain-language` | Prefer plain words over jargon |
186
+ | `structured` | Headings, tables, and lists over long paragraphs |
187
+ | `terminology` | Enforce project-specific vocabulary, filled in from your codebase |
188
+
189
+ Two verbosity presets is the one combination that does not work: `terse` says "do
190
+ not explain unless asked" and `explanatory` says "explain the specific choice", so
191
+ together they cancel and the model resolves the contradiction differently every
192
+ turn. Everything else stacks.
193
+
194
+ The presets live in `src/data/styles/*.md` and are compiled into the binary, so
195
+ they are always the version this magus was built from.
196
+
197
+ ### What gets written
198
+
199
+ ```
200
+ .claude/output-styles/composed.md the composed rules (generated — do not hand-edit)
201
+ .claude/settings.json "outputStyle": "composed"
202
+ .claude/style.json the declaration — commit this to share it
203
+ ```
204
+
205
+ With a profile active the file is named `composed-<profile>` and the choice is
206
+ also recorded in `.claude/profiles.json`, because `magus install` rebuilds
207
+ `settings.json` from the manifest and would otherwise drop it.
208
+
209
+ `style.json` is the reviewable statement of what the project wants; `composed.md`
210
+ is the artifact Claude Code reads. A teammate who clones the repo gets the
211
+ declaration, and magus tells them when their local artifact does not match it.
212
+
213
+ ### Styles you already have
214
+
215
+ Output styles you wrote yourself compose too — magus finds them in
216
+ `~/.claude/output-styles/` and `.claude/output-styles/` and lists them alongside
217
+ the presets. Community styles can be fetched from their source repository on an
218
+ explicit action, and are cached outside `~/.claude/output-styles/` so a re-fetch
219
+ never overwrites something you wrote.
220
+
221
+ Built-in styles ship inside the Claude Code binary rather than on disk, so there
222
+ is no file to import until you capture one. `--discover` names the set the
223
+ installed version actually has — it was `Concise`, `Explanatory`, `Learning` and
224
+ `Proactive` on Claude Code 2.1.239, and Anthropic adds to it:
225
+
226
+ ```bash
227
+ bun scripts/capture-builtin.ts --discover # what Anthropic ships today
228
+ bun scripts/capture-builtin.ts --all # capture every built-in
229
+ bun scripts/capture-builtin.ts --check # what fell behind after an upgrade
230
+ ```
231
+
232
+ Each capture runs one real `claude -p` round trip behind a transparent proxy and
233
+ records the system prompt Claude Code actually sent, writing
234
+ `~/.claude/output-styles/builtin-<name>.md`. Captures are per machine and stamped
235
+ with the Claude Code version they came from — **re-run `--check` after every
236
+ upgrade**. Nothing is committed to this repo: the text is Anthropic's, and it
237
+ changes on their release schedule, not ours.
238
+
239
+ ### Coding rules stay on
240
+
241
+ Every generated style sets `keep-coding-instructions: true`. Without that flag
242
+ Claude Code drops its own coding-discipline rules from the system prompt, and a
243
+ setting about how to *communicate* has no business switching off how code gets
244
+ written. `force-for-plugin` is never set either — that would override your own
245
+ `/output-style` choice.
246
+
247
+ ## Files magus touches
248
+
249
+ | Path | Owner | What magus does |
250
+ |---|---|---|
251
+ | `.claude/profiles.json` | you, committed | reads it; `profile sync` writes to it |
252
+ | `.claude/_profiles/<name>/` | magus, gitignored | generated build output |
253
+ | `.claude/settings.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
254
+ | `.claude/settings.local.json` | you, gitignored | env values collected during install |
255
+ | `.claude/style.json` | you, committed | the project's declared style — written on apply |
256
+ | `.claude/output-styles/composed*.md` | magus, committed | the generated style Claude Code reads |
257
+ | `.mcp.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
258
+ | `~/.claude/plugins/*` | Claude Code | never written directly — always via the `claude` CLI |
259
+
260
+ magus never hand-edits Claude Code's own registries
261
+ (`installed_plugins.json`, `known_marketplaces.json`, the plugin cache). Those go
262
+ through `claude plugin ...` so Claude Code stays the single writer.
263
+
264
+ ## Development
265
+
266
+ ```bash
267
+ bun install
268
+ bun test # full suite
269
+ bun run typecheck
270
+ bun run lint
271
+ bun run src/main.tsx # run from source
272
+ bun run build:binaries # cross-compile all platform binaries + their packages
273
+ ```
274
+
275
+ Tests run per-file isolated in CI (`bun run test:ci`) because `mock.module`
276
+ leaks across files in a shared process.
277
+
278
+ **Do not bump `@opentui` past 0.1.x.** 0.4.x breaks `bun build --compile`: it
279
+ resolves tree-sitter workers eagerly, bypassing the `OTUI_ASSET_ROOT` override,
280
+ so a bare `import "@opentui/core"` crashes at import time in the compiled
281
+ binary. Re-run the compile spike before changing that pin.
282
+
283
+ ## Releasing
284
+
285
+ Tag-driven. `.github/workflows/magus-release.yml` fires on `tools/magus/v*`:
286
+
287
+ 1. **verify** — install, typecheck, `test:ci`, version and optional-deps gates
288
+ 2. **binaries** — a matrix building each target on its **native** runner
289
+ (`bun --compile` cannot cross-compile in CI because opentui ships a
290
+ per-platform native library)
291
+ 3. **publish** — platform packages first, main package last, so its
292
+ `optionalDependencies` already resolve
293
+
294
+ ```bash
295
+ # bump "version" AND the three optionalDependencies to match
296
+ bun run check:optional-deps
297
+ git commit -am "feat(magus): vX.Y.Z - description"
298
+ git tag -a tools/magus/vX.Y.Z -m "Release message"
299
+ git push origin main --tags
300
+ ```
301
+
302
+ Auth is npm OIDC trusted publishing — the workflow references no secrets. Adding
303
+ a **new** platform target needs a one-time manual publish to create the package
304
+ name (OIDC can only authenticate against a package that already exists); see
305
+ [`docs/npm-trusted-publishing-setup.md`](../../docs/npm-trusted-publishing-setup.md).
306
+
307
+ ## License
308
+
309
+ MIT
package/bin/magus.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * magus launcher.
4
+ *
5
+ * Prefers a prebuilt, self-contained binary shipped as a per-platform
6
+ * optionalDependency (magus-cli-<platform>-<arch>) — that binary embeds the Bun
7
+ * runtime, so it runs without Bun installed. Falls back to running from source
8
+ * via Bun for dev checkouts and unsupported platforms.
9
+ *
10
+ * Set MAGUS_NO_BINARY=1 to skip the prebuilt binary and force the source
11
+ * path. That escape hatch exists because the launcher PREFERS the binary: when
12
+ * a shipped binary cannot start on a given machine, without it magus is a
13
+ * hard block rather than a slow start.
14
+ *
15
+ * This file must stay self-contained and import only node: builtins. Its tests
16
+ * copy it into a throwaway tree, where any relative import would not resolve.
17
+ */
18
+
19
+ import { spawnSync } from "node:child_process";
20
+ import { createRequire } from "node:module";
21
+ import { fileURLToPath } from "node:url";
22
+ import { dirname, join } from "node:path";
23
+ import { existsSync } from "node:fs";
24
+ import { constants } from "node:os";
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const args = process.argv.slice(2);
28
+ const { platform, arch } = process;
29
+
30
+ const ISSUES_URL = "https://github.com/MadAppGang/magus/issues";
31
+
32
+ /**
33
+ * The four signals a person sends on purpose. Everything else is a crash.
34
+ *
35
+ * The rule is deliberately inverted from the obvious one. An allowlist of
36
+ * crash signals leaves holes: SIGABRT is what a Bun/JSC panic raises, which is
37
+ * exactly the "this binary is broken, use source" case, and it would fall
38
+ * through the allowlist into the old masked path.
39
+ */
40
+ const USER_SIGNALS = new Set(["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"]);
41
+
42
+ /**
43
+ * How long after exec a crash still counts as "it never started".
44
+ *
45
+ * An invalid code signature kills the binary in single-digit milliseconds, at
46
+ * exec, before any output. A `kill -9` against a running TUI, or an OOM kill,
47
+ * lands seconds later. Without this gate the fix would trade a silent exit 0
48
+ * for a surprise relaunch of an app the user deliberately killed.
49
+ */
50
+ const CRASH_FALLBACK_WINDOW_MS = 2000;
51
+
52
+ /** Shell convention: a signal death is 128 + the signal number. */
53
+ function exitCodeForSignal(signal) {
54
+ const number = constants.signals[signal];
55
+ return typeof number === "number" ? 128 + number : 1;
56
+ }
57
+
58
+ // Set when a binary was found and tried but could not deliver a result. It
59
+ // changes what the source path says if IT then fails: "no prebuilt binary for
60
+ // this platform" is true when none was installed and a flat contradiction when
61
+ // one was just reported as killed.
62
+ let binaryFailed = false;
63
+
64
+ // 1. Prefer the prebuilt platform binary, unless the source path is forced.
65
+ const forceSource = process.env.MAGUS_NO_BINARY === "1";
66
+ const pkgName = `magus-cli-${platform}-${arch}`;
67
+ let binaryPath = null;
68
+ try {
69
+ const pkgJson = require.resolve(`${pkgName}/package.json`);
70
+ const candidate = join(dirname(pkgJson), "bin", "magus");
71
+ if (existsSync(candidate)) binaryPath = candidate;
72
+ } catch {
73
+ // optional dep not installed for this platform — fall through
74
+ }
75
+
76
+ if (binaryPath && !forceSource) {
77
+ const startedAt = Date.now();
78
+ const result = spawnSync(binaryPath, args, { stdio: "inherit" });
79
+ const elapsedMs = Date.now() - startedAt;
80
+
81
+ if (!result.error) {
82
+ // `status === null` is the authoritative "did not exit normally" marker,
83
+ // NOT the truthiness of `signal`. Node writes an empty string into
84
+ // `signal` for a signal it has no name for (Linux real-time signals,
85
+ // SIGRTMIN+n), and "" is falsy — so branching on the signal would fall
86
+ // straight back into the `status ?? 0` this release exists to remove.
87
+ if (result.status !== null) process.exit(result.status);
88
+
89
+ const signalName = result.signal || "an unnamed signal";
90
+ const deliberate =
91
+ USER_SIGNALS.has(result.signal) ||
92
+ elapsedMs >= CRASH_FALLBACK_WINDOW_MS;
93
+ if (deliberate) process.exit(exitCodeForSignal(result.signal));
94
+
95
+ binaryFailed = true;
96
+ // The elapsed time is measured; "before producing any output" is not —
97
+ // the child ran under stdio: "inherit", so this process never saw its
98
+ // output and must not claim there was none.
99
+ console.error(
100
+ `magus: the prebuilt binary was killed by ${signalName} ${elapsedMs}ms after it started.\n` +
101
+ "\n" +
102
+ ` binary: ${binaryPath}\n` +
103
+ ` platform: ${platform}-${arch}\n` +
104
+ "\n" +
105
+ "On macOS this almost always means the binary's code signature is invalid and the\n" +
106
+ "kernel refused to run it. Confirm with:\n" +
107
+ "\n" +
108
+ ` codesign --verify --strict "${binaryPath}"\n` +
109
+ "\n" +
110
+ "Falling back to running from source via Bun. To skip the binary permanently:\n" +
111
+ " export MAGUS_NO_BINARY=1\n" +
112
+ "\n" +
113
+ `Please report this: ${ISSUES_URL}`,
114
+ );
115
+ // fall through to the source path
116
+ } else {
117
+ // A binary that could not be executed AT ALL (ENOENT, EACCES, bad arch)
118
+ // also leaves status null. Say so and fall through.
119
+ binaryFailed = true;
120
+ console.error(
121
+ `magus: prebuilt binary could not start (${result.error.message}); falling back to source.`,
122
+ );
123
+ }
124
+ }
125
+
126
+ // 2. Fallback: run from source via Bun.
127
+ const here = dirname(fileURLToPath(import.meta.url));
128
+ const mainSrc = join(here, "..", "src", "main.tsx");
129
+ // --no-env-file: Bun autoloads the cwd's .env into process.env before any user
130
+ // code runs, and the theme snapshot in main.tsx must precede any .env merge
131
+ // (loadProjectDotenv is the only merger). The compiled binary never autoloads.
132
+ const bunRun = spawnSync("bun", ["--no-env-file", mainSrc, ...args], {
133
+ stdio: "inherit",
134
+ });
135
+ if (bunRun.error) {
136
+ // Two different states reach here, and one message for both is a lie in one
137
+ // of them. A user who installed magus from npm on macOS 27 and has no
138
+ // Bun hits the FIRST branch — telling them no binary exists for their
139
+ // platform contradicts the diagnostic printed seconds earlier, and
140
+ // "use a supported platform" is advice for a problem they do not have.
141
+ if (binaryFailed) {
142
+ console.error(
143
+ `magus: the prebuilt binary for ${platform}-${arch} is unusable on this machine, and Bun is not installed to fall back to.\n` +
144
+ "\n" +
145
+ "Reinstall to pick up a working binary:\n" +
146
+ " npm install -g magus-cli@latest\n" +
147
+ "\n" +
148
+ "Or install Bun (https://bun.sh) so the source path can run.\n" +
149
+ `Please report this: ${ISSUES_URL}`,
150
+ );
151
+ } else {
152
+ console.error(
153
+ `magus: no prebuilt binary for ${platform}-${arch}, and Bun is not installed.`,
154
+ );
155
+ console.error("Install Bun (https://bun.sh) or use a supported platform.");
156
+ }
157
+ process.exit(1);
158
+ }
159
+ // Same masking bug, second site, keyed the same way: `status === null` decides,
160
+ // not the signal. There is no binary here and nowhere left to fall back to, so
161
+ // this reports and exits — no codesign hint, which would be nonsense for a Bun
162
+ // crash.
163
+ if (bunRun.status !== null) process.exit(bunRun.status);
164
+ if (!USER_SIGNALS.has(bunRun.signal)) {
165
+ console.error(
166
+ `magus: running from source was killed by ${bunRun.signal || "an unnamed signal"}.\n` +
167
+ `Please report this: ${ISSUES_URL}`,
168
+ );
169
+ }
170
+ process.exit(exitCodeForSignal(bunRun.signal));
package/package.json CHANGED
@@ -1,7 +1,71 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "0.0.0-placeholder",
4
- "description": "Placeholder — real package is published by CI.",
3
+ "version": "7.0.0",
4
+ "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
+ "type": "module",
6
+ "main": "src/main.tsx",
7
+ "bin": {
8
+ "magus": "bin/magus.js"
9
+ },
10
+ "scripts": {
11
+ "start": "bun --no-env-file src/main.tsx",
12
+ "dev": "bun --watch --no-env-file src/main.tsx",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "biome check src/",
15
+ "format": "biome format --write src/",
16
+ "test": "bun test",
17
+ "test:ci": "bun scripts/test-isolated.ts",
18
+ "test:update": "bun test --update-snapshots",
19
+ "build:binaries": "bun scripts/build-binaries.ts",
20
+ "check:optional-deps": "bun scripts/check-optional-deps.ts"
21
+ },
22
+ "keywords": [
23
+ "claude",
24
+ "claude-code",
25
+ "mcp",
26
+ "plugins",
27
+ "tui",
28
+ "cli",
29
+ "opentui",
30
+ "react"
31
+ ],
32
+ "author": "Jack Rudenko <i@madappgang.com>",
5
33
  "license": "MIT",
6
- "repository": { "type": "git", "url": "git+https://github.com/MadAppGang/magus-src.git" }
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/MadAppGang/magus"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/MadAppGang/magus/issues"
40
+ },
41
+ "homepage": "https://github.com/MadAppGang/magus/tree/main/tools/magus#readme",
42
+ "engines": {
43
+ "bun": ">=1.0.0"
44
+ },
45
+ "files": [
46
+ "bin/",
47
+ "src/",
48
+ "scripts/"
49
+ ],
50
+ "dependencies": {
51
+ "@opentui/core": "^0.1.75",
52
+ "@opentui/react": "^0.1.75",
53
+ "chalk": "^5.3.0",
54
+ "fs-extra": "^11.2.0",
55
+ "react": "^19.0.0",
56
+ "semver": "^7.6.3"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "^1.9.4",
60
+ "@types/fs-extra": "^11.0.4",
61
+ "@types/node": "^22.9.0",
62
+ "@types/react": "^19.0.0",
63
+ "@types/semver": "^7.5.8",
64
+ "typescript": "^5.6.3"
65
+ },
66
+ "optionalDependencies": {
67
+ "magus-cli-darwin-arm64": "7.0.0",
68
+ "magus-cli-darwin-x64": "7.0.0",
69
+ "magus-cli-linux-x64": "7.0.0"
70
+ }
7
71
  }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build per-platform, self-contained magus binaries and their npm packages.
4
+ *
5
+ * Each target is compiled with `bun build --compile --target=bun-<os>-<arch>`,
6
+ * producing a binary that embeds the Bun runtime (runs without Bun installed).
7
+ * The output is a publishable package per platform under dist-binaries/:
8
+ *
9
+ * dist-binaries/magus-cli-darwin-arm64/
10
+ * package.json (os/cpu restricted, no `bin` — the launcher execs it)
11
+ * bin/magus (the compiled binary)
12
+ *
13
+ * The main `magus` package lists these as optionalDependencies; npm installs
14
+ * only the one matching the host os/cpu, and bin/magus.js execs it.
15
+ *
16
+ * Hard constraint: magus pins @opentui 0.1.x — 0.4.x breaks --compile.
17
+ *
18
+ * Hard constraint: every darwin binary is re-signed and verified here. The
19
+ * signature --compile leaves behind does not cover the appended bundle, and
20
+ * macOS 27 kills such a binary at exec. See src/services/binary-signing.ts.
21
+ *
22
+ * Hard constraint: the two --no-compile-autoload flags below must stay. A Bun
23
+ * standalone executable autoloads .env and bunfig.toml from its CURRENT
24
+ * DIRECTORY by default, and magus reads neither on purpose. Bun 1.4.0 dies
25
+ * in the .env path when that file is a symlink pointing at a FIFO — exactly how
26
+ * 1Password serves secrets into a git worktree — exiting 1 with nothing on
27
+ * stdout or stderr, because the crash lands before any JS runs and therefore
28
+ * before any error handler exists. A FIFO also hands its bytes to whoever opens
29
+ * it first, so an autoloading magus can swallow the secrets a dev server was
30
+ * waiting for. Dropping the flags re-arms both failures.
31
+ */
32
+
33
+ import { $ } from "bun";
34
+ import { mkdir, writeFile, rm } from "node:fs/promises";
35
+ import path from "node:path";
36
+ import pkg from "../package.json";
37
+ import { signAndVerify } from "../src/services/binary-signing.js";
38
+
39
+ interface Target {
40
+ os: "darwin" | "linux";
41
+ cpu: "arm64" | "x64";
42
+ bunTarget: string;
43
+ }
44
+
45
+ const TARGETS: Target[] = [
46
+ { os: "darwin", cpu: "arm64", bunTarget: "bun-darwin-arm64" },
47
+ { os: "darwin", cpu: "x64", bunTarget: "bun-darwin-x64" },
48
+ { os: "linux", cpu: "x64", bunTarget: "bun-linux-x64" },
49
+ ];
50
+
51
+ const version = (pkg as { version: string }).version;
52
+ const root = path.join(import.meta.dir, "..");
53
+ const distRoot = path.join(root, "dist-binaries");
54
+ const entry = path.join(root, "src", "main.tsx");
55
+
56
+ // Optional filter: `bun scripts/build-binaries.ts darwin-arm64`
57
+ const only = process.argv[2];
58
+
59
+ // Only a full build purges the whole tree. Scoping a filtered build to its own
60
+ // target means `build:binaries linux-x64` on a Linux host does not first wipe
61
+ // dist-binaries/ and then throw on the darwin guard below, leaving the user
62
+ // with neither the old output nor the one target they could actually build.
63
+ if (!only) await rm(distRoot, { recursive: true, force: true });
64
+
65
+ for (const t of TARGETS) {
66
+ const id = `${t.os}-${t.cpu}`;
67
+ if (only && only !== id) continue;
68
+
69
+ const pkgName = `magus-cli-${id}`;
70
+ const outDir = path.join(distRoot, pkgName);
71
+ const binPath = path.join(outDir, "bin", "magus");
72
+
73
+ // `codesign` is macOS-only, and an unsigned darwin binary is killed at exec
74
+ // by macOS 27. Building one anywhere else produces an artifact that cannot
75
+ // be gated, which is precisely how the broken binaries shipped.
76
+ if (t.os === "darwin" && process.platform !== "darwin") {
77
+ throw new Error(
78
+ `Cannot build ${pkgName} on ${process.platform}: darwin binaries must be signed, and codesign exists only on macOS. Build this target on a macOS runner, or name a buildable one: bun run build:binaries linux-x64`,
79
+ );
80
+ }
81
+
82
+ await rm(outDir, { recursive: true, force: true });
83
+ await mkdir(path.join(outDir, "bin"), { recursive: true });
84
+
85
+ console.log(`Building ${pkgName} (${t.bunTarget})…`);
86
+ // The --no-compile-autoload flags are load-bearing; see the header.
87
+ await $`bun build --compile --no-compile-autoload-dotenv --no-compile-autoload-bunfig --target=${t.bunTarget} ${entry} --outfile ${binPath}`;
88
+
89
+ // --compile signs the Mach-O, then appends the bundle after it, leaving a
90
+ // signature that does not cover the file. Re-sign and refuse to continue if
91
+ // the result does not verify.
92
+ signAndVerify(t, binPath);
93
+
94
+ // Platform package: os/cpu-restricted, ships only the binary, declares NO
95
+ // `bin` (the main package's launcher resolves and execs bin/magus).
96
+ const platformPkg = {
97
+ name: pkgName,
98
+ version,
99
+ description: `Prebuilt magus binary for ${id}`,
100
+ os: [t.os],
101
+ cpu: [t.cpu],
102
+ files: ["bin/"],
103
+ license: (pkg as { license: string }).license,
104
+ repository: (pkg as { repository: unknown }).repository,
105
+ };
106
+ await writeFile(
107
+ path.join(outDir, "package.json"),
108
+ `${JSON.stringify(platformPkg, null, 2)}\n`,
109
+ );
110
+ }
111
+
112
+ console.log(`\nDone. Packages in ${path.relative(process.cwd(), distRoot)}/`);
113
+ console.log("Publish each with `npm publish` (then the main package).");