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,523 @@
1
+ /**
2
+ * BLACK-BOX: R1 — one plugin classifier, and the TUI text contract (§8.1, §8.7).
3
+ * Written from requirements.md and architecture §8 only.
4
+ */
5
+ import { describe, expect, test } from "bun:test";
6
+ import type { PluginInfo } from "../services/plugin-manager.js";
7
+ import {
8
+ type PluginUpdateItem,
9
+ type UpdatePlan,
10
+ classifyPlugin,
11
+ classifyPluginRows,
12
+ countPluginStates,
13
+ indexCatalog,
14
+ pinForRow,
15
+ planHasWork,
16
+ planIsBehind,
17
+ planPluginUpdates,
18
+ scopeIsBehind,
19
+ } from "../services/update-plan.js";
20
+ import type { PluginPluginItem } from "../ui/adapters/pluginsAdapter.js";
21
+ import { pluginRowText } from "../ui/renderers/pluginRowText.js";
22
+ import { pluginStatusText } from "../ui/renderers/pluginStatusText.js";
23
+
24
+ function info(
25
+ id: string,
26
+ catalogVersion: string | null,
27
+ installed: string | null,
28
+ extra: Partial<PluginInfo> = {},
29
+ ): PluginInfo {
30
+ const at = id.indexOf("@");
31
+ return {
32
+ id,
33
+ name: at >= 0 ? id.slice(0, at) : id,
34
+ version: catalogVersion,
35
+ description: "",
36
+ marketplace: at >= 0 ? id.slice(at + 1) : "",
37
+ marketplaceDisplay: at >= 0 ? id.slice(at + 1) : "",
38
+ enabled: installed !== null,
39
+ ...(installed !== null
40
+ ? {
41
+ installedVersion: installed,
42
+ projectScope: { enabled: true, version: installed },
43
+ }
44
+ : {}),
45
+ ...extra,
46
+ };
47
+ }
48
+
49
+ function item(over: Partial<PluginUpdateItem>): PluginUpdateItem {
50
+ return {
51
+ pluginId: "x@magus",
52
+ pinned: "latest",
53
+ installed: "1.0.0",
54
+ available: "1.0.0",
55
+ target: null,
56
+ action: "current",
57
+ reason: "up-to-date",
58
+ scopes: [],
59
+ beforeVersions: {},
60
+ ...over,
61
+ };
62
+ }
63
+
64
+ describe("indexCatalog", () => {
65
+ const idx = indexCatalog([
66
+ info("dev@magus", "1.0.0", "1.0.0"),
67
+ info("old@magus", null, "0.1.0", { isOrphaned: true }),
68
+ info("solo@lonely", null, "0.1.0", { isOrphaned: true }),
69
+ ]);
70
+
71
+ test("isListed is true only for a catalog row that is not an orphan", () => {
72
+ expect(idx.isListed("dev@magus")).toBe(true);
73
+ expect(idx.isListed("old@magus")).toBe(false);
74
+ expect(idx.isListed("missing@magus")).toBe(false);
75
+ });
76
+
77
+ test("marketplaceIsKnown is true when another listed id shares the marketplace suffix", () => {
78
+ expect(idx.marketplaceIsKnown("missing@magus")).toBe(true);
79
+ expect(idx.marketplaceIsKnown("solo@lonely")).toBe(false);
80
+ expect(idx.marketplaceIsKnown("no-at-sign")).toBe(false);
81
+ });
82
+
83
+ test("accepts a Map as well as an array", () => {
84
+ const fromMap = indexCatalog(
85
+ new Map([["dev@magus", info("dev@magus", "1.0.0", null)]]),
86
+ );
87
+ expect(fromMap.get("dev@magus")?.version).toBe("1.0.0");
88
+ expect(fromMap.isListed("dev@magus")).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe("classifyPlugin — one state plus a human reason", () => {
93
+ test("declared, absent from the catalog, marketplace read: unknown / not-offered, no target", () => {
94
+ const idx = indexCatalog([info("dev@magus", "1.0.0", "1.0.0")]);
95
+
96
+ const got = classifyPlugin("gone@magus", "latest", idx);
97
+
98
+ expect({
99
+ action: got.action,
100
+ reason: got.reason,
101
+ target: got.target,
102
+ }).toEqual({
103
+ action: "unknown",
104
+ reason: "not-offered",
105
+ target: null,
106
+ });
107
+ expect(got.note).toBeTruthy();
108
+ });
109
+
110
+ test("installed orphan whose marketplace contributes no listed id: unknown / marketplace-unread", () => {
111
+ const idx = indexCatalog([
112
+ info("dev@magus", "1.0.0", "1.0.0"),
113
+ info("ghost@unread-mp", null, "0.3.0", { isOrphaned: true }),
114
+ ]);
115
+
116
+ const got = classifyPlugin("ghost@unread-mp", "latest", idx);
117
+
118
+ expect({ action: got.action, reason: got.reason }).toEqual({
119
+ action: "unknown",
120
+ reason: "marketplace-unread",
121
+ });
122
+ });
123
+
124
+ test("declared latest, installed nowhere: install / absent, aiming at the catalog version", () => {
125
+ const idx = indexCatalog([info("bunjs@magus", "0.4.4", null)]);
126
+
127
+ const got = classifyPlugin("bunjs@magus", "latest", idx);
128
+
129
+ expect({
130
+ action: got.action,
131
+ reason: got.reason,
132
+ target: got.target,
133
+ }).toEqual({
134
+ action: "install",
135
+ reason: "absent",
136
+ target: "0.4.4",
137
+ });
138
+ });
139
+
140
+ test("installed behind the catalog: update / behind with the scope and its before-version", () => {
141
+ const idx = indexCatalog([info("designer@magus", "2.0.0", "1.5.0")]);
142
+
143
+ const got = classifyPlugin("designer@magus", "latest", idx);
144
+
145
+ expect({
146
+ action: got.action,
147
+ reason: got.reason,
148
+ target: got.target,
149
+ installed: got.installed,
150
+ available: got.available,
151
+ }).toEqual({
152
+ action: "update",
153
+ reason: "behind",
154
+ target: "2.0.0",
155
+ installed: "1.5.0",
156
+ available: "2.0.0",
157
+ });
158
+ expect(got.scopes).toContain("project");
159
+ expect(got.beforeVersions.project).toBe("1.5.0");
160
+ });
161
+
162
+ test("installed equal to the catalog: current / up-to-date", () => {
163
+ const idx = indexCatalog([info("dev@magus", "7.4.0", "7.4.0")]);
164
+
165
+ const got = classifyPlugin("dev@magus", "latest", idx);
166
+
167
+ expect({ action: got.action, reason: got.reason }).toEqual({
168
+ action: "current",
169
+ reason: "up-to-date",
170
+ });
171
+ });
172
+
173
+ test("unversioned catalog with a commit-sha install is unknown, never current (report #1)", () => {
174
+ const idx = indexCatalog([
175
+ info("code-review@claude-plugins-official", null, "3ea32df27be7"),
176
+ ]);
177
+
178
+ const got = classifyPlugin(
179
+ "code-review@claude-plugins-official",
180
+ "latest",
181
+ idx,
182
+ );
183
+
184
+ expect({
185
+ action: got.action,
186
+ reason: got.reason,
187
+ target: got.target,
188
+ }).toEqual({
189
+ action: "unknown",
190
+ reason: "no-published-version",
191
+ target: null,
192
+ });
193
+ expect(got.note).toBeTruthy();
194
+ });
195
+
196
+ test("a failed catalog check on a latest pin is unknown / catalog-unverified, never current", () => {
197
+ const idx = indexCatalog([
198
+ info("claudish@magus", "2.0.0", "2.0.0", { updateCheckFailed: true }),
199
+ ]);
200
+
201
+ const got = classifyPlugin("claudish@magus", "latest", idx);
202
+
203
+ expect({ action: got.action, reason: got.reason }).toEqual({
204
+ action: "unknown",
205
+ reason: "catalog-unverified",
206
+ });
207
+ });
208
+
209
+ test("every unknown item carries a note and no target", () => {
210
+ const idx = indexCatalog([
211
+ info("a@magus", null, "abc123def456"),
212
+ info("b@magus", null, "1.0.0", { isOrphaned: true }),
213
+ info("c@magus", "1.0.0", "1.0.0", { updateCheckFailed: true }),
214
+ info("e@nowhere", null, "0.3.0", { isOrphaned: true }),
215
+ ]);
216
+
217
+ for (const id of [
218
+ "a@magus",
219
+ "b@magus",
220
+ "c@magus",
221
+ "d@magus",
222
+ "e@nowhere",
223
+ ]) {
224
+ const got = classifyPlugin(id, "latest", idx);
225
+ expect({ id, action: got.action }).toEqual({ id, action: "unknown" });
226
+ expect({ id, target: got.target, hasNote: Boolean(got.note) }).toEqual({
227
+ id,
228
+ target: null,
229
+ hasNote: true,
230
+ });
231
+ }
232
+ });
233
+ });
234
+
235
+ describe("planPluginUpdates is classifyPlugin over every pin, sorted", () => {
236
+ test("output is sorted by pluginId and equals classifyPlugin item for item", () => {
237
+ const catalog = [
238
+ info("zeta@magus", "2.0.0", "1.0.0"),
239
+ info("alpha@magus", "1.0.0", "1.0.0"),
240
+ info("mid@magus", null, "abc123def456"),
241
+ ];
242
+ const pins = {
243
+ "zeta@magus": "latest",
244
+ "alpha@magus": "latest",
245
+ "mid@magus": "latest",
246
+ };
247
+
248
+ const plan = planPluginUpdates(
249
+ pins,
250
+ new Map(catalog.map((c) => [c.id, c])),
251
+ );
252
+
253
+ expect(plan.map((p) => p.pluginId)).toEqual([
254
+ "alpha@magus",
255
+ "mid@magus",
256
+ "zeta@magus",
257
+ ]);
258
+ const idx = indexCatalog(catalog);
259
+ for (const p of plan) {
260
+ expect(p).toEqual(
261
+ classifyPlugin(p.pluginId, pins[p.pluginId as keyof typeof pins], idx),
262
+ );
263
+ }
264
+ });
265
+ });
266
+
267
+ describe("pinForRow / classifyPluginRows", () => {
268
+ const declared = info("dev@magus", "1.0.0", "1.0.0");
269
+ const installedOnly = info("extra@magus", "1.0.0", "1.0.0");
270
+ const enabledOnly = info("en@magus", "1.0.0", null, {
271
+ enabled: true,
272
+ userScope: { enabled: true },
273
+ });
274
+ const browseOnly = info("browse@magus", "1.0.0", null);
275
+ const pins = { "dev@magus": "1.0.0" };
276
+
277
+ test("closure pin wins; installed or enabled rows are judged against latest; browse-only has no pin", () => {
278
+ expect(pinForRow(declared, pins)).toBe("1.0.0");
279
+ expect(pinForRow(installedOnly, pins)).toBe("latest");
280
+ expect(pinForRow(enabledOnly, pins)).toBe("latest");
281
+ expect(pinForRow(browseOnly, pins)).toBeNull();
282
+ });
283
+
284
+ test("classifyPluginRows has one status per pinned row and none for browse-only rows", () => {
285
+ const rows = classifyPluginRows(
286
+ [declared, installedOnly, enabledOnly, browseOnly],
287
+ pins,
288
+ );
289
+
290
+ expect([...rows.keys()].sort()).toEqual([
291
+ "dev@magus",
292
+ "en@magus",
293
+ "extra@magus",
294
+ ]);
295
+ expect(rows.get("en@magus")?.action).toBe("install");
296
+ expect(rows.get("en@magus")?.reason).toBe("enabled-not-installed");
297
+ });
298
+ });
299
+
300
+ describe("countPluginStates", () => {
301
+ test("tallies each state exactly", () => {
302
+ const counts = countPluginStates([
303
+ item({ action: "install", reason: "absent" }),
304
+ item({ action: "update", reason: "behind" }),
305
+ item({ action: "update", reason: "behind" }),
306
+ item({ action: "repair", reason: "content-stale" }),
307
+ item({ action: "current" }),
308
+ item({ action: "unknown", reason: "no-published-version" }),
309
+ ]);
310
+
311
+ expect(counts).toEqual({
312
+ install: 1,
313
+ update: 2,
314
+ repair: 1,
315
+ current: 1,
316
+ unknown: 1,
317
+ });
318
+ });
319
+
320
+ test("an empty input counts zero everywhere", () => {
321
+ expect(countPluginStates([])).toEqual({
322
+ install: 0,
323
+ update: 0,
324
+ repair: 0,
325
+ current: 0,
326
+ unknown: 0,
327
+ });
328
+ });
329
+ });
330
+
331
+ describe("planIsBehind — plugin actions at the --check gate", () => {
332
+ function planOf(plugins: PluginUpdateItem[]): UpdatePlan {
333
+ return {
334
+ profileId: "default",
335
+ plugins,
336
+ bins: [],
337
+ skills: [],
338
+ binSpecs: new Map(),
339
+ };
340
+ }
341
+
342
+ test("install, update and repair fail the gate; current and unknown do not", () => {
343
+ // The unknown row is the point: "could not be checked" is not evidence of
344
+ // drift, and for a marketplace that publishes no version it never will be,
345
+ // so the same rule as an unknown tool applies. It is still not APPLIED,
346
+ // which is planHasWork's business, not this gate's.
347
+ const table: Array<[PluginUpdateItem["action"], boolean]> = [
348
+ ["install", true],
349
+ ["update", true],
350
+ ["repair", true],
351
+ ["current", false],
352
+ ["unknown", false],
353
+ ];
354
+ for (const [action, behind] of table) {
355
+ const plan = planOf([item({ action })]);
356
+ expect({ action, behind: planIsBehind(plan) }).toEqual({
357
+ action,
358
+ behind,
359
+ });
360
+ }
361
+ });
362
+
363
+ test("one unknown among current rows does not fail the gate; one update does", () => {
364
+ const unknowns = planOf([
365
+ item({ action: "current" }),
366
+ item({ action: "unknown", reason: "no-published-version" }),
367
+ ]);
368
+ expect(planIsBehind(unknowns)).toBe(false);
369
+ expect(planHasWork(unknowns)).toBe(true);
370
+
371
+ const withUpdate = planOf([
372
+ item({ action: "unknown", reason: "no-published-version" }),
373
+ item({ action: "update", reason: "behind" }),
374
+ ]);
375
+ expect(planIsBehind(withUpdate)).toBe(true);
376
+ });
377
+ });
378
+
379
+ describe("scopeIsBehind", () => {
380
+ test("true only for an update whose scopes include that scope", () => {
381
+ const upd = item({
382
+ action: "update",
383
+ reason: "behind",
384
+ scopes: ["project"],
385
+ });
386
+
387
+ expect(scopeIsBehind(upd, "project")).toBe(true);
388
+ expect(scopeIsBehind(upd, "user")).toBe(false);
389
+ expect(
390
+ scopeIsBehind(
391
+ item({ action: "unknown", scopes: ["project"] }),
392
+ "project",
393
+ ),
394
+ ).toBe(false);
395
+ expect(scopeIsBehind(null, "project")).toBe(false);
396
+ expect(scopeIsBehind(undefined, "user")).toBe(false);
397
+ });
398
+ });
399
+
400
+ describe("pluginStatusText — the TUI suffix contract (§8.7)", () => {
401
+ test("null status: empty, muted", () => {
402
+ expect(pluginStatusText(null)).toEqual({ suffix: "", tone: "muted" });
403
+ });
404
+
405
+ test("unknown: ' ? <note>' with the note verbatim", () => {
406
+ const note =
407
+ "its marketplace publishes no version for this plugin, so there is nothing to compare";
408
+ expect(
409
+ pluginStatusText(
410
+ item({ action: "unknown", reason: "no-published-version", note }),
411
+ ).suffix,
412
+ ).toBe(` ? ${note}`);
413
+ });
414
+
415
+ test("update: ' → v<target> available', plus ' (<note>)' when a note is set", () => {
416
+ expect(
417
+ pluginStatusText(
418
+ item({ action: "update", reason: "behind", target: "2.0.0" }),
419
+ ).suffix,
420
+ ).toBe(" → v2.0.0 available");
421
+ expect(
422
+ pluginStatusText(
423
+ item({
424
+ action: "update",
425
+ reason: "pin-undeliverable",
426
+ target: "1.0.0",
427
+ note: "pin n",
428
+ }),
429
+ ).suffix,
430
+ ).toBe(" → v1.0.0 available (pin n)");
431
+ });
432
+
433
+ test("repair: ' stale — reinstall (<note>)'", () => {
434
+ expect(
435
+ pluginStatusText(
436
+ item({
437
+ action: "repair",
438
+ reason: "content-stale",
439
+ note: "files moved",
440
+ }),
441
+ ).suffix,
442
+ ).toBe(" stale — reinstall (files moved)");
443
+ });
444
+
445
+ test("install: absent claims the profile declares it; enabled-not-installed shows its note instead", () => {
446
+ expect(
447
+ pluginStatusText(
448
+ item({ action: "install", reason: "absent", installed: null }),
449
+ ).suffix,
450
+ ).toBe(" not installed — profile declares it");
451
+ const enabled = pluginStatusText(
452
+ item({
453
+ action: "install",
454
+ reason: "enabled-not-installed",
455
+ installed: null,
456
+ note: "enabled in user settings",
457
+ }),
458
+ ).suffix;
459
+ expect(enabled).toBe(" not installed (enabled in user settings)");
460
+ expect(enabled).not.toContain("profile declares");
461
+ });
462
+
463
+ test("current: empty, or ' (<note>)' for pin-undeliverable", () => {
464
+ expect(pluginStatusText(item({ action: "current" })).suffix).toBe("");
465
+ expect(
466
+ pluginStatusText(
467
+ item({ action: "current", reason: "pin-undeliverable", note: "pin n" }),
468
+ ).suffix,
469
+ ).toBe(" (pin n)");
470
+ });
471
+ });
472
+
473
+ describe("pluginRowText", () => {
474
+ function rowItem(
475
+ plugin: PluginInfo,
476
+ status: PluginUpdateItem | null,
477
+ ): PluginPluginItem {
478
+ return {
479
+ id: `plugin:${plugin.id}`,
480
+ kind: "plugin",
481
+ label: plugin.name,
482
+ plugin,
483
+ isExpandable: false,
484
+ isExpanded: false,
485
+ status,
486
+ };
487
+ }
488
+
489
+ test("an unknown status on a 0.0.0 install still renders the note (no version gate)", () => {
490
+ const plugin = info("zero-version@claude-plugins-official", null, "0.0.0");
491
+ const status = classifyPlugin(plugin.id, "latest", indexCatalog([plugin]));
492
+
493
+ const out = pluginRowText(rowItem(plugin, status));
494
+
495
+ expect(status.action).toBe("unknown");
496
+ expect(out.text).toContain(status.note ?? "<missing note>");
497
+ });
498
+
499
+ test("text is exactly the segments joined, and contains the status suffix", () => {
500
+ const plugin = info("designer@magus", "2.0.0", "1.5.0");
501
+ const status = classifyPlugin(plugin.id, "latest", indexCatalog([plugin]));
502
+
503
+ const out = pluginRowText(rowItem(plugin, status));
504
+
505
+ expect(out.text).toBe(out.segments.map((s) => s.text).join(""));
506
+ expect(out.text).toContain(pluginStatusText(status).suffix);
507
+ });
508
+
509
+ test("an orphan row renders its note", () => {
510
+ const plugin = info("legacy@magus", null, "0.5.0", { isOrphaned: true });
511
+ const sibling = info("dev@magus", "1.0.0", "1.0.0");
512
+ const status = classifyPlugin(
513
+ plugin.id,
514
+ "latest",
515
+ indexCatalog([plugin, sibling]),
516
+ );
517
+
518
+ const out = pluginRowText(rowItem(plugin, status));
519
+
520
+ expect(status.action).toBe("unknown");
521
+ expect(out.text).toContain(status.note ?? "<missing note>");
522
+ });
523
+ });