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,445 @@
1
+ /**
2
+ * Tests for the `magus update` report.
3
+ *
4
+ * These assert the things a screenshot cannot: that every row lines up at every
5
+ * data shape, that turning colour off leaves a report someone can still read,
6
+ * and that no bar lies about what it measures.
7
+ *
8
+ * The alignment tests exist because the old report built each row with string
9
+ * concatenation and `String.padEnd`, which counts escape bytes as columns. Add
10
+ * one colour to one cell and every column below it shifts — invisible to a
11
+ * character-level assertion, obvious and ugly on screen.
12
+ */
13
+
14
+ import { afterEach, describe, expect, test } from "bun:test";
15
+ import { setColorEnabled, stripAnsi, width } from "../cli/ansi.js";
16
+ import {
17
+ type ApplyState,
18
+ type Step,
19
+ applyFrame,
20
+ applyRow,
21
+ header,
22
+ nothingToApplyNotice,
23
+ planReport,
24
+ stepFrame,
25
+ stepRecord,
26
+ summaryLine,
27
+ uncheckedGateNotice,
28
+ unknownPluginsNotice,
29
+ unknownToolsNotice,
30
+ } from "../cli/update-view.js";
31
+ import type { UpdatePlan } from "../services/update-plan.js";
32
+
33
+ afterEach(() => setColorEnabled(null));
34
+
35
+ // -- fixtures -----------------------------------------------------------------
36
+
37
+ function plan(over: Partial<UpdatePlan> = {}): UpdatePlan {
38
+ return {
39
+ profileId: "default",
40
+ plugins: [
41
+ {
42
+ pluginId: "agentdev@magus",
43
+ pinned: "latest",
44
+ installed: "1.7.0",
45
+ available: "1.7.0",
46
+ target: null,
47
+ action: "current",
48
+ scopes: ["project"],
49
+ },
50
+ {
51
+ pluginId: "dev@magus",
52
+ pinned: "latest",
53
+ installed: "4.6.1",
54
+ available: "5.0.0",
55
+ target: "5.0.0",
56
+ action: "update",
57
+ scopes: ["project"],
58
+ },
59
+ {
60
+ pluginId: "feature-dev@claude-plugins-official",
61
+ pinned: "latest",
62
+ installed: null,
63
+ available: null,
64
+ target: null,
65
+ action: "install",
66
+ scopes: [],
67
+ },
68
+ {
69
+ pluginId: "designer@magus",
70
+ pinned: "latest",
71
+ installed: "1.8.0",
72
+ available: null,
73
+ target: null,
74
+ action: "unknown",
75
+ scopes: ["project"],
76
+ note: "catalog unreachable",
77
+ },
78
+ ],
79
+ bins: [
80
+ {
81
+ name: "tmux-mcp",
82
+ action: "current",
83
+ reason: "at-pin",
84
+ version: "v1.7.1",
85
+ installed: "v1.7.1",
86
+ latest: null,
87
+ target: null,
88
+ sources: [],
89
+ },
90
+ {
91
+ name: "tmux",
92
+ action: "upgrade",
93
+ reason: "behind",
94
+ installed: "3.7b",
95
+ latest: "3.7c",
96
+ target: "3.7c",
97
+ note: "3.7c is available",
98
+ sources: [],
99
+ },
100
+ ],
101
+ skills: [
102
+ {
103
+ name: "release",
104
+ action: "install",
105
+ ref: { name: "release", repo: "MadAppGang/magus", path: "s/SKILL.md" },
106
+ },
107
+ ],
108
+ binSpecs: new Map([
109
+ ["tmux-mcp", { name: "tmux-mcp", via: "bun", sources: [] }],
110
+ ["tmux", { name: "tmux", via: "brew", formula: "tmux", sources: [] }],
111
+ ]),
112
+ ...over,
113
+ } as UpdatePlan;
114
+ }
115
+
116
+ function steps(): Step[] {
117
+ return [
118
+ {
119
+ label: "profile",
120
+ state: "done",
121
+ detail: "4 plugins",
122
+ startedAt: 0,
123
+ endedAt: 100,
124
+ },
125
+ {
126
+ label: "marketplaces",
127
+ state: "running",
128
+ detail: "pulling magus",
129
+ startedAt: 100,
130
+ progress: { done: 1, total: 4 },
131
+ },
132
+ { label: "catalog", state: "pending", detail: "" },
133
+ ];
134
+ }
135
+
136
+ // -- alignment ----------------------------------------------------------------
137
+
138
+ describe("alignment", () => {
139
+ test("every plugin row shares one column layout, colour or not", () => {
140
+ setColorEnabled(true);
141
+ const rows = planReport(plan())
142
+ .filter((l) => /@/.test(stripAnsi(l)))
143
+ .map((l) => stripAnsi(l));
144
+ // Every row's version column starts at the same visible offset, whatever
145
+ // mix of badges, dim text and notes the rows carry.
146
+ const offsets = rows.map((r) => r.indexOf("1.") + r.indexOf("latest"));
147
+ expect(offsets.length).toBeGreaterThan(0);
148
+ const versionCol = rows.map((r) =>
149
+ r.search(/(1\.7\.0|4\.6\.1|latest|1\.8\.0)/),
150
+ );
151
+ expect(new Set(versionCol).size).toBe(1);
152
+ });
153
+
154
+ test("the same row is the same visible width with colour on and off", () => {
155
+ setColorEnabled(false);
156
+ const plain = planReport(plan()).map((l) => width(l));
157
+ setColorEnabled(true);
158
+ const painted = planReport(plan()).map((l) => width(l));
159
+ expect(painted).toEqual(plain);
160
+ });
161
+
162
+ test("a very long plugin id widens the column for every row", () => {
163
+ setColorEnabled(false);
164
+ const long = "a-very-long-plugin-name-indeed@some-marketplace";
165
+ const p = plan();
166
+ p.plugins[0]!.pluginId = long;
167
+ const rows = planReport(p).filter((l) => l.includes("@"));
168
+ const cols = rows.map((r) => r.search(/(1\.7\.0|4\.6\.1|latest|1\.8\.0)/));
169
+ expect(new Set(cols).size).toBe(1);
170
+ expect(rows[0]).toContain(long);
171
+ });
172
+ });
173
+
174
+ // -- readability without colour ----------------------------------------------
175
+
176
+ describe("NO_COLOR", () => {
177
+ test("the report emits no escape sequence at all", () => {
178
+ setColorEnabled(false);
179
+ const out = [
180
+ ...header("default"),
181
+ ...stepFrame(steps(), 3, 500),
182
+ ...stepRecord(steps(), 500),
183
+ ...planReport(plan()),
184
+ ...applyFrame(
185
+ { done: 1, total: 4, current: "dev@magus", startedAt: 0 },
186
+ 2,
187
+ 900,
188
+ ),
189
+ applyRow(true, "dev@magus", 20, "project", 400),
190
+ ...summaryLine(2, 5, 1, 0, 9000),
191
+ ].join("\n");
192
+ expect(out).not.toContain("\x1b");
193
+ });
194
+
195
+ test("every action stays distinguishable without colour", () => {
196
+ setColorEnabled(false);
197
+ const out = planReport(plan()).join("\n");
198
+ // Colour never carries meaning alone: each action has its own word AND
199
+ // its own glyph, so the report survives a pipe and colour blindness.
200
+ for (const label of ["CURRENT", "UPDATE", "INSTALL", "UNKNOWN", "UPGRADE"])
201
+ expect(out).toContain(label);
202
+ for (const glyph of ["=", "↑", "+", "?"]) expect(out).toContain(glyph);
203
+ });
204
+
205
+ test("a note rides along on the row it explains", () => {
206
+ setColorEnabled(false);
207
+ const row = planReport(plan()).find((l) => l.includes("designer@magus"))!;
208
+ expect(row).toContain("catalog unreachable");
209
+ });
210
+ });
211
+
212
+ // -- the bars tell the truth --------------------------------------------------
213
+
214
+ describe("bars", () => {
215
+ test("a running step with a denominator gets a determinate meter", () => {
216
+ setColorEnabled(false);
217
+ const frame = stepFrame(steps(), 0, 500);
218
+ // 1 of 4 → a quarter of the 18-cell bar filled.
219
+ const bar = frame[1]!.match(/[█░]+/)?.[0] ?? "";
220
+ expect(bar).toHaveLength(18);
221
+ expect(bar.split("█").length - 1).toBe(5);
222
+ });
223
+
224
+ test("a running step with no denominator gets a sweep, not a fake meter", () => {
225
+ setColorEnabled(false);
226
+ const s = steps();
227
+ s[1]!.progress = undefined;
228
+ const bar = stepFrame(s, 3, 500)[1]!.match(/[█░]+/)?.[0] ?? "";
229
+ expect(bar).toHaveLength(18);
230
+ // A sweep is partly filled at every tick; a "0%" meter would be all track.
231
+ expect(bar).toContain("█");
232
+ expect(bar).toContain("░");
233
+ });
234
+
235
+ test("only the running row carries a bar in the live frame", () => {
236
+ setColorEnabled(false);
237
+ const frame = stepFrame(steps(), 0, 500);
238
+ expect(frame[0]).not.toMatch(/[█░]/);
239
+ expect(frame[1]).toMatch(/[█░]/);
240
+ expect(frame[2]).not.toMatch(/[█░]/);
241
+ });
242
+
243
+ test("the record scales each step against the slowest, not the total", () => {
244
+ setColorEnabled(false);
245
+ const record = stepRecord(
246
+ [
247
+ {
248
+ label: "fast",
249
+ state: "done",
250
+ detail: "",
251
+ startedAt: 0,
252
+ endedAt: 100,
253
+ },
254
+ {
255
+ label: "slow",
256
+ state: "done",
257
+ detail: "",
258
+ startedAt: 0,
259
+ endedAt: 1000,
260
+ },
261
+ ],
262
+ 1000,
263
+ );
264
+ const bars = record.map((r) => r.match(/[█░]+/)![0]);
265
+ expect(bars[1]!.split("█").length - 1).toBe(18);
266
+ expect(bars[0]!.split("█").length - 1).toBe(2);
267
+ });
268
+
269
+ test("a step that never ran does not divide by zero", () => {
270
+ setColorEnabled(false);
271
+ const record = stepRecord(
272
+ [{ label: "x", state: "pending", detail: "" }],
273
+ 0,
274
+ );
275
+ expect(record[0]).toContain("░");
276
+ });
277
+
278
+ test("the apply meter is full exactly when the work is done", () => {
279
+ setColorEnabled(false);
280
+ const state: ApplyState = { done: 4, total: 4, current: "", startedAt: 0 };
281
+ const bar = applyFrame(state, 0, 100)[1]!.match(/[█░]+/)![0];
282
+ expect(bar).toBe("█".repeat(36));
283
+ });
284
+
285
+ test("an empty apply reads as complete rather than as stuck at zero", () => {
286
+ setColorEnabled(false);
287
+ const state: ApplyState = { done: 0, total: 0, current: "", startedAt: 0 };
288
+ expect(applyFrame(state, 0, 100)[1]).toContain("█".repeat(36));
289
+ });
290
+
291
+ test("the summary bar survives an all-zero run", () => {
292
+ setColorEnabled(false);
293
+ const line = summaryLine(0, 0, 0, 0, 100)[1]!;
294
+ expect(line.match(/[█░]+/)![0]).toHaveLength(36);
295
+ });
296
+
297
+ test("unchanged is its own count, never folded into changed", () => {
298
+ setColorEnabled(false);
299
+ const line = summaryLine(1, 4, 0, 0, 100, 2)[1]!;
300
+ expect(line).toContain("1 changed");
301
+ expect(line).toContain("2 unchanged");
302
+ // Omitted when zero, and the parameter defaults to zero.
303
+ expect(summaryLine(1, 4, 0, 0, 100)[1]).not.toContain("unchanged");
304
+ });
305
+ });
306
+
307
+ // -- misc ---------------------------------------------------------------------
308
+
309
+ describe("sections", () => {
310
+ test("an empty section is omitted entirely, not printed as a heading", () => {
311
+ setColorEnabled(false);
312
+ const out = planReport(plan({ bins: [], skills: [] }) as UpdatePlan).join(
313
+ "\n",
314
+ );
315
+ expect(out).toContain("Plugins");
316
+ expect(out).not.toContain("CLI");
317
+ expect(out).not.toContain("Skills");
318
+ });
319
+
320
+ test("a section legend counts every action it contains", () => {
321
+ setColorEnabled(false);
322
+ const line = planReport(plan()).find((l) => l.includes("Plugins"))!;
323
+ expect(line).toContain("1 current");
324
+ expect(line).toContain("1 update");
325
+ expect(line).toContain("1 install");
326
+ expect(line).toContain("1 unknown");
327
+ });
328
+
329
+ test("a CLI upgrade shows the command that will run", () => {
330
+ setColorEnabled(false);
331
+ const row = planReport(plan()).find(
332
+ (l) => l.includes("tmux ") && l.includes("UPGRADE"),
333
+ )!;
334
+ expect(row).toContain("brew upgrade tmux");
335
+ expect(row).toContain("3.7b → 3.7c");
336
+ });
337
+
338
+ test("an unpinned CURRENT tool shows its version, not 'pinned —' (F8)", () => {
339
+ setColorEnabled(false);
340
+ const p = plan();
341
+ p.bins = [
342
+ {
343
+ name: "tmux",
344
+ action: "current",
345
+ reason: "up-to-date",
346
+ installed: "3.7c",
347
+ latest: null,
348
+ target: null,
349
+ sources: [],
350
+ },
351
+ ];
352
+ const row = planReport(p).find(
353
+ (l) => l.includes("tmux ") && l.includes("CURRENT"),
354
+ )!;
355
+ expect(row).toContain("3.7c");
356
+ expect(row).not.toContain("pinned");
357
+ });
358
+
359
+ test("a pinned current tool still names its pin", () => {
360
+ setColorEnabled(false);
361
+ const row = planReport(plan()).find((l) => l.includes("tmux-mcp"))!;
362
+ expect(row).toContain("pinned v1.7.1");
363
+ });
364
+
365
+ test("an UNKNOWN tool shows its reason and no command", () => {
366
+ setColorEnabled(false);
367
+ const p = plan();
368
+ p.bins = [
369
+ {
370
+ name: "crush",
371
+ action: "unknown",
372
+ reason: "not-managed",
373
+ note: "on PATH at /n/crush.js, which npm (root /l) does not own",
374
+ installed: "0.7.1",
375
+ latest: null,
376
+ target: null,
377
+ sources: [],
378
+ },
379
+ ];
380
+ p.binSpecs = new Map([
381
+ [
382
+ "crush",
383
+ { name: "crush", via: "npm", package: "@charmland/crush", sources: [] },
384
+ ],
385
+ ]);
386
+ const row = planReport(p).find((l) => l.includes("crush"))!;
387
+ expect(row).toContain("0.7.1");
388
+ expect(row).toContain("which npm (root /l) does not own");
389
+ expect(row).not.toContain("npm install");
390
+ });
391
+ });
392
+
393
+ // -- unverified notices -------------------------------------------------------
394
+
395
+ describe("unverified notices", () => {
396
+ // A plugin is `unknown` for several reasons, each printed on its own row. The
397
+ // closing notices must hold for every one of them. They used to say "re-run
398
+ // once the catalog is reachable", which is false advice for a plugin whose
399
+ // marketplace publishes no version — the case a real dry run showed five of.
400
+ const notices = () => [
401
+ unknownPluginsNotice(5),
402
+ nothingToApplyNotice(5),
403
+ unknownToolsNotice(1, ["crush"]),
404
+ uncheckedGateNotice(6),
405
+ ];
406
+
407
+ test("never advise a re-run, never blame the catalog", () => {
408
+ setColorEnabled(false);
409
+ for (const line of notices()) {
410
+ expect(line).not.toMatch(/re-run/i);
411
+ expect(line).not.toContain("catalog is reachable");
412
+ expect(line).toContain("Reasons are shown above.");
413
+ }
414
+ });
415
+
416
+ test("an unverified plugin is never reported as up to date", () => {
417
+ setColorEnabled(false);
418
+ const under = unknownPluginsNotice(5);
419
+ expect(under).toContain("5 plugin(s) could not be checked");
420
+ expect(under).toContain("They are NOT reported as up to date.");
421
+ const nothing = nothingToApplyNotice(5);
422
+ expect(nothing.trimStart()).toStartWith("⚠ Nothing to apply.");
423
+ expect(nothing).toContain("5 plugin(s) could not be checked");
424
+ expect(nothing).toContain("They are NOT reported as up to date.");
425
+ });
426
+
427
+ test("the tool notice names the tools it could not check", () => {
428
+ setColorEnabled(false);
429
+ expect(unknownToolsNotice(2, ["crush", "tmux"])).toContain(
430
+ "2 tool(s) could not be checked: crush, tmux — not applied.",
431
+ );
432
+ });
433
+
434
+ test("the gate notice says the unknowns passed without being verified", () => {
435
+ // `--check` exits 0 with unknowns present, so this line carries the whole
436
+ // difference between "nothing is known to be behind" and "all verified".
437
+ setColorEnabled(false);
438
+ const line = uncheckedGateNotice(6);
439
+ expect(line).toContain("6 item(s) could not be checked");
440
+ expect(line).toContain("they do not fail this gate");
441
+ expect(line).toContain("They are NOT reported as up to date.");
442
+ // A warning, not a failure: the gate is about to pass.
443
+ expect(line.trimStart()).toStartWith("⚠");
444
+ });
445
+ });