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,526 @@
1
+ /**
2
+ * update-view.ts — every string `magus update` prints, and nothing else.
3
+ *
4
+ * Pure functions: plan in, lines out. No IO, no timers, no terminal. That is
5
+ * what makes the look testable — the old report was `console.log` calls braided
6
+ * through the control flow, so the only way to check a column lined up was to
7
+ * run the command against a real machine.
8
+ *
9
+ * ## The visual contract
10
+ *
11
+ * Three rules, applied mechanically rather than case by case:
12
+ *
13
+ * 1. **A discrete status is a badge**, not a coloured word: dark-or-white ink
14
+ * on a saturated fill, so it reads as a chip at a glance.
15
+ * 2. **A bounded value is a bar**, not a numeral. A step's share of the run's
16
+ * wall clock and the apply loop's completion are both bars; the number
17
+ * labels the bar rather than replacing it.
18
+ * 3. **Dim the chrome, saturate the signal.** A plugin already at the right
19
+ * version is chrome — grey, no chip. Everything that needs a decision or a
20
+ * download is bright. Nine grey rows and three coloured ones is the whole
21
+ * point: the eye lands on the three.
22
+ *
23
+ * Colour never carries meaning alone. Every action has its own glyph and its
24
+ * own word, so the report survives NO_COLOR, a pipe, and colour blindness.
25
+ */
26
+
27
+ import { binCommandFor } from "../services/update-engine.js";
28
+ import type {
29
+ BinUpdateItem,
30
+ PluginUpdateItem,
31
+ SkillUpdateItem,
32
+ UpdatePlan,
33
+ } from "../services/update-plan.js";
34
+ import { brand } from "../ui/theme.js";
35
+ import { formatCommand } from "../utils/command-utils.js";
36
+ import {
37
+ badge,
38
+ bold,
39
+ dim,
40
+ elapsed,
41
+ fg,
42
+ meter,
43
+ padEnd,
44
+ padStart,
45
+ ramps,
46
+ spinner,
47
+ stackedBar,
48
+ sweep,
49
+ width,
50
+ } from "./ansi.js";
51
+
52
+ /** Two spaces of gutter on every line, so the report reads as one block. */
53
+ const PAD = " ";
54
+ /** Bars are this wide everywhere; a bar that changes width cannot be compared. */
55
+ const BAR = 18;
56
+
57
+ // -- action vocabulary -------------------------------------------------------
58
+
59
+ /**
60
+ * The single source of truth for what an action looks like.
61
+ *
62
+ * Glyph, word and hue together — defined once so a plugin `install` and a
63
+ * binary `install` cannot drift into two different blues with two different
64
+ * symbols, which is exactly what happened when each row built its own string.
65
+ */
66
+ interface ActionStyle {
67
+ glyph: string;
68
+ label: string;
69
+ color: string;
70
+ /** Chrome rows are dimmed whole and carry no chip. */
71
+ chrome?: boolean;
72
+ }
73
+
74
+ /** Every action any of the three planners can produce, in one union. */
75
+ type ActionKey =
76
+ | "current"
77
+ | "install"
78
+ | "update"
79
+ | "upgrade"
80
+ | "repair"
81
+ | "refresh"
82
+ | "unknown";
83
+
84
+ const ACTIONS: Record<ActionKey, ActionStyle> = {
85
+ current: { glyph: "=", label: "CURRENT", color: brand.muted, chrome: true },
86
+ install: { glyph: "+", label: "INSTALL", color: brand.link },
87
+ update: { glyph: "↑", label: "UPDATE", color: brand.warning },
88
+ upgrade: { glyph: "↑", label: "UPGRADE", color: brand.warning },
89
+ repair: { glyph: "⟳", label: "REPAIR", color: brand.info },
90
+ refresh: { glyph: "⟳", label: "REFRESH", color: brand.info },
91
+ // Warning, not danger: "cannot tell" is not a failure, and red UNKNOWN read
92
+ // as "broken". The summary line and the TUI already use warning for it.
93
+ unknown: { glyph: "?", label: "UNKNOWN", color: brand.warning },
94
+ };
95
+
96
+ /** Widest label plus the badge's own two spaces of padding. */
97
+ const CHIP_WIDTH =
98
+ Math.max(...Object.values(ACTIONS).map((a) => a.label.length)) + 2;
99
+
100
+ /** A chip for an actionable row; flat dim text for a chrome one. */
101
+ function chip(key: ActionKey): string {
102
+ const style = ACTIONS[key];
103
+ const text = style.chrome
104
+ ? dim(padEnd(` ${style.label} `, CHIP_WIDTH))
105
+ : badge(style.label, style.color);
106
+ return padEnd(text, CHIP_WIDTH);
107
+ }
108
+
109
+ function glyph(key: ActionKey): string {
110
+ return fg(ACTIONS[key].color, ACTIONS[key].glyph);
111
+ }
112
+
113
+ /** Bright for a row that needs something, grey for a row that does not. */
114
+ function rowText(key: ActionKey, text: string): string {
115
+ return ACTIONS[key].chrome ? dim(text) : text;
116
+ }
117
+
118
+ // -- header ------------------------------------------------------------------
119
+
120
+ export function header(profileId: string): string[] {
121
+ return [
122
+ "",
123
+ `${PAD}${fg(brand.accent, "◆")} ${bold("magus update")} ${dim(
124
+ "profile",
125
+ )} ${fg(brand.accent, profileId)}`,
126
+ "",
127
+ ];
128
+ }
129
+
130
+ // -- phase checklist ---------------------------------------------------------
131
+
132
+ export type StepState = "pending" | "running" | "done" | "warn" | "fail";
133
+
134
+ export interface Step {
135
+ label: string;
136
+ state: StepState;
137
+ /** Right-hand summary. While running, say what it is blocked on. */
138
+ detail: string;
139
+ startedAt?: number;
140
+ endedAt?: number;
141
+ /**
142
+ * Countable work, when the step has any.
143
+ *
144
+ * Present → a determinate meter. Absent → an indeterminate sweep, which is
145
+ * the honest widget for a call with no denominator. Never fake a percentage
146
+ * for one: a bar that creeps to 90% and stops is worse than no bar.
147
+ */
148
+ progress?: { done: number; total: number };
149
+ }
150
+
151
+ const STEP_MARK: Record<Exclude<StepState, "running">, [string, string]> = {
152
+ pending: ["·", brand.muted],
153
+ done: ["✓", brand.success],
154
+ warn: ["⚠", brand.warning],
155
+ fail: ["✗", brand.danger],
156
+ };
157
+
158
+ function stepMark(step: Step, tick: number): string {
159
+ if (step.state === "running") return fg(brand.accent, spinner(tick));
160
+ const [mark, color] = STEP_MARK[step.state];
161
+ return fg(color, mark);
162
+ }
163
+
164
+ function stepDuration(step: Step, now: number): number | null {
165
+ if (step.startedAt === undefined) return null;
166
+ return (step.endedAt ?? now) - step.startedAt;
167
+ }
168
+
169
+ const STEP_LABEL_WIDTH = 14;
170
+
171
+ /**
172
+ * The animated frame: one row per step, spinner on whichever is running.
173
+ *
174
+ * Deliberately shows the pending steps too. A single spinner says "busy"; a
175
+ * four-row checklist with one spinner says "busy, on step two of four, and here
176
+ * is what is left" — which is the difference between a hang and progress.
177
+ */
178
+ export function stepFrame(steps: Step[], tick: number, now: number): string[] {
179
+ return steps.map((step) => {
180
+ const ms = stepDuration(step, now);
181
+ const time = ms === null ? "" : dim(padStart(elapsed(ms), 6));
182
+ const label =
183
+ step.state === "pending"
184
+ ? dim(padEnd(step.label, STEP_LABEL_WIDTH))
185
+ : padEnd(step.label, STEP_LABEL_WIDTH);
186
+ // Only the running row carries a bar. A finished row's bar belongs in the
187
+ // record below, where every step is on one scale and comparable; drawing
188
+ // it here too would put two competing bar columns on the same screen.
189
+ const bar =
190
+ step.state !== "running"
191
+ ? " ".repeat(BAR)
192
+ : step.progress && step.progress.total > 0
193
+ ? meter((step.progress.done / step.progress.total) * 100, BAR)
194
+ : sweep(BAR, tick, brand.accent);
195
+ // The bar carries the shape; this labels it. Without the count a half-full
196
+ // meter says "some of it", which is not the same information as "2 of 4".
197
+ const count =
198
+ step.state === "running" && step.progress && step.progress.total > 0
199
+ ? `${fg(brand.accent, `${step.progress.done}/${step.progress.total}`)} `
200
+ : "";
201
+ const detail =
202
+ step.state === "running"
203
+ ? fg(brand.accent, step.detail)
204
+ : dim(step.detail);
205
+ return `${PAD}${stepMark(step, tick)} ${label} ${bar} ${time} ${count}${detail}`;
206
+ });
207
+ }
208
+
209
+ /**
210
+ * The permanent record, with a duration bar per step.
211
+ *
212
+ * The bar is each step's share of the SLOWEST step, not of the total — the
213
+ * question a reader has when an update felt slow is "which part was it?", and
214
+ * shares of a total flatten to a uniform smear once there are four of them.
215
+ * Colour ramps green to red with that share, so the offender is the red one.
216
+ */
217
+ export function stepRecord(steps: Step[], now: number): string[] {
218
+ const times = steps.map((s) => stepDuration(s, now) ?? 0);
219
+ const slowest = Math.max(1, ...times);
220
+ return steps.map((step, i) => {
221
+ const share = (times[i] / slowest) * 100;
222
+ const bar = meter(share, BAR, ramps.severity);
223
+ const label = padEnd(step.label, STEP_LABEL_WIDTH);
224
+ const time = dim(padStart(elapsed(times[i]), 6));
225
+ return `${PAD}${stepMark(step, 0)} ${label} ${bar} ${time} ${dim(
226
+ step.detail,
227
+ )}`;
228
+ });
229
+ }
230
+
231
+ // -- the plan ----------------------------------------------------------------
232
+
233
+ function pluginAction(item: PluginUpdateItem): ActionKey {
234
+ return item.action;
235
+ }
236
+
237
+ /** What the row says on its right: versions, never the action word again. */
238
+ function pluginDetail(item: PluginUpdateItem): string {
239
+ const note = item.note ? ` ${dim(`(${item.note})`)}` : "";
240
+ switch (item.action) {
241
+ case "install":
242
+ return `${item.target ?? "latest"}${note}`;
243
+ case "update":
244
+ return `${dim(item.installed ?? "—")} ${fg(
245
+ brand.warning,
246
+ "→",
247
+ )} ${fg(brand.warning, item.target ?? "?")}${note}`;
248
+ case "repair":
249
+ case "unknown":
250
+ return `${item.installed ?? "—"}${note}`;
251
+ case "current":
252
+ return `${item.installed ?? "—"}${note}`;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * What a tool row says on its right.
258
+ *
259
+ * A `current` row used to print `pinned —` unconditionally, which was true only
260
+ * while "current" implied "pinned". An unpinned tool can be current now, so the
261
+ * pin is shown only when there is one.
262
+ */
263
+ function binDetail(item: BinUpdateItem, plan: UpdatePlan): string {
264
+ const spec = plan.binSpecs.get(item.name);
265
+ // Rendered for the reader's eyes only — this row says what WILL run, while
266
+ // applyBins runs the argv itself. Same builder, so they cannot differ.
267
+ const cmd = spec ? binCommandFor(item, spec) : null;
268
+ const command = cmd ? dim(formatCommand(cmd)) : "";
269
+ switch (item.action) {
270
+ case "current":
271
+ return `${item.installed ?? "—"}${
272
+ item.version ? ` ${dim(`pinned ${item.version}`)}` : ""
273
+ }`;
274
+ case "upgrade":
275
+ return `${dim(item.installed ?? "—")} ${fg(brand.warning, "→")} ${fg(
276
+ brand.warning,
277
+ item.target ?? "?",
278
+ )}${command ? ` ${command}` : ""}${
279
+ // An upgrade that also changes the tool's owner says so here; a
280
+ // plain one reads identically to before.
281
+ item.adopted ? ` ${dim(`(${item.adopted})`)}` : ""
282
+ }`;
283
+ case "unknown":
284
+ return `${item.installed ?? "—"}${
285
+ item.note ? ` ${dim(`(${item.note})`)}` : ""
286
+ }`;
287
+ case "install":
288
+ return command;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * A section: a stacked bar of the action mix, then one row per item.
294
+ *
295
+ * The bar is the section's whole story in one line — a reader who takes in
296
+ * nothing else knows whether this section is mostly settled or mostly work.
297
+ */
298
+ function section(
299
+ title: string,
300
+ rows: Array<{ action: ActionKey; name: string; detail: string }>,
301
+ nameWidth: number,
302
+ ): string[] {
303
+ if (rows.length === 0) return [];
304
+ const counts = new Map<ActionKey, number>();
305
+ for (const row of rows)
306
+ counts.set(row.action, (counts.get(row.action) ?? 0) + 1);
307
+ const bar = stackedBar(
308
+ BAR,
309
+ [...counts].map(([action, value]) => ({
310
+ value,
311
+ color: ACTIONS[action].color,
312
+ })),
313
+ );
314
+ const legend = [...counts]
315
+ .map(([action, n]) =>
316
+ fg(ACTIONS[action].color, `${n} ${ACTIONS[action].label.toLowerCase()}`),
317
+ )
318
+ .join(dim(" · "));
319
+
320
+ return [
321
+ "",
322
+ `${PAD}${bold(padEnd(title, 9))} ${bar} ${dim(
323
+ padStart(String(rows.length), 3),
324
+ )} ${legend}`,
325
+ "",
326
+ ...rows.map(
327
+ (row) =>
328
+ `${PAD}${glyph(row.action)} ${rowText(
329
+ row.action,
330
+ padEnd(row.name, nameWidth),
331
+ )} ${chip(row.action)} ${row.detail}`,
332
+ ),
333
+ ];
334
+ }
335
+
336
+ export function planReport(plan: UpdatePlan): string[] {
337
+ const nameWidth = Math.max(
338
+ 12,
339
+ ...plan.plugins.map((p) => width(p.pluginId)),
340
+ ...plan.bins.map((b) => width(b.name)),
341
+ ...plan.skills.map((s) => width(s.name)),
342
+ );
343
+
344
+ return [
345
+ ...section(
346
+ "Plugins",
347
+ plan.plugins.map((item) => ({
348
+ action: pluginAction(item),
349
+ name: item.pluginId,
350
+ detail: pluginDetail(item),
351
+ })),
352
+ nameWidth,
353
+ ),
354
+ ...section(
355
+ "CLI",
356
+ plan.bins.map((item: BinUpdateItem) => ({
357
+ action: item.action as ActionKey,
358
+ name: item.name,
359
+ detail: binDetail(item, plan),
360
+ })),
361
+ nameWidth,
362
+ ),
363
+ ...section(
364
+ "Skills",
365
+ plan.skills.map((item: SkillUpdateItem) => ({
366
+ action: item.action as ActionKey,
367
+ name: item.name,
368
+ detail: dim(`${item.ref.repo}/${item.ref.path}`),
369
+ })),
370
+ nameWidth,
371
+ ),
372
+ ];
373
+ }
374
+
375
+ // -- apply -------------------------------------------------------------------
376
+
377
+ export interface ApplyState {
378
+ /** Items finished, of `total`. */
379
+ done: number;
380
+ total: number;
381
+ /** What is being worked on right now. */
382
+ current: string;
383
+ startedAt: number;
384
+ }
385
+
386
+ /**
387
+ * The animated apply frame: a gradient meter that turns green as it fills.
388
+ *
389
+ * The meter's ramp encodes POSITION along the fill, which is what a smooth
390
+ * gradient is for — the count beside it is the label, not the signal.
391
+ */
392
+ export function applyFrame(
393
+ state: ApplyState,
394
+ tick: number,
395
+ now: number,
396
+ ): string[] {
397
+ const pct = state.total === 0 ? 100 : (state.done / state.total) * 100;
398
+ const count = `${state.done}/${state.total}`;
399
+ return [
400
+ "",
401
+ `${PAD}${fg(brand.accent, spinner(tick))} ${meter(pct, BAR * 2)} ${padStart(
402
+ count,
403
+ 7,
404
+ )} ${fg(brand.accent, state.current)} ${dim(
405
+ elapsed(now - state.startedAt),
406
+ )}`,
407
+ ];
408
+ }
409
+
410
+ /** One finished item, written permanently above the meter. */
411
+ export function applyRow(
412
+ ok: boolean,
413
+ name: string,
414
+ nameWidth: number,
415
+ detail: string,
416
+ ms: number,
417
+ ): string {
418
+ const mark = ok ? fg(brand.success, "✓") : fg(brand.danger, "✗");
419
+ return `${PAD}${mark} ${padEnd(name, nameWidth)} ${detail} ${dim(
420
+ padStart(elapsed(ms), 7),
421
+ )}`;
422
+ }
423
+
424
+ /**
425
+ * Closing line: the run's shape as a bar, then the counts that label it.
426
+ *
427
+ * `unchanged` is its own count: an installer that exited 0 while read-back
428
+ * shows nothing moved. Folding it into `changed` is how every run printed
429
+ * "3 changed" while changing nothing; it is not a failure either.
430
+ */
431
+ export function summaryLine(
432
+ changed: number,
433
+ current: number,
434
+ unknown: number,
435
+ failed: number,
436
+ ms: number,
437
+ unchanged = 0,
438
+ ): string[] {
439
+ const bar = stackedBar(BAR * 2, [
440
+ { value: changed, color: brand.success },
441
+ { value: current, color: brand.muted },
442
+ { value: unchanged + unknown, color: brand.warning },
443
+ { value: failed, color: brand.danger },
444
+ ]);
445
+ const parts = [
446
+ changed > 0 ? fg(brand.success, `${changed} changed`) : "",
447
+ unchanged > 0 ? fg(brand.warning, `${unchanged} unchanged`) : "",
448
+ current > 0 ? dim(`${current} current`) : "",
449
+ unknown > 0 ? fg(brand.warning, `${unknown} unverified`) : "",
450
+ failed > 0 ? fg(brand.danger, `${failed} failed`) : "",
451
+ ].filter(Boolean);
452
+ return [
453
+ "",
454
+ `${PAD}${bar} ${parts.join(dim(" · "))} ${dim(elapsed(ms))}`,
455
+ "",
456
+ ];
457
+ }
458
+
459
+ // -- one-off notices ---------------------------------------------------------
460
+
461
+ export function ok(text: string): string {
462
+ return `${PAD}${fg(brand.success, "✓")} ${text}`;
463
+ }
464
+
465
+ export function warn(text: string): string {
466
+ return `${PAD}${fg(brand.warning, "⚠")} ${text}`;
467
+ }
468
+
469
+ export function fail(text: string): string {
470
+ return `${PAD}${fg(brand.danger, "✗")} ${text}`;
471
+ }
472
+
473
+ export function info(text: string): string {
474
+ return `${PAD}${fg(brand.info, "›")} ${text}`;
475
+ }
476
+
477
+ export function note(text: string): string {
478
+ return `${PAD} ${dim(text)}`;
479
+ }
480
+
481
+ // -- unverified items ----------------------------------------------------------
482
+ //
483
+ // A plugin is `unknown` for several distinct reasons — its marketplace publishes
484
+ // no version, the two versions cannot be ordered, no marketplace offers it, its
485
+ // marketplace was not read, or the catalog fetch failed — and every row above
486
+ // already prints its own. These lines therefore claim only what holds for EVERY
487
+ // reason. They once advised "re-run once the catalog is reachable", which is
488
+ // false for a plugin whose marketplace publishes no version: re-running changes
489
+ // nothing, and the reader went looking for a network fault that did not exist.
490
+
491
+ /** Unverified plugins, under the plan. Never "up to date" — that is the point. */
492
+ export function unknownPluginsNotice(count: number): string {
493
+ return warn(
494
+ `${bold(String(count))} plugin(s) could not be checked — not applied. They are NOT reported as up to date. Reasons are shown above.`,
495
+ );
496
+ }
497
+
498
+ /** Unverified tools, under the plan. */
499
+ export function unknownToolsNotice(count: number, names: string[]): string {
500
+ return warn(
501
+ `${bold(String(count))} tool(s) could not be checked: ${names.join(", ")} — not applied. Reasons are shown above.`,
502
+ );
503
+ }
504
+
505
+ /**
506
+ * `--check` is about to exit 0 while N items went unchecked.
507
+ *
508
+ * The gate does not fail on an unknown — plugin or tool — because most unknowns
509
+ * are permanent (a marketplace that publishes no version, a tool another npm
510
+ * owns), and a gate that can never pass protects nothing. This line is what
511
+ * keeps the exit 0 honest: it is "nothing is KNOWN to be behind", not
512
+ * "everything was verified". It counts plugins and tools together because the
513
+ * two per-kind notices above already name which is which.
514
+ */
515
+ export function uncheckedGateNotice(count: number): string {
516
+ return warn(
517
+ `${bold(String(count))} item(s) could not be checked — they do not fail this gate. They are NOT reported as up to date. Reasons are shown above.`,
518
+ );
519
+ }
520
+
521
+ /** The run ends with nothing to apply because every remaining plugin is unverified. */
522
+ export function nothingToApplyNotice(unknownPlugins: number): string {
523
+ return warn(
524
+ `Nothing to apply. ${unknownPlugins} plugin(s) could not be checked. They are NOT reported as up to date. Reasons are shown above.`,
525
+ );
526
+ }