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,163 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ALIAS_FLAGS } from "../data/alias-flags";
3
+ import {
4
+ CHANNELS_FLAG_ID,
5
+ DEFAULT_ALIAS_NAME,
6
+ DEVLOAD_FLAG_ID,
7
+ type FlagValue,
8
+ defaultAliasConfig,
9
+ defaultValueFor,
10
+ derivedChannelValues,
11
+ listValues,
12
+ validateAliasName,
13
+ withoutDerivedChannel,
14
+ } from "../services/alias-store";
15
+
16
+ function textList(values: string[], enabled = true): FlagValue {
17
+ return { kind: "text-list", enabled, values };
18
+ }
19
+
20
+ describe("defaultAliasConfig", () => {
21
+ it("seeds every known flag with a sane default and the default alias name", () => {
22
+ const c = defaultAliasConfig();
23
+ expect(c.aliasName).toBe(DEFAULT_ALIAS_NAME);
24
+ expect(c.aliasName).toBe("c");
25
+ expect(c.flags["dangerously-skip-permissions"]).toEqual({
26
+ kind: "boolean",
27
+ enabled: false,
28
+ });
29
+ expect(c.flags.chrome).toEqual({ kind: "tri-state", state: "unset" });
30
+ expect(c.flags.effort).toEqual({
31
+ kind: "select",
32
+ enabled: false,
33
+ value: "low",
34
+ });
35
+ expect(c.flags["dangerously-load-development-channels"]).toEqual({
36
+ kind: "text-list",
37
+ enabled: false,
38
+ values: ["plugin:claudish@magus"],
39
+ });
40
+ expect(c.flags.debug).toEqual({
41
+ kind: "multi-with-custom",
42
+ enabled: false,
43
+ picked: [],
44
+ custom: [],
45
+ });
46
+ });
47
+
48
+ it("covers every flag in the catalog", () => {
49
+ const c = defaultAliasConfig();
50
+ for (const flag of ALIAS_FLAGS) {
51
+ expect(c.flags[flag.id]).toBeDefined();
52
+ }
53
+ });
54
+ });
55
+
56
+ describe("defaultValueFor", () => {
57
+ it("returns the right default for each kind", () => {
58
+ const ide = ALIAS_FLAGS.find((f) => f.id === "ide")!;
59
+ expect(defaultValueFor(ide)).toEqual({ kind: "boolean", enabled: false });
60
+
61
+ const chrome = ALIAS_FLAGS.find((f) => f.id === "chrome")!;
62
+ expect(defaultValueFor(chrome)).toEqual({
63
+ kind: "tri-state",
64
+ state: "unset",
65
+ });
66
+
67
+ const debug = ALIAS_FLAGS.find((f) => f.id === "debug")!;
68
+ expect(defaultValueFor(debug)).toEqual({
69
+ kind: "multi-with-custom",
70
+ enabled: false,
71
+ picked: [],
72
+ custom: [],
73
+ });
74
+ });
75
+ });
76
+
77
+ describe("derivedChannelValues (derive-on-display model)", () => {
78
+ it("returns dev-load values when the dev-load flag is enabled", () => {
79
+ const flags: Record<string, FlagValue> = {
80
+ [DEVLOAD_FLAG_ID]: textList(["plugin:claudish@magus"], true),
81
+ [CHANNELS_FLAG_ID]: textList([], false),
82
+ };
83
+ expect(derivedChannelValues(flags)).toEqual(["plugin:claudish@magus"]);
84
+ });
85
+
86
+ it("returns nothing when the dev-load flag is DISABLED", () => {
87
+ const flags: Record<string, FlagValue> = {
88
+ [DEVLOAD_FLAG_ID]: textList(["plugin:claudish@magus"], false),
89
+ [CHANNELS_FLAG_ID]: textList([], false),
90
+ };
91
+ expect(derivedChannelValues(flags)).toEqual([]);
92
+ });
93
+
94
+ it("returns nothing when the dev-load flag is absent", () => {
95
+ expect(derivedChannelValues({})).toEqual([]);
96
+ });
97
+
98
+ it("does NOT read from channels.values (provenance stays separate)", () => {
99
+ // channels holds OWN values; dev-load is the only derived source.
100
+ const flags: Record<string, FlagValue> = {
101
+ [CHANNELS_FLAG_ID]: textList(["plugin:own@m"], true),
102
+ [DEVLOAD_FLAG_ID]: textList(["plugin:dev@m"], true),
103
+ };
104
+ expect(derivedChannelValues(flags)).toEqual(["plugin:dev@m"]);
105
+ // channels.values is untouched — own only.
106
+ expect(listValues(flags[CHANNELS_FLAG_ID])).toEqual(["plugin:own@m"]);
107
+ });
108
+ });
109
+
110
+ describe("withoutDerivedChannel (delete-through to dev-load)", () => {
111
+ it("removes the value from dev-load, NOT from channels.values", () => {
112
+ const flags: Record<string, FlagValue> = {
113
+ [CHANNELS_FLAG_ID]: textList(["plugin:own@m"], true),
114
+ [DEVLOAD_FLAG_ID]: textList(["plugin:dev@m", "plugin:keep@m"], true),
115
+ };
116
+ const next = withoutDerivedChannel(flags, "plugin:dev@m");
117
+ expect(listValues(next[DEVLOAD_FLAG_ID])).toEqual(["plugin:keep@m"]);
118
+ // channels.values untouched — the deletion landed on the source flag.
119
+ expect(listValues(next[CHANNELS_FLAG_ID])).toEqual(["plugin:own@m"]);
120
+ });
121
+
122
+ it("disables dev-load when its last value is removed", () => {
123
+ const flags: Record<string, FlagValue> = {
124
+ [DEVLOAD_FLAG_ID]: textList(["plugin:only@m"], true),
125
+ };
126
+ const next = withoutDerivedChannel(flags, "plugin:only@m");
127
+ expect(next[DEVLOAD_FLAG_ID]).toEqual({
128
+ kind: "text-list",
129
+ enabled: false,
130
+ values: [],
131
+ });
132
+ });
133
+
134
+ it("is a no-op when the value isn't present", () => {
135
+ const flags: Record<string, FlagValue> = {
136
+ [DEVLOAD_FLAG_ID]: textList(["plugin:a@m"], true),
137
+ };
138
+ expect(
139
+ listValues(withoutDerivedChannel(flags, "nope")[DEVLOAD_FLAG_ID]),
140
+ ).toEqual(["plugin:a@m"]);
141
+ });
142
+ });
143
+
144
+ describe("validateAliasName", () => {
145
+ it("accepts safe identifiers", () => {
146
+ expect(validateAliasName("c")).toBeNull();
147
+ expect(validateAliasName("claude")).toBeNull();
148
+ expect(validateAliasName("cc-yolo_2")).toBeNull();
149
+ expect(validateAliasName("_underscore")).toBeNull();
150
+ });
151
+
152
+ it("rejects empty, leading-digit, and special-char names", () => {
153
+ expect(validateAliasName("")).toMatch(/empty/);
154
+ expect(validateAliasName("9live")).toMatch(/letter/);
155
+ expect(validateAliasName("c laude")).toMatch(/letters/);
156
+ expect(validateAliasName("c'")).toMatch(/letters/);
157
+ expect(validateAliasName("c=d")).toMatch(/letters/);
158
+ });
159
+
160
+ it("rejects pathologically long names", () => {
161
+ expect(validateAliasName("a".repeat(64))).toMatch(/long/);
162
+ });
163
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `services/binary-signing.ts` — the build-time code-signing gate.
3
+ *
4
+ * Why this exists at all: `bun build --compile` emits an ad-hoc,
5
+ * linker-signed Mach-O and then appends the JavaScript bundle to it. The
6
+ * signature therefore does not cover the file it ships in. `codesign -v`
7
+ * reported "code or signature have been modified" on every published magus
8
+ * back to 4.22.0. macOS 27 kills such a binary at exec, with no output, which
9
+ * is what broke 4.40.0 onward.
10
+ *
11
+ * The decision logic lives in `src/` rather than in `scripts/build-binaries.ts`
12
+ * for one blunt reason: `scripts/test-isolated.ts` only walks `src/`, so a test
13
+ * placed beside the build script would never be collected and would report
14
+ * nothing at all.
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+ import { spawnSync } from "node:child_process";
19
+ import { appendFileSync, copyFileSync, mkdtempSync, rmSync } from "node:fs";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ import {
23
+ type Run,
24
+ needsSigning,
25
+ signAndVerify,
26
+ } from "../services/binary-signing.js";
27
+
28
+ /** Records every command instead of running one. */
29
+ function recorder(status = 0) {
30
+ const calls: Array<[string, string[]]> = [];
31
+ const run: Run = (cmd, args) => {
32
+ calls.push([cmd, args]);
33
+ return { status, stderr: "" };
34
+ };
35
+ return { calls, run };
36
+ }
37
+
38
+ describe("binary-signing", () => {
39
+ test("T-sign-1: a darwin target is signed ad-hoc, then verified", () => {
40
+ const { calls, run } = recorder();
41
+ signAndVerify({ os: "darwin" }, "/out/bin/magus", run);
42
+
43
+ // Ad-hoc (`--sign -`), not Developer ID: npm-installed files are not
44
+ // quarantined, so the kernel signature check is the only failing gate.
45
+ // No Apple account, no CI secrets, no notarization wait.
46
+ expect(calls).toEqual([
47
+ ["codesign", ["--force", "--sign", "-", "/out/bin/magus"]],
48
+ ["codesign", ["--verify", "--strict", "/out/bin/magus"]],
49
+ ]);
50
+ });
51
+
52
+ test("T-sign-2: a failing codesign is a hard gate, not a warning", () => {
53
+ // A release that ships an unverifiable binary is the whole defect. The
54
+ // build must stop, not print and continue.
55
+ const signFails: Run = (_cmd, args) =>
56
+ args[0] === "--force"
57
+ ? { status: 1, stderr: "sign failed" }
58
+ : { status: 0, stderr: "" };
59
+ expect(() =>
60
+ signAndVerify({ os: "darwin" }, "/out/bin/magus", signFails),
61
+ ).toThrow();
62
+
63
+ const verifyFails: Run = (_cmd, args) =>
64
+ args[0] === "--verify"
65
+ ? { status: 1, stderr: "verify failed" }
66
+ : { status: 0, stderr: "" };
67
+ expect(() =>
68
+ signAndVerify({ os: "darwin" }, "/out/bin/magus", verifyFails),
69
+ ).toThrow();
70
+ });
71
+
72
+ test("T-sign-3: a linux target runs no codesign at all", () => {
73
+ // `codesign` does not exist off macOS. Running it would fail the linux
74
+ // leg of the release matrix for no reason.
75
+ expect(needsSigning({ os: "linux" })).toBe(false);
76
+ expect(needsSigning({ os: "darwin" })).toBe(true);
77
+
78
+ const { calls, run } = recorder();
79
+ signAndVerify({ os: "linux" }, "/out/bin/magus", run);
80
+ expect(calls).toEqual([]);
81
+ });
82
+
83
+ // darwin-only: the subject is the macOS signature format itself. skipIf, not
84
+ // a bare early return — both CI jobs run on ubuntu, where a return would
85
+ // execute zero assertions and report green. The macOS coverage of this
86
+ // invariant in CI comes from the release matrix, which runs the real
87
+ // signAndVerify during `build:binaries` on both darwin legs.
88
+ test.skipIf(process.platform !== "darwin")(
89
+ "T-sign-4: real codesign rejects a signed file with bytes appended",
90
+ () => {
91
+ const dir = mkdtempSync(path.join(os.tmpdir(), "magus-signing-"));
92
+ try {
93
+ const subject = path.join(dir, "subject");
94
+ copyFileSync("/bin/echo", subject);
95
+
96
+ // Positive control. Without it a passing T-sign-4 would prove only
97
+ // that codesign fails on this machine for some unrelated reason.
98
+ expect(() => signAndVerify({ os: "darwin" }, subject)).not.toThrow();
99
+
100
+ // Exactly what `bun build --compile` does: append the bundle after
101
+ // the signature has been written.
102
+ appendFileSync(subject, "appended-bundle-bytes");
103
+
104
+ // Skip the re-sign so the REAL verify judges the corrupted file.
105
+ const verifyForReal: Run = (cmd, args) => {
106
+ if (args.includes("--sign")) return { status: 0, stderr: "" };
107
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
108
+ return { status: r.status, stderr: r.stderr ?? "" };
109
+ };
110
+ // Assert on throwing only. codesign's wording moves between macOS
111
+ // releases; the verdict does not.
112
+ expect(() =>
113
+ signAndVerify({ os: "darwin" }, subject, verifyForReal),
114
+ ).toThrow();
115
+ } finally {
116
+ rmSync(dir, { recursive: true, force: true });
117
+ }
118
+ },
119
+ );
120
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * BLACK-BOX: the one profile choice both front ends resolve through (R2
3
+ * precondition: "for the same project and profile"). §8.4 contracts only;
4
+ * IO injected through `loadActiveClosure`'s deps.
5
+ */
6
+ import { describe, expect, test } from "bun:test";
7
+ import {
8
+ type ActiveClosureDeps,
9
+ loadActiveClosure,
10
+ selectProfileId,
11
+ } from "../services/active-closure.js";
12
+ import type {
13
+ BinRequirement,
14
+ ProfileManifest,
15
+ ResolvedClosure,
16
+ } from "../types/index.js";
17
+
18
+ describe("selectProfileId: explicit ?? active ?? the only id ?? null", () => {
19
+ test.each([
20
+ { ids: ["a", "b"], explicit: "b", active: "a", want: "b" },
21
+ { ids: ["a", "b"], explicit: "zz", active: "a", want: "a" },
22
+ { ids: ["a", "b"], explicit: undefined, active: "b", want: "b" },
23
+ { ids: ["a", "b"], explicit: undefined, active: "zz", want: null },
24
+ { ids: ["only"], explicit: undefined, active: null, want: "only" },
25
+ { ids: ["only"], explicit: "zz", active: "yy", want: "only" },
26
+ { ids: ["a", "b"], explicit: undefined, active: null, want: null },
27
+ { ids: [], explicit: "a", active: "a", want: null },
28
+ ])(
29
+ "ids=$ids explicit=$explicit active=$active → $want",
30
+ ({ ids, explicit, active, want }) => {
31
+ expect(selectProfileId(ids, explicit, active)).toBe(want);
32
+ },
33
+ );
34
+ });
35
+
36
+ function manifest(): ProfileManifest {
37
+ return {
38
+ version: 2,
39
+ profiles: {
40
+ default: { name: "Default", plugins: { "dev@magus": "latest" } },
41
+ other: { name: "Other", plugins: { "setup@magus": "latest" } },
42
+ },
43
+ };
44
+ }
45
+
46
+ function closureFor(id: string): ResolvedClosure {
47
+ return {
48
+ marketplaces: {},
49
+ plugins: { [`${id}-plugin@magus`]: "latest" },
50
+ mcpServers: {},
51
+ bins: [],
52
+ skills: [],
53
+ settings: {},
54
+ env: { required: [], optional: [] },
55
+ };
56
+ }
57
+
58
+ describe("loadActiveClosure", () => {
59
+ test("resolves the active profile through the injected deps", async () => {
60
+ const resolved: string[] = [];
61
+ const deps: Partial<ActiveClosureDeps> = {
62
+ readManifest: async () => manifest(),
63
+ activeProfile: async () => "other",
64
+ resolveProfile: async (_m, id) => {
65
+ resolved.push(id);
66
+ return closureFor(id);
67
+ },
68
+ };
69
+
70
+ const got = await loadActiveClosure("/proj", { deps });
71
+
72
+ expect(got?.profileId).toBe("other");
73
+ // selectProfileId returns `string | null`; widen the matcher's type so the
74
+ // comparison type-checks. Runtime assertion unchanged.
75
+ expect<string | null | undefined>(got?.profileId).toBe(
76
+ selectProfileId(["default", "other"], undefined, "other"),
77
+ );
78
+ expect(got?.closure.plugins).toEqual({ "other-plugin@magus": "latest" });
79
+ expect(resolved).toEqual(["other"]);
80
+ });
81
+
82
+ test("resolveBins: false hands resolveProfile a bin resolver that yields nothing", async () => {
83
+ let resolver: ((pluginId: string) => Promise<BinRequirement[]>) | undefined;
84
+ const deps: Partial<ActiveClosureDeps> = {
85
+ readManifest: async () => manifest(),
86
+ activeProfile: async () => "default",
87
+ resolveProfile: async (_m, id, opts) => {
88
+ resolver = opts?.binResolver;
89
+ return closureFor(id);
90
+ },
91
+ };
92
+
93
+ await loadActiveClosure("/proj", { resolveBins: false, deps });
94
+
95
+ expect(resolver).toBeDefined();
96
+ expect(await resolver?.("terminal@magus")).toEqual([]);
97
+ });
98
+
99
+ test("an unreadable manifest yields null and never throws", async () => {
100
+ const deps: Partial<ActiveClosureDeps> = {
101
+ readManifest: async () => {
102
+ throw new Error("ENOENT profiles.json");
103
+ },
104
+ activeProfile: async () => "default",
105
+ };
106
+
107
+ expect(await loadActiveClosure("/proj", { deps })).toBeNull();
108
+ });
109
+
110
+ test("no selectable profile (two profiles, none active) yields null", async () => {
111
+ const deps: Partial<ActiveClosureDeps> = {
112
+ readManifest: async () => manifest(),
113
+ activeProfile: async () => null,
114
+ resolveProfile: async (_m, id) => closureFor(id),
115
+ };
116
+
117
+ expect(await loadActiveClosure("/proj", { deps })).toBeNull();
118
+ });
119
+ });