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.
- package/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CLI Tools row, built from the SAME probe facts and the SAME classifier
|
|
3
|
+
* `magus update` plans with.
|
|
4
|
+
*
|
|
5
|
+
* The screen used to carry its own probe — a shell `which`, `npm view` per
|
|
6
|
+
* tool, and a private comparator that read "1.2.3-beta" as "1.2.3" — so the
|
|
7
|
+
* row and the CLI could disagree about the same tool. Now the row's state is
|
|
8
|
+
* `classifyTool(spec, facts)`, and for a tool the active profile declares the
|
|
9
|
+
* spec IS the profile's (`specForCatalogTool`), so the two cannot differ.
|
|
10
|
+
*
|
|
11
|
+
* The same goes for the COMMANDS: a declared tool's row builds them with
|
|
12
|
+
* `binCommandForState`, the rule `magus update`'s apply path uses, so the
|
|
13
|
+
* row's target and the row's key can never aim at different versions.
|
|
14
|
+
*
|
|
15
|
+
* Pure: no IO, no React. The screen gathers facts; this only assembles.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { CliTool } from "../../data/cli-tools.js";
|
|
19
|
+
import {
|
|
20
|
+
cliToolInstall,
|
|
21
|
+
cliToolUpdate,
|
|
22
|
+
} from "../../services/cli-tool-commands.js";
|
|
23
|
+
import {
|
|
24
|
+
type InstallInfo,
|
|
25
|
+
type ToolFacts,
|
|
26
|
+
type ToolStatus,
|
|
27
|
+
classifyTool,
|
|
28
|
+
} from "../../services/tool-probe.js";
|
|
29
|
+
import { binCommandForState } from "../../services/toolchain.js";
|
|
30
|
+
import type { BinRequirement, ResolvedBin } from "../../types/index.js";
|
|
31
|
+
import type { Command } from "../../utils/command-utils.js";
|
|
32
|
+
import type { StatusTone } from "../renderers/pluginStatusText.js";
|
|
33
|
+
|
|
34
|
+
export interface CliToolRow {
|
|
35
|
+
tool: CliTool;
|
|
36
|
+
/** The requirement the tool was probed through (closure → detected → catalogue). */
|
|
37
|
+
spec: BinRequirement;
|
|
38
|
+
/** True until facts arrive. */
|
|
39
|
+
checking: boolean;
|
|
40
|
+
/** Null while checking. */
|
|
41
|
+
status: ToolStatus | null;
|
|
42
|
+
install?: InstallInfo;
|
|
43
|
+
/**
|
|
44
|
+
* What Enter runs for an installed tool. For a tool the active profile
|
|
45
|
+
* declares this is the SAME command `magus update` would run for the
|
|
46
|
+
* same row (pin included); otherwise the DETECTED method's updater, so it
|
|
47
|
+
* names the installer the row was probed through.
|
|
48
|
+
*/
|
|
49
|
+
updateCommand?: Command;
|
|
50
|
+
/** What Enter runs for a tool that is not installed. */
|
|
51
|
+
installCommand: Command;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Did this spec come from the resolver's closure — i.e. does the active profile
|
|
56
|
+
* declare this tool?
|
|
57
|
+
*
|
|
58
|
+
* `ResolvedBin` is `BinRequirement` plus `sources`, and only the resolver adds
|
|
59
|
+
* them, so the provenance travels with the spec itself rather than being
|
|
60
|
+
* re-derived here (which would be a second copy of `specForCatalogTool`'s
|
|
61
|
+
* branch 1, free to disagree with it).
|
|
62
|
+
*/
|
|
63
|
+
function declaredSpec(spec: BinRequirement): ResolvedBin | null {
|
|
64
|
+
const candidate = spec as ResolvedBin;
|
|
65
|
+
return Array.isArray(candidate.sources) ? candidate : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildToolRow(
|
|
69
|
+
tool: CliTool,
|
|
70
|
+
spec: BinRequirement,
|
|
71
|
+
facts: ToolFacts | undefined,
|
|
72
|
+
install?: InstallInfo,
|
|
73
|
+
): CliToolRow {
|
|
74
|
+
const declared = declaredSpec(spec);
|
|
75
|
+
// A declared tool installs at its pin, the way `magus update` does; the
|
|
76
|
+
// catalogue entry has no pin to honour.
|
|
77
|
+
const installCommand =
|
|
78
|
+
(declared && binCommandForState("install", null, declared)) ||
|
|
79
|
+
cliToolInstall(tool);
|
|
80
|
+
const base = { tool, spec, installCommand, ...(install ? { install } : {}) };
|
|
81
|
+
if (!facts) return { ...base, checking: true, status: null };
|
|
82
|
+
const status = classifyTool(spec, facts);
|
|
83
|
+
const updateCommand = updateCommandFor(tool, declared, status, install);
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
checking: false,
|
|
87
|
+
status,
|
|
88
|
+
...(updateCommand ? { updateCommand } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The command the row's key runs, from the same rule as the row's state.
|
|
94
|
+
*
|
|
95
|
+
* A declared tool goes through `binCommandForState` — the CLI apply path's own
|
|
96
|
+
* builder over the PROFILE's requirement — so a row whose target is the pin
|
|
97
|
+
* installs the pin. Building `@latest` here (what this used to do, always) let
|
|
98
|
+
* the TUI overshoot a pin the CLI hits exactly, and the tool then classified
|
|
99
|
+
* `unknown / ahead-of-pin` for good: magus never downgrades.
|
|
100
|
+
*
|
|
101
|
+
* An undeclared catalogue tool keeps the screen's own rule, "follow how it IS
|
|
102
|
+
* installed": there is no pin to honour, and `pnpm`/`yarn` have no
|
|
103
|
+
* `BinInstaller` counterpart to hand the shared builder.
|
|
104
|
+
*/
|
|
105
|
+
function updateCommandFor(
|
|
106
|
+
tool: CliTool,
|
|
107
|
+
declared: ResolvedBin | null,
|
|
108
|
+
status: ToolStatus,
|
|
109
|
+
install: InstallInfo | undefined,
|
|
110
|
+
): Command | undefined {
|
|
111
|
+
if (!isInstalled(status)) return undefined;
|
|
112
|
+
if (declared) {
|
|
113
|
+
return binCommandForState("upgrade", status.target, declared) ?? undefined;
|
|
114
|
+
}
|
|
115
|
+
if (!install) return undefined;
|
|
116
|
+
return cliToolUpdate(tool, install.primary, install.brewFormula) ?? undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** On PATH, as far as the probe could tell. */
|
|
120
|
+
export function isInstalled(status: ToolStatus | null): boolean {
|
|
121
|
+
return status !== null && status.state !== "install";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A version for display, with exactly one leading "v". */
|
|
125
|
+
export function displayVersion(v: string): string {
|
|
126
|
+
return `v${v.replace(/^v/i, "")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface CliToolStatusText {
|
|
130
|
+
/** Text after the version, with its leading space; "" for nothing to say. */
|
|
131
|
+
suffix: string;
|
|
132
|
+
tone: StatusTone;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The row's status words. An `unknown` shows its note verbatim — the same
|
|
137
|
+
* string `magus update` prints for that tool.
|
|
138
|
+
*/
|
|
139
|
+
export function cliToolStatusText(row: CliToolRow): CliToolStatusText {
|
|
140
|
+
const { status } = row;
|
|
141
|
+
if (row.checking || !status) return { suffix: " ...", tone: "muted" };
|
|
142
|
+
switch (status.state) {
|
|
143
|
+
case "unknown":
|
|
144
|
+
return {
|
|
145
|
+
suffix: ` ? ${status.note ?? "cannot tell whether it is current"}`,
|
|
146
|
+
tone: "warning",
|
|
147
|
+
};
|
|
148
|
+
case "upgrade":
|
|
149
|
+
return {
|
|
150
|
+
suffix: status.target
|
|
151
|
+
? ` → ${displayVersion(status.target)}`
|
|
152
|
+
: " update available",
|
|
153
|
+
tone: "warning",
|
|
154
|
+
};
|
|
155
|
+
case "install":
|
|
156
|
+
case "current":
|
|
157
|
+
return { suffix: "", tone: "muted" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Header counts, from the rows' states. */
|
|
162
|
+
export function countToolRows(rows: readonly CliToolRow[]): {
|
|
163
|
+
installed: number;
|
|
164
|
+
updates: number;
|
|
165
|
+
unverified: number;
|
|
166
|
+
} {
|
|
167
|
+
let installed = 0;
|
|
168
|
+
let updates = 0;
|
|
169
|
+
let unverified = 0;
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
if (isInstalled(row.status)) installed++;
|
|
172
|
+
if (row.status?.state === "upgrade") updates++;
|
|
173
|
+
if (row.status?.state === "unknown") unverified++;
|
|
174
|
+
}
|
|
175
|
+
return { installed, updates, unverified };
|
|
176
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILT_IN_PRESETS,
|
|
3
|
+
DEFAULT_PRESET,
|
|
4
|
+
presetLabel,
|
|
5
|
+
} from "../../data/models-presets.js";
|
|
6
|
+
import {
|
|
7
|
+
GRADES,
|
|
8
|
+
type ModelsConfig,
|
|
9
|
+
type ModelsState,
|
|
10
|
+
type ModelsStatus,
|
|
11
|
+
sameRouting,
|
|
12
|
+
} from "../../services/models-core.js";
|
|
13
|
+
|
|
14
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The one header row: what is routed right now.
|
|
18
|
+
*
|
|
19
|
+
* Not selectable — the cursor skips it, the same way the Styles screen skips a
|
|
20
|
+
* category label. It carries only what the ROW draws; the drift lines and
|
|
21
|
+
* warnings behind the counts live on the `ModelsStatus` the detail renderer is
|
|
22
|
+
* handed, so they are not stored twice and cannot disagree.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* What the row for a diverged config is called.
|
|
26
|
+
*
|
|
27
|
+
* A fixed word, never the file's `preset` string: that string names whatever was last
|
|
28
|
+
* applied, and a config edited afterwards would wear a built-in's name while not being it.
|
|
29
|
+
*/
|
|
30
|
+
export const CUSTOM_LABEL = "Custom";
|
|
31
|
+
|
|
32
|
+
export interface ModelsStatusItem {
|
|
33
|
+
id: string;
|
|
34
|
+
kind: "status";
|
|
35
|
+
label: string;
|
|
36
|
+
state: ModelsState;
|
|
37
|
+
/** The preset the project's config names, or null when there is none. */
|
|
38
|
+
preset: string | null;
|
|
39
|
+
driftCount: number;
|
|
40
|
+
warningCount: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ModelsPresetItem {
|
|
44
|
+
id: string;
|
|
45
|
+
kind: "preset";
|
|
46
|
+
label: string;
|
|
47
|
+
config: ModelsConfig;
|
|
48
|
+
/** The routing this project's `models.json` names. */
|
|
49
|
+
active: boolean;
|
|
50
|
+
/** What `magus models use` picks when nothing else is chosen. */
|
|
51
|
+
isDefault: boolean;
|
|
52
|
+
/** Not one of the built-ins — this project's own, hand-edited config. */
|
|
53
|
+
custom: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ModelsBrowserItem = ModelsStatusItem | ModelsPresetItem;
|
|
57
|
+
|
|
58
|
+
// ─── Adapter ──────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export interface BuildModelsItemsArgs {
|
|
61
|
+
/** What `readModelsStatus` reported, or null before the first read lands. */
|
|
62
|
+
status: ModelsStatus | null;
|
|
63
|
+
/** The validated config, or null when there is none (or it failed validation). */
|
|
64
|
+
config: ModelsConfig | null;
|
|
65
|
+
query: string;
|
|
66
|
+
/**
|
|
67
|
+
* Presets to offer. Defaults to the shipped set, which is a compile-time
|
|
68
|
+
* constant — so building the list still costs no I/O whatsoever.
|
|
69
|
+
*/
|
|
70
|
+
presets?: ModelsConfig[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Does `config` answer to the filter?
|
|
75
|
+
*
|
|
76
|
+
* Matched over the preset name AND the models it names, because "sonnet" is the
|
|
77
|
+
* thing a user actually looks for — the preset names encode which model leads,
|
|
78
|
+
* not which models appear.
|
|
79
|
+
*/
|
|
80
|
+
function matches(config: ModelsConfig, lowerQuery: string): boolean {
|
|
81
|
+
if (!lowerQuery) return true;
|
|
82
|
+
// Both names are searchable: the row SHOWS "Opus with Fable help", and a user who knows
|
|
83
|
+
// the command types "fable-advisor". Matching only one of them makes the other look like
|
|
84
|
+
// a missing preset.
|
|
85
|
+
//
|
|
86
|
+
// Bound models are searchable too: a project that routes `smart` to `mate1` → `grok-4.6`
|
|
87
|
+
// is findable by typing `grok`, which is the name its author thinks in. The slot names
|
|
88
|
+
// come along with the grade models above, so both halves of the binding match.
|
|
89
|
+
const haystack = [
|
|
90
|
+
config.preset,
|
|
91
|
+
presetLabel(config.preset),
|
|
92
|
+
config.main.model,
|
|
93
|
+
...GRADES.map((grade) => config.grades[grade].model),
|
|
94
|
+
...Object.values(config.mates ?? {}).map((binding) => binding.model),
|
|
95
|
+
]
|
|
96
|
+
.join(" ")
|
|
97
|
+
.toLowerCase();
|
|
98
|
+
return haystack.includes(lowerQuery);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the flat list for the Models screen.
|
|
103
|
+
*
|
|
104
|
+
* One status header, then one row per preset in the order they ship. Nothing is
|
|
105
|
+
* grouped: there are four of them and a grade table each, so sections would cost
|
|
106
|
+
* rows and buy no navigation.
|
|
107
|
+
*
|
|
108
|
+
* The header is emitted only when at least one preset row survives the filter. A
|
|
109
|
+
* header with nothing under it reads as a result, and would leave the empty
|
|
110
|
+
* state — the one thing that says how to clear the filter — unreachable.
|
|
111
|
+
*/
|
|
112
|
+
export function buildModelsItems({
|
|
113
|
+
status,
|
|
114
|
+
config,
|
|
115
|
+
query,
|
|
116
|
+
presets = BUILT_IN_PRESETS,
|
|
117
|
+
}: BuildModelsItemsArgs): ModelsBrowserItem[] {
|
|
118
|
+
if (!status) return [];
|
|
119
|
+
|
|
120
|
+
const lowerQuery = query.trim().toLowerCase();
|
|
121
|
+
const activeName = config?.preset ?? null;
|
|
122
|
+
|
|
123
|
+
// DOES THE PROJECT'S OWN CONFIG SAY SOMETHING NO BUILT-IN DOES?
|
|
124
|
+
//
|
|
125
|
+
// The test used to be "does it name a preset the built-ins do not", and that missed the
|
|
126
|
+
// ordinary case entirely. `magus models use opus-lead` writes `preset: "opus-lead"`,
|
|
127
|
+
// and hand-editing the file afterwards — adding a `mates` block, rebinding an agent —
|
|
128
|
+
// leaves that name untouched. So the config carrying the mate assignments matched a
|
|
129
|
+
// built-in by NAME, got no row of its own, and the list rendered the BUILT-IN's contents
|
|
130
|
+
// under the built-in's label.
|
|
131
|
+
//
|
|
132
|
+
// The consequence was not cosmetic: the only config on the machine that can name a mate
|
|
133
|
+
// was unreachable on screen, so neither the agent table nor the workflow bar could ever
|
|
134
|
+
// show one, no matter what the file said. `magus models list` printed it correctly the
|
|
135
|
+
// whole time, which is what made the gap look like a rendering bug rather than a missing
|
|
136
|
+
// row.
|
|
137
|
+
//
|
|
138
|
+
// Compared by CONTENT now, so a project that merely applied a preset still gets one row
|
|
139
|
+
// (its file and the built-in agree) and a project that changed anything gets two.
|
|
140
|
+
const builtInNamed = presets.find((preset) => preset.preset === activeName);
|
|
141
|
+
const projectOwn =
|
|
142
|
+
config && (!builtInNamed || !sameRouting(config, builtInNamed))
|
|
143
|
+
? config
|
|
144
|
+
: null;
|
|
145
|
+
|
|
146
|
+
const rows: ModelsPresetItem[] = presets
|
|
147
|
+
.filter((preset) => matches(preset, lowerQuery))
|
|
148
|
+
.map((preset) => ({
|
|
149
|
+
id: `preset:${preset.preset}`,
|
|
150
|
+
kind: "preset" as const,
|
|
151
|
+
label: presetLabel(preset.preset),
|
|
152
|
+
config: preset,
|
|
153
|
+
// `●` marks WHAT IS RUNNING, and when the project has diverged the built-in of
|
|
154
|
+
// that name is not it. Leaving the mark on both rows would say two different
|
|
155
|
+
// routings are simultaneously in force.
|
|
156
|
+
active: preset.preset === activeName && projectOwn === null,
|
|
157
|
+
isDefault: preset.preset === DEFAULT_PRESET,
|
|
158
|
+
custom: false,
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
// A project whose settings differ from every built-in gets ONE row, called `Custom`.
|
|
162
|
+
//
|
|
163
|
+
// Divergence is a THING YOU HAVE, not a fault to warn about. A project can legitimately
|
|
164
|
+
// run models or efforts no preset ships, and the list's job is to show that as one more
|
|
165
|
+
// option carrying those settings — selectable, inspectable, with its own tier table and
|
|
166
|
+
// chart — rather than as a badge saying the config and the settings disagree.
|
|
167
|
+
//
|
|
168
|
+
// It is called `Custom` and never the string in the file's `preset` field. That string is
|
|
169
|
+
// whatever was last applied — `models use opus-lead` writes `opus-lead` and hand-editing
|
|
170
|
+
// afterwards leaves it — so showing it puts a built-in's name on a row that is not that
|
|
171
|
+
// built-in.
|
|
172
|
+
if (projectOwn && matches(projectOwn, lowerQuery)) {
|
|
173
|
+
rows.push({
|
|
174
|
+
// `project:` rather than `preset:`, because the name it carries may be a built-in's
|
|
175
|
+
// and two rows may not share an id.
|
|
176
|
+
id: `project:${projectOwn.preset}`,
|
|
177
|
+
kind: "preset",
|
|
178
|
+
label: CUSTOM_LABEL,
|
|
179
|
+
config: projectOwn,
|
|
180
|
+
active: true,
|
|
181
|
+
isDefault: false,
|
|
182
|
+
custom: true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (rows.length === 0) return [];
|
|
187
|
+
|
|
188
|
+
const header: ModelsStatusItem = {
|
|
189
|
+
id: "status",
|
|
190
|
+
kind: "status",
|
|
191
|
+
label: status.preset
|
|
192
|
+
? `Model tiers: ${status.state} · ${presetLabel(status.preset)}`
|
|
193
|
+
: `Model tiers: ${status.state}`,
|
|
194
|
+
state: status.state,
|
|
195
|
+
preset: status.preset,
|
|
196
|
+
driftCount: status.drift.length,
|
|
197
|
+
warningCount: status.warnings.length,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return [header, ...rows];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Index of the first row the user can actually land on, or 0 if none.
|
|
205
|
+
*
|
|
206
|
+
* The status row is a header, not a choice: every key this screen binds acts on
|
|
207
|
+
* a preset, so parking the cursor there would make `a` and Enter do nothing on
|
|
208
|
+
* the row the screen opens with.
|
|
209
|
+
*/
|
|
210
|
+
export function firstSelectableIndex(items: ModelsBrowserItem[]): number {
|
|
211
|
+
const index = items.findIndex((item) => item.kind !== "status");
|
|
212
|
+
return index >= 0 ? index : 0;
|
|
213
|
+
}
|