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,412 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * capture-builtin.ts — turn a built-in Claude Code output style into an
4
+ * importable file on THIS machine.
5
+ *
6
+ * Built-in styles (Explanatory, Learning, Proactive) ship inside the Claude
7
+ * Code binary, so there is no file for the composer to read. This captures the
8
+ * real text instead of guessing it: a transparent proxy sits in front of the
9
+ * Anthropic API, one `claude -p` round trip runs with the style active, and
10
+ * the system prompt it actually sent is recorded.
11
+ *
12
+ * Why capture rather than grep the binary: the binary's symbol names are
13
+ * minified and change every release, so a grep breaks silently. A capture
14
+ * reads what the harness genuinely sent, for the exact version installed.
15
+ *
16
+ * Why on the user's machine rather than committed here: a snapshot in this
17
+ * repo would be Anthropic's prompt text redistributed, and would go stale on
18
+ * their release schedule rather than ours.
19
+ *
20
+ * Section boundaries are found by DIFF, not by heading. The style's own body
21
+ * contains H1 headings of its own (Explanatory ships "# Explanatory Style
22
+ * Active"), so splitting on the next "# " truncates it. Capturing once with
23
+ * the style and once without isolates exactly the inserted block.
24
+ *
25
+ * Usage:
26
+ * bun capture-builtin.ts --discover # names Anthropic ships today
27
+ * bun capture-builtin.ts --check # which captures fell behind
28
+ * bun capture-builtin.ts --all # capture every built-in
29
+ * bun capture-builtin.ts --style Explanatory # capture one
30
+ * [--out DIR] [--port 8899] [--timeout 180] [--dry-run]
31
+ *
32
+ * RE-RUN AFTER EVERY CLAUDE CODE UPGRADE. A capture records the version it
33
+ * came from; `--check` compares that against the installed binary and reports
34
+ * both stale files and built-ins that exist but were never captured, which is
35
+ * how a newly introduced style gets noticed.
36
+ *
37
+ * Costs one real API round trip per style, plus one shared baseline.
38
+ * Exit code: 1 on any failure, or from --check when anything is stale.
39
+ */
40
+
41
+ import {
42
+ existsSync,
43
+ mkdirSync,
44
+ mkdtempSync,
45
+ readFileSync,
46
+ readdirSync,
47
+ rmSync,
48
+ writeFileSync,
49
+ } from "node:fs";
50
+ import { homedir, tmpdir } from "node:os";
51
+ import { join } from "node:path";
52
+ import { parseArgs } from "node:util";
53
+
54
+ import { splitFrontmatter } from "../src/services/styles-manager.js";
55
+
56
+ const UPSTREAM = "https://api.anthropic.com";
57
+
58
+ interface CaptureResult {
59
+ system: string;
60
+ blocks: number;
61
+ }
62
+
63
+ /**
64
+ * Run one `claude -p` through a recording proxy and return the system prompt
65
+ * it sent. `styleName` of null captures the baseline with no style active.
66
+ */
67
+ async function captureSystem(
68
+ styleName: string | null,
69
+ port: number,
70
+ timeoutMs: number,
71
+ ): Promise<CaptureResult> {
72
+ const project = mkdtempSync(join(tmpdir(), "style-capture-"));
73
+ mkdirSync(join(project, ".claude"), { recursive: true });
74
+ writeFileSync(
75
+ join(project, ".claude", "settings.json"),
76
+ `${JSON.stringify(styleName ? { outputStyle: styleName } : {}, null, 2)}\n`,
77
+ "utf8",
78
+ );
79
+
80
+ let captured: unknown = null;
81
+
82
+ const server = Bun.serve({
83
+ port,
84
+ idleTimeout: 240,
85
+ async fetch(req) {
86
+ const url = new URL(req.url);
87
+ const body =
88
+ req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
89
+
90
+ if (body && captured === null) {
91
+ try {
92
+ const parsed = JSON.parse(new TextDecoder().decode(body)) as { system?: unknown };
93
+ if (parsed.system) captured = parsed.system;
94
+ } catch {
95
+ // Not JSON. Forward it regardless — recording is best effort.
96
+ }
97
+ }
98
+
99
+ const headers = new Headers(req.headers);
100
+ headers.set("host", new URL(UPSTREAM).host);
101
+ headers.delete("content-length");
102
+ // fetch() transparently decompresses, so forwarding the upstream
103
+ // content-encoding would tell the client to decompress plaintext.
104
+ // Ask for identity and drop the header on the way back.
105
+ headers.set("accept-encoding", "identity");
106
+
107
+ const upstream = await fetch(`${UPSTREAM}${url.pathname}${url.search}`, {
108
+ method: req.method,
109
+ headers,
110
+ body,
111
+ });
112
+ const out = new Headers(upstream.headers);
113
+ out.delete("content-encoding");
114
+ out.delete("content-length");
115
+ return new Response(upstream.body, { status: upstream.status, headers: out });
116
+ },
117
+ });
118
+
119
+ try {
120
+ const proc = Bun.spawn(["claude", "-p", "reply with the single word ok"], {
121
+ cwd: project,
122
+ env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
123
+ stdout: "pipe",
124
+ stderr: "pipe",
125
+ });
126
+
127
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
128
+ const exitCode = await proc.exited;
129
+ clearTimeout(timer);
130
+
131
+ if (captured === null) {
132
+ const stderr = await new Response(proc.stderr).text();
133
+ throw new Error(
134
+ `no system prompt captured (claude exited ${exitCode}). ${stderr.trim().slice(0, 300)}`,
135
+ );
136
+ }
137
+ } finally {
138
+ server.stop(true);
139
+ rmSync(project, { recursive: true, force: true });
140
+ }
141
+
142
+ const blocks = Array.isArray(captured) ? captured : [captured];
143
+ const system = blocks
144
+ .map((block) =>
145
+ typeof block === "string" ? block : String((block as { text?: string })?.text ?? ""),
146
+ )
147
+ .join("\n");
148
+
149
+ return { system, blocks: blocks.length };
150
+ }
151
+
152
+ /**
153
+ * The inserted block is what the styled capture has and the baseline does not.
154
+ *
155
+ * Given baseline = A + B and styled = A + X + B, the end of X is fixed by
156
+ * ARITHMETIC, not by walking back from the end. A common-suffix walk keeps
157
+ * going whenever X's last characters happen to match B's last characters, and
158
+ * it does happen: the first version of this truncated Explanatory mid-sentence
159
+ * at "general programming concepts", eating the closing two sentences.
160
+ */
161
+ /**
162
+ * Extract the section beginning at `marker` from `styled`.
163
+ *
164
+ * The end is found by heading, with `baseline` deciding which headings are
165
+ * STRUCTURAL. Splitting naively on the next `# ` truncates, because a style's
166
+ * own body contains H1s (Explanatory ships "# Explanatory Style Active"); but
167
+ * a heading that also appears in the baseline belongs to the harness, so it
168
+ * marks the real boundary.
169
+ *
170
+ * Character-level diffing was tried first and abandoned. If styled = A + X + B
171
+ * and baseline = A + B, the split is genuinely ambiguous whenever X starts or
172
+ * ends the way the surrounding text does — every rotation of X satisfies the
173
+ * length constraint. All three variants failed on real captures: a suffix walk
174
+ * cut Explanatory mid-sentence at "general programming concepts", a prefix
175
+ * walk slid past the marker entirely, and smallest-valid-split returned
176
+ * "D\nINSERTE" for "INSERTED\n".
177
+ *
178
+ * Residual limitation: a style whose body contains an H1 that also appears in
179
+ * the baseline would be cut there. None of the three built-ins does.
180
+ */
181
+ export function extractSection(baseline: string, styled: string, marker: string): string {
182
+ const start = styled.indexOf(marker);
183
+ if (start === -1) return "";
184
+
185
+ const structural = new Set(
186
+ baseline
187
+ .split("\n")
188
+ .filter((line) => line.startsWith("# "))
189
+ .map((line) => line.trimEnd()),
190
+ );
191
+
192
+ const lines = styled.slice(start).split("\n");
193
+ const out: string[] = [lines[0]];
194
+ for (const line of lines.slice(1)) {
195
+ if (line.startsWith("# ") && structural.has(line.trimEnd())) break;
196
+ out.push(line);
197
+ }
198
+ return out.join("\n").trimEnd();
199
+ }
200
+
201
+ /** Strip the harness's own `# Output Style: <name>` header off the block. */
202
+ export function stripSectionHeader(block: string, styleName: string): string {
203
+ const marker = `# Output Style: ${styleName}`;
204
+ const at = block.indexOf(marker);
205
+ if (at === -1) return block.trim();
206
+ return block.slice(at + marker.length).trim();
207
+ }
208
+
209
+ export function slugFor(styleName: string): string {
210
+ return `builtin-${styleName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
211
+ }
212
+
213
+ export function toStyleFile(styleName: string, body: string, version: string): string {
214
+ return [
215
+ "---",
216
+ `name: ${slugFor(styleName)}`,
217
+ `description: "Captured built-in output style: ${styleName}"`,
218
+ // Our composed file sets this itself; setting it here too means the file
219
+ // is also safe to activate directly.
220
+ "keep-coding-instructions: true",
221
+ // Machine-readable so --check can tell when Claude Code moved on. Keep it
222
+ // a bare version: --check compares it, a human sentence would not parse.
223
+ `captured-from: ${version}`,
224
+ `captured-style: ${styleName}`,
225
+ 'generated-by: "capture-builtin.ts. Re-run after a Claude Code upgrade."',
226
+ "---",
227
+ "",
228
+ body,
229
+ "",
230
+ ].join("\n");
231
+ }
232
+
233
+ /** "2.1.233 (Claude Code)" -> "2.1.233". --check compares these. */
234
+ export function normalizeVersion(raw: string): string {
235
+ return /(\d+\.\d+\.\d+)/.exec(raw)?.[1] ?? raw.trim() ?? "unknown";
236
+ }
237
+
238
+ async function run(cmd: string[]): Promise<string> {
239
+ try {
240
+ const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore" });
241
+ await proc.exited;
242
+ return (await new Response(proc.stdout).text()).trim();
243
+ } catch {
244
+ return "";
245
+ }
246
+ }
247
+
248
+ async function claudeVersion(): Promise<string> {
249
+ return normalizeVersion(await run(["claude", "--version"])) || "unknown";
250
+ }
251
+
252
+ /**
253
+ * Names of the built-in styles, read out of the installed binary.
254
+ *
255
+ * This is the one place a minified-bundle grep is acceptable: it recovers
256
+ * NAMES, which are cheap to verify by eye against `/output-style` and
257
+ * harmless to get wrong. The prompt TEXT is never read this way — that is
258
+ * what the capture is for. If a future release changes the shape and this
259
+ * returns nothing, read the names off the `/output-style` picker and pass
260
+ * `--style` directly.
261
+ */
262
+ export async function discoverBuiltins(): Promise<string[]> {
263
+ const binary = await run(["sh", "-c", "readlink -f \"$(command -v claude)\" 2>/dev/null || command -v claude"]);
264
+ if (!binary) return [];
265
+ const out = await run([
266
+ "grep",
267
+ "-o",
268
+ "-a",
269
+ "-E",
270
+ 'name:"[A-Za-z0-9 -]+",source:"built-in"',
271
+ binary,
272
+ ]);
273
+ const names = new Set<string>();
274
+ for (const line of out.split("\n")) {
275
+ const match = /name:"([^"]+)"/.exec(line);
276
+ if (match) names.add(match[1]);
277
+ }
278
+ return [...names].sort();
279
+ }
280
+
281
+ async function main(): Promise<number> {
282
+ const { values } = parseArgs({
283
+ args: process.argv.slice(2),
284
+ options: {
285
+ style: { type: "string" },
286
+ all: { type: "boolean", default: false },
287
+ discover: { type: "boolean", default: false },
288
+ check: { type: "boolean", default: false },
289
+ out: { type: "string" },
290
+ port: { type: "string", default: "8899" },
291
+ timeout: { type: "string", default: "180" },
292
+ "dry-run": { type: "boolean", default: false },
293
+ },
294
+ allowPositionals: false,
295
+ });
296
+
297
+ const port = Number(values.port);
298
+ const timeoutMs = Number(values.timeout) * 1000;
299
+ const outDir = values.out ?? join(homedir(), ".claude", "output-styles");
300
+ const version = await claudeVersion();
301
+
302
+ if (values.discover) {
303
+ const names = await discoverBuiltins();
304
+ if (names.length === 0) {
305
+ console.error(
306
+ "Could not read the built-in names from the binary. Open /output-style and\n" +
307
+ "pass the names you see with --style.",
308
+ );
309
+ return 1;
310
+ }
311
+ console.log(`Built-in output styles in Claude Code ${version}:`);
312
+ for (const name of names) console.log(` ${name}`);
313
+ return 0;
314
+ }
315
+
316
+ if (values.check) {
317
+ const captured = existsSync(outDir)
318
+ ? readdirSync(outDir).filter((f) => f.startsWith("builtin-") && f.endsWith(".md"))
319
+ : [];
320
+ const known = await discoverBuiltins();
321
+
322
+ console.log(`Claude Code ${version}`);
323
+ console.log("════════════════════════════════════════");
324
+ let stale = 0;
325
+ for (const file of captured.sort()) {
326
+ const { frontmatter } = splitFrontmatter(readFileSync(join(outDir, file), "utf8"));
327
+ const from = frontmatter["captured-from"] ?? "unknown";
328
+ const ok = from === version;
329
+ if (!ok) stale++;
330
+ console.log(` ${ok ? "current" : "STALE "} ${file} (captured from ${from})`);
331
+ }
332
+ const missing = known.filter((name) => !captured.includes(`${slugFor(name)}.md`));
333
+ for (const name of missing) console.log(` MISSING ${name}`);
334
+ console.log("════════════════════════════════════════");
335
+ if (stale > 0 || missing.length > 0) {
336
+ console.log("Re-capture with: bun capture-builtin.ts --all");
337
+ return 1;
338
+ }
339
+ console.log("All built-ins captured at the current version.");
340
+ return 0;
341
+ }
342
+
343
+ let targets: string[];
344
+ if (values.all) {
345
+ targets = await discoverBuiltins();
346
+ if (targets.length === 0) {
347
+ console.error("error: --all found no built-in names. Use --style with an explicit name.");
348
+ return 1;
349
+ }
350
+ } else if (values.style) {
351
+ targets = [values.style];
352
+ } else {
353
+ console.error(
354
+ "error: pass --style <name>, or --all, or --discover to list what exists.",
355
+ );
356
+ return 1;
357
+ }
358
+
359
+ // One baseline serves every style — it is the same prompt minus the section.
360
+ console.error(`[capture] baseline (no output style) …`);
361
+ const baseline = await captureSystem(null, port, timeoutMs);
362
+ console.error(`[capture] baseline: ${baseline.system.length} chars, ${baseline.blocks} blocks`);
363
+
364
+ let failures = 0;
365
+ for (const [index, styleName] of targets.entries()) {
366
+ console.error(`[capture] with ${styleName} …`);
367
+ const styled = await captureSystem(styleName, port + 1 + index, timeoutMs);
368
+
369
+ const marker = `# Output Style: ${styleName}`;
370
+ const section = extractSection(baseline.system, styled.system, marker);
371
+ if (!section) {
372
+ console.error(
373
+ styled.system.includes(marker)
374
+ ? `error: ${styleName}: found the marker but not where the section ends.`
375
+ : `error: ${styleName}: "${marker}" is not in the captured prompt. Real built-in?`,
376
+ );
377
+ failures++;
378
+ continue;
379
+ }
380
+
381
+ const body = stripSectionHeader(section, styleName);
382
+ const file = toStyleFile(styleName, body, version);
383
+ const path = join(outDir, `${slugFor(styleName)}.md`);
384
+
385
+ if (values["dry-run"]) {
386
+ console.log(`--- ${path}`);
387
+ console.log(file);
388
+ continue;
389
+ }
390
+
391
+ mkdirSync(outDir, { recursive: true });
392
+ writeFileSync(path, file, "utf8");
393
+ console.log(
394
+ `captured ${styleName.padEnd(14)} ${body.length} chars -> --import user:${slugFor(styleName)}`,
395
+ );
396
+ }
397
+
398
+ if (!values["dry-run"] && failures === 0) {
399
+ console.log(`\nAll captures are from Claude Code ${version}. Re-run after an upgrade;`);
400
+ console.log(`"bun capture-builtin.ts --check" reports when they fall behind.`);
401
+ }
402
+ return failures > 0 ? 1 : 0;
403
+ }
404
+
405
+ if (import.meta.main) {
406
+ try {
407
+ process.exit(await main());
408
+ } catch (error) {
409
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
410
+ process.exit(1);
411
+ }
412
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * check-optional-deps.ts — assert the per-platform optionalDependencies are pinned
4
+ * to the main package's own version.
5
+ *
6
+ * build-binaries.ts stamps each generated per-platform package with
7
+ * `package.json`'s version, and the release workflow publishes those first so the
8
+ * main package's optionalDependencies already resolve. If the two drift, the main
9
+ * package ships pointing at versions that were never published.
10
+ *
11
+ * That failure is quiet: optionalDependencies are allowed to fail to resolve, so
12
+ * npm/bun install succeeds and the launcher silently falls back to running from
13
+ * source via Bun — meaning every user loses the prebuilt binary and anyone without
14
+ * Bun on PATH gets a broken `magus`. Nothing errors, so it can ship unnoticed.
15
+ *
16
+ * Bumping the package version without bumping these is easy to do by hand, which
17
+ * is exactly what happened going 4.20.0 -> 4.21.0.
18
+ */
19
+
20
+ import pkg from "../package.json";
21
+
22
+ const version = (pkg as { version: string }).version;
23
+ const optional = (pkg as { optionalDependencies?: Record<string, string> })
24
+ .optionalDependencies;
25
+
26
+ if (!optional || Object.keys(optional).length === 0) {
27
+ console.log("check:optional-deps — no optionalDependencies declared, nothing to verify");
28
+ process.exit(0);
29
+ }
30
+
31
+ const mismatched = Object.entries(optional).filter(([, v]) => v !== version);
32
+
33
+ if (mismatched.length > 0) {
34
+ console.error(
35
+ `check:optional-deps FAILED — package version is ${version}, but:`,
36
+ );
37
+ for (const [name, v] of mismatched) {
38
+ console.error(` ${name}: ${v} (expected ${version})`);
39
+ }
40
+ console.error(
41
+ "\nThese are published from this same version by build-binaries.ts.\n" +
42
+ "Update optionalDependencies in package.json to match, then re-run.",
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log(
48
+ `check:optional-deps — all ${Object.keys(optional).length} platform packages pinned to ${version}`,
49
+ );
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run each test file in its own `bun test` process.
4
+ *
5
+ * Some tests use bun's `mock.module`, which is process-global and NOT restored
6
+ * across files (bun 1.3.10). In a single shared process the outcome depends on
7
+ * file order — fine locally, flaky on CI (different order / workers). Per-file
8
+ * isolation removes the cross-file coupling entirely: every file gets a fresh
9
+ * module registry. Slower than `bun test`, but deterministic — which is what CI
10
+ * needs. For fast local iteration use `bun test`.
11
+ */
12
+
13
+ import { readdirSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+
17
+ function findTests(dir: string): string[] {
18
+ const out: string[] = [];
19
+ for (const entry of readdirSync(dir)) {
20
+ const p = join(dir, entry);
21
+ if (statSync(p).isDirectory()) out.push(...findTests(p));
22
+ // `.tsx` as well as `.ts`. The suffix was written when no `.tsx` test
23
+ // existed, so it silently excluded the only file testing a UI primitive
24
+ // — the class of test most likely to be `.tsx` and least likely to be
25
+ // noticed missing, because a file that is never collected reports
26
+ // nothing at all. The printed `N/N` count below is the guard: it moves
27
+ // when collection changes.
28
+ else if (/\.test\.tsx?$/.test(p)) out.push(p);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ const root = join(import.meta.dir, "..");
34
+ const files = findTests(join(root, "src")).sort();
35
+
36
+ let failed = 0;
37
+ for (const file of files) {
38
+ const rel = file.slice(root.length + 1);
39
+ process.stdout.write(`\n── ${rel}\n`);
40
+ const result = spawnSync("bun", ["test", file], {
41
+ stdio: "inherit",
42
+ cwd: root,
43
+ });
44
+ if (result.status !== 0) failed++;
45
+ }
46
+
47
+ const passed = files.length - failed;
48
+ console.log(`\n${passed}/${files.length} test files passed (isolated).`);
49
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * verify-binary.ts — prove a freshly built platform binary is shippable.
3
+ *
4
+ * Both `magus-ci.yml` (every PR) and `magus-release.yml` (every tag) run this.
5
+ * It exists as one script rather than two copies of the same shell block because
6
+ * the two workflows drifting is how a gate quietly stops gating: a fix applied to
7
+ * the release copy and missed on the CI copy leaves PRs green on the old rules.
8
+ *
9
+ * Three checks, each earned by a release that shipped broken:
10
+ *
11
+ * 1. STARTS. A binary that compiles but cannot start is worse than no binary,
12
+ * because the launcher prefers it over the working Bun-source fallback.
13
+ *
14
+ * 2. STARTS WHERE .env IS A SYMLINK TO A FIFO. v4.41.0 passed check 1 and still
15
+ * died in every git worktree whose .env is a FIFO (how 1Password serves
16
+ * secrets): the embedded Bun aborted while autoloading that file — exit 1,
17
+ * nothing on stdout or stderr, because the crash lands before any JS runs.
18
+ *
19
+ * 3. THE SIGNATURE COVERS THE WHOLE FILE (darwin only). `bun build --compile`
20
+ * signs the Mach-O and then appends the bundle after it, so the signature
21
+ * covers a shorter file than the one published — true of every release from
22
+ * 4.22.0 to 6.8.0. macOS 27 SIGKILLs such a binary at exec with no output.
23
+ * Check 1 does NOT catch it: GitHub's runner images are behind that
24
+ * enforcement, so the binary starts there while users get exit 137.
25
+ * `codesign --verify --strict` is enforcement-independent and fails on older
26
+ * macOS too, which is the only reason this is catchable in CI at all.
27
+ *
28
+ * Usage: bun scripts/verify-binary.ts <target> e.g. darwin-arm64
29
+ */
30
+ import { spawnSync } from "node:child_process";
31
+ import { existsSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
32
+ import { tmpdir } from "node:os";
33
+ import path from "node:path";
34
+
35
+ const target = process.argv[2];
36
+ if (!target) {
37
+ console.error("usage: bun scripts/verify-binary.ts <target> e.g. darwin-arm64");
38
+ process.exit(2);
39
+ }
40
+
41
+ const root = path.join(import.meta.dir, "..");
42
+ const bin = path.join(root, "dist-binaries", `magus-cli-${target}`, "bin", "magus");
43
+
44
+ function fail(message: string): never {
45
+ console.error(`verify-binary(${target}): ${message}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ // --- 1. present and executable -------------------------------------------
50
+ if (!existsSync(bin)) fail(`missing: ${bin}`);
51
+ const version = spawnSync(bin, ["--version"], { encoding: "utf8" });
52
+ if (version.error) fail(`cannot execute ${bin}: ${version.error.message}`);
53
+ if (version.status !== 0) {
54
+ // A signal leaves status null. Report it as itself — an unsigned darwin binary
55
+ // on an enforcing kernel lands here as SIGKILL, and "exit null" would hide that.
56
+ const how = version.signal ? `killed by ${version.signal}` : `exit ${version.status}`;
57
+ fail(`--version ${how}`);
58
+ }
59
+ console.log(` starts ${version.stdout.trim()}`);
60
+
61
+ // --- 2. starts where .env is a symlink to a FIFO --------------------------
62
+ const probe = mkdtempSync(path.join(tmpdir(), "magus-fifo-probe-"));
63
+ try {
64
+ const pipe = path.join(probe, "secrets-pipe");
65
+ const mkfifo = spawnSync("mkfifo", [pipe]);
66
+ if (mkfifo.status !== 0) fail("could not create the FIFO probe");
67
+ symlinkSync(pipe, path.join(probe, ".env"));
68
+
69
+ const inProbe = spawnSync(bin, ["--version"], { cwd: probe, encoding: "utf8" });
70
+ if (inProbe.error || inProbe.status !== 0) {
71
+ fail("cannot start where .env is a symlink to a FIFO");
72
+ }
73
+ console.log(" fifo .env ok");
74
+ } finally {
75
+ rmSync(probe, { recursive: true, force: true });
76
+ }
77
+
78
+ // --- 3. the signature covers the shipped bytes (darwin only) --------------
79
+ if (target.startsWith("darwin")) {
80
+ const verified = spawnSync("codesign", ["--verify", "--strict", bin], {
81
+ encoding: "utf8",
82
+ });
83
+ if (verified.status !== 0) {
84
+ fail(
85
+ `code signature does not verify.\n${verified.stderr.trim()}\n\n` +
86
+ "This is the 4.22.0–6.8.0 failure: --compile signs the Mach-O and then " +
87
+ "appends the bundle, so the signature covers a shorter file. macOS 27 " +
88
+ "SIGKILLs such a binary at exec. Check that build-binaries.ts still calls " +
89
+ "signAndVerify().",
90
+ );
91
+ }
92
+ console.log(" code signature verified");
93
+ }
94
+
95
+ console.log(`verify-binary(${target}): OK`);