magus-cli 0.0.0-placeholder → 7.1.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 +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -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 +524 -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 +248 -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 +340 -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__/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 +345 -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 +310 -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 +1396 -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 +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -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 +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -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 +1564 -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 +657 -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 +245 -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 +847 -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 +377 -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 +820 -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 +535 -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 +1423 -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 +440 -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 +149 -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,717 @@
|
|
|
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 { type ReportBadgeKind, reportBadge } from "../ui/theme-mode.js";
|
|
35
|
+
import { brand } from "../ui/theme.js";
|
|
36
|
+
import { formatCommand } from "../utils/command-utils.js";
|
|
37
|
+
import {
|
|
38
|
+
badge,
|
|
39
|
+
bold,
|
|
40
|
+
dim,
|
|
41
|
+
elapsed,
|
|
42
|
+
fg,
|
|
43
|
+
meter,
|
|
44
|
+
padEnd,
|
|
45
|
+
padStart,
|
|
46
|
+
ramps,
|
|
47
|
+
spinner,
|
|
48
|
+
stackedBar,
|
|
49
|
+
sweep,
|
|
50
|
+
width,
|
|
51
|
+
} from "./ansi.js";
|
|
52
|
+
|
|
53
|
+
/** Two spaces of gutter on every line, so the report reads as one block. */
|
|
54
|
+
const PAD = " ";
|
|
55
|
+
/** Bars are this wide everywhere; a bar that changes width cannot be compared. */
|
|
56
|
+
const BAR = 18;
|
|
57
|
+
|
|
58
|
+
// -- action vocabulary -------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The single source of truth for what an action looks like.
|
|
62
|
+
*
|
|
63
|
+
* Glyph, word and hue together — defined once so a plugin `install` and a
|
|
64
|
+
* binary `install` cannot drift into two different blues with two different
|
|
65
|
+
* symbols, which is exactly what happened when each row built its own string.
|
|
66
|
+
*/
|
|
67
|
+
interface ActionStyle {
|
|
68
|
+
glyph: string;
|
|
69
|
+
label: string;
|
|
70
|
+
color: string;
|
|
71
|
+
/** Chrome rows are dimmed whole and carry no chip. */
|
|
72
|
+
chrome?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Every action any of the three planners can produce, in one union. */
|
|
76
|
+
type ActionKey =
|
|
77
|
+
| "current"
|
|
78
|
+
| "install"
|
|
79
|
+
| "update"
|
|
80
|
+
| "upgrade"
|
|
81
|
+
| "repair"
|
|
82
|
+
| "refresh"
|
|
83
|
+
| "unknown";
|
|
84
|
+
|
|
85
|
+
const ACTIONS: Record<ActionKey, ActionStyle> = {
|
|
86
|
+
current: { glyph: "=", label: "CURRENT", color: brand.muted, chrome: true },
|
|
87
|
+
install: { glyph: "+", label: "INSTALL", color: brand.link },
|
|
88
|
+
// Green, the tone magus uses for an update everywhere else. Amber stays
|
|
89
|
+
// on `unknown` below, so the two states remain distinguishable: green is
|
|
90
|
+
// "a newer version exists", amber is "we could not tell".
|
|
91
|
+
update: { glyph: "↑", label: "UPDATE", color: brand.success },
|
|
92
|
+
upgrade: { glyph: "↑", label: "UPGRADE", color: brand.success },
|
|
93
|
+
repair: { glyph: "⟳", label: "REPAIR", color: brand.info },
|
|
94
|
+
refresh: { glyph: "⟳", label: "REFRESH", color: brand.info },
|
|
95
|
+
// Warning, not danger: "cannot tell" is not a failure, and red UNKNOWN read
|
|
96
|
+
// as "broken". The summary line and the TUI already use warning for it.
|
|
97
|
+
unknown: { glyph: "?", label: "UNKNOWN", color: brand.warning },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Widest label plus the badge's own two spaces of padding. */
|
|
101
|
+
const CHIP_WIDTH =
|
|
102
|
+
Math.max(...Object.values(ACTIONS).map((a) => a.label.length)) + 2;
|
|
103
|
+
|
|
104
|
+
/** A chip for an actionable row; flat dim text for a chrome one. */
|
|
105
|
+
function chip(key: ActionKey): string {
|
|
106
|
+
const style = ACTIONS[key];
|
|
107
|
+
const text = style.chrome
|
|
108
|
+
? dim(padEnd(` ${style.label} `, CHIP_WIDTH))
|
|
109
|
+
: badge(style.label, style.color);
|
|
110
|
+
return padEnd(text, CHIP_WIDTH);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function glyph(key: ActionKey): string {
|
|
114
|
+
return fg(ACTIONS[key].color, ACTIONS[key].glyph);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Bright for a row that needs something, grey for a row that does not. */
|
|
118
|
+
function rowText(key: ActionKey, text: string): string {
|
|
119
|
+
return ACTIONS[key].chrome ? dim(text) : text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// -- header ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export function header(profileId: string): string[] {
|
|
125
|
+
return [
|
|
126
|
+
"",
|
|
127
|
+
`${PAD}${fg(brand.accent, "◆")} ${bold("magus update")} ${dim(
|
|
128
|
+
"profile",
|
|
129
|
+
)} ${fg(brand.accent, profileId)}`,
|
|
130
|
+
"",
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// -- phase checklist ---------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
export type StepState = "pending" | "running" | "done" | "warn" | "fail";
|
|
137
|
+
|
|
138
|
+
export interface Step {
|
|
139
|
+
label: string;
|
|
140
|
+
state: StepState;
|
|
141
|
+
/** Right-hand summary. While running, say what it is blocked on. */
|
|
142
|
+
detail: string;
|
|
143
|
+
startedAt?: number;
|
|
144
|
+
endedAt?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Countable work, when the step has any.
|
|
147
|
+
*
|
|
148
|
+
* Present → a determinate meter. Absent → an indeterminate sweep, which is
|
|
149
|
+
* the honest widget for a call with no denominator. Never fake a percentage
|
|
150
|
+
* for one: a bar that creeps to 90% and stops is worse than no bar.
|
|
151
|
+
*/
|
|
152
|
+
progress?: { done: number; total: number };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const STEP_MARK: Record<Exclude<StepState, "running">, [string, string]> = {
|
|
156
|
+
pending: ["·", brand.muted],
|
|
157
|
+
done: ["✓", brand.success],
|
|
158
|
+
warn: ["⚠", brand.warning],
|
|
159
|
+
fail: ["✗", brand.danger],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function stepMark(step: Step, tick: number): string {
|
|
163
|
+
if (step.state === "running") return fg(brand.accent, spinner(tick));
|
|
164
|
+
const [mark, color] = STEP_MARK[step.state];
|
|
165
|
+
return fg(color, mark);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stepDuration(step: Step, now: number): number | null {
|
|
169
|
+
if (step.startedAt === undefined) return null;
|
|
170
|
+
return (step.endedAt ?? now) - step.startedAt;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const STEP_LABEL_WIDTH = 14;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The animated frame: one row per step, spinner on whichever is running.
|
|
177
|
+
*
|
|
178
|
+
* Deliberately shows the pending steps too. A single spinner says "busy"; a
|
|
179
|
+
* four-row checklist with one spinner says "busy, on step two of four, and here
|
|
180
|
+
* is what is left" — which is the difference between a hang and progress.
|
|
181
|
+
*/
|
|
182
|
+
export function stepFrame(steps: Step[], tick: number, now: number): string[] {
|
|
183
|
+
return steps.map((step) => {
|
|
184
|
+
const ms = stepDuration(step, now);
|
|
185
|
+
const time = ms === null ? "" : dim(padStart(elapsed(ms), 6));
|
|
186
|
+
const label =
|
|
187
|
+
step.state === "pending"
|
|
188
|
+
? dim(padEnd(step.label, STEP_LABEL_WIDTH))
|
|
189
|
+
: padEnd(step.label, STEP_LABEL_WIDTH);
|
|
190
|
+
// Only the running row carries a bar. A finished row's bar belongs in the
|
|
191
|
+
// record below, where every step is on one scale and comparable; drawing
|
|
192
|
+
// it here too would put two competing bar columns on the same screen.
|
|
193
|
+
const bar =
|
|
194
|
+
step.state !== "running"
|
|
195
|
+
? " ".repeat(BAR)
|
|
196
|
+
: step.progress && step.progress.total > 0
|
|
197
|
+
? meter((step.progress.done / step.progress.total) * 100, BAR)
|
|
198
|
+
: sweep(BAR, tick, brand.accent);
|
|
199
|
+
// The bar carries the shape; this labels it. Without the count a half-full
|
|
200
|
+
// meter says "some of it", which is not the same information as "2 of 4".
|
|
201
|
+
const count =
|
|
202
|
+
step.state === "running" && step.progress && step.progress.total > 0
|
|
203
|
+
? `${fg(brand.accent, `${step.progress.done}/${step.progress.total}`)} `
|
|
204
|
+
: "";
|
|
205
|
+
const detail =
|
|
206
|
+
step.state === "running"
|
|
207
|
+
? fg(brand.accent, step.detail)
|
|
208
|
+
: dim(step.detail);
|
|
209
|
+
return `${PAD}${stepMark(step, tick)} ${label} ${bar} ${time} ${count}${detail}`;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The permanent record: one row per step, its duration, and what it found.
|
|
215
|
+
*
|
|
216
|
+
* NO BAR. Each row carried an 18-cell meter of that step's share of the
|
|
217
|
+
* slowest step — which is precisely what the right-aligned millisecond column
|
|
218
|
+
* beside it already says, so the row stated one fact twice. A shape earns its
|
|
219
|
+
* width by making something readable that the number does not; a share of a
|
|
220
|
+
* five-row set, already sorted into a column, is not that.
|
|
221
|
+
*
|
|
222
|
+
* The LIVE frame keeps its bar (`stepFrame` above) and that is not the same
|
|
223
|
+
* widget: there the bar is a progress meter or an indeterminate sweep on the
|
|
224
|
+
* step running right now, and it answers "is this working or hung?" — a
|
|
225
|
+
* question no printed number can answer.
|
|
226
|
+
*/
|
|
227
|
+
export function stepRecord(steps: Step[], now: number): string[] {
|
|
228
|
+
return steps.map((step) => {
|
|
229
|
+
const label = padEnd(step.label, STEP_LABEL_WIDTH);
|
|
230
|
+
const time = dim(padStart(elapsed(stepDuration(step, now) ?? 0), 6));
|
|
231
|
+
return `${PAD}${stepMark(step, 0)} ${label} ${time} ${dim(step.detail)}`;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// -- the plan ----------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
function pluginAction(item: PluginUpdateItem): ActionKey {
|
|
238
|
+
return item.action;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** What the row says on its right: versions, never the action word again. */
|
|
242
|
+
function pluginDetail(item: PluginUpdateItem): string {
|
|
243
|
+
// A note the row's own badge already states is noise, not information. The
|
|
244
|
+
// DEPRECATED chip and the heading above it both say "no configured
|
|
245
|
+
// marketplace offers this", so printing the sentence again put the longest
|
|
246
|
+
// string in the table on the least interesting rows.
|
|
247
|
+
//
|
|
248
|
+
// The note stays on the item. The TUI has no heading to lean on and renders
|
|
249
|
+
// it as that row's only explanation — this is a rendering decision about a
|
|
250
|
+
// table that carries the fact twice, not a change to what is known.
|
|
251
|
+
const note =
|
|
252
|
+
item.note && item.reason !== "not-offered"
|
|
253
|
+
? ` ${dim(`(${item.note})`)}`
|
|
254
|
+
: "";
|
|
255
|
+
switch (item.action) {
|
|
256
|
+
case "install":
|
|
257
|
+
return `${item.target ?? "latest"}${note}`;
|
|
258
|
+
case "update":
|
|
259
|
+
// The GROUP's page tone, not a literal and not ACTIONS: the arrow and
|
|
260
|
+
// the target sit in the same row as that group's glyph and chip, so
|
|
261
|
+
// they must resolve per page from the same place. A hardcoded tone
|
|
262
|
+
// here is how the row ended up half green and half amber.
|
|
263
|
+
return `${dim(item.installed ?? "—")} ${fg(
|
|
264
|
+
groupStyle("Update").text,
|
|
265
|
+
"→",
|
|
266
|
+
)} ${fg(groupStyle("Update").text, item.target ?? "?")}${note}`;
|
|
267
|
+
case "repair":
|
|
268
|
+
case "unknown":
|
|
269
|
+
return `${item.installed ?? "—"}${note}`;
|
|
270
|
+
case "current":
|
|
271
|
+
return `${item.installed ?? "—"}${note}`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* What a tool row says on its right.
|
|
277
|
+
*
|
|
278
|
+
* A `current` row used to print `pinned —` unconditionally, which was true only
|
|
279
|
+
* while "current" implied "pinned". An unpinned tool can be current now, so the
|
|
280
|
+
* pin is shown only when there is one.
|
|
281
|
+
*/
|
|
282
|
+
function binDetail(item: BinUpdateItem, plan: UpdatePlan): string {
|
|
283
|
+
const spec = plan.binSpecs.get(item.name);
|
|
284
|
+
// Rendered for the reader's eyes only — this row says what WILL run, while
|
|
285
|
+
// applyBins runs the argv itself. Same builder, so they cannot differ.
|
|
286
|
+
const cmd = spec ? binCommandFor(item, spec) : null;
|
|
287
|
+
const command = cmd ? dim(formatCommand(cmd)) : "";
|
|
288
|
+
switch (item.action) {
|
|
289
|
+
case "current":
|
|
290
|
+
return `${item.installed ?? "—"}${
|
|
291
|
+
item.version ? ` ${dim(`pinned ${item.version}`)}` : ""
|
|
292
|
+
}`;
|
|
293
|
+
case "upgrade":
|
|
294
|
+
return `${dim(item.installed ?? "—")} ${fg(
|
|
295
|
+
ACTIONS.upgrade.color,
|
|
296
|
+
"→",
|
|
297
|
+
)} ${fg(
|
|
298
|
+
ACTIONS.upgrade.color,
|
|
299
|
+
item.target ?? "?",
|
|
300
|
+
)}${command ? ` ${command}` : ""}${
|
|
301
|
+
// An upgrade that also changes the tool's owner says so here; a
|
|
302
|
+
// plain one reads identically to before.
|
|
303
|
+
item.adopted ? ` ${dim(`(${item.adopted})`)}` : ""
|
|
304
|
+
}`;
|
|
305
|
+
case "unknown":
|
|
306
|
+
return `${item.installed ?? "—"}${
|
|
307
|
+
item.note ? ` ${dim(`(${item.note})`)}` : ""
|
|
308
|
+
}`;
|
|
309
|
+
case "install":
|
|
310
|
+
return command;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A section: a stacked bar of the action mix, then one row per item.
|
|
316
|
+
*
|
|
317
|
+
* The bar is the section's whole story in one line — a reader who takes in
|
|
318
|
+
* nothing else knows whether this section is mostly settled or mostly work.
|
|
319
|
+
*/
|
|
320
|
+
/**
|
|
321
|
+
* Which heading a plugin row sits under.
|
|
322
|
+
*
|
|
323
|
+
* Read off the plan's machine codes — `action` and `reason` — never the human
|
|
324
|
+
* note. Five headings, and no more:
|
|
325
|
+
*
|
|
326
|
+
* Update something an apply would move forward
|
|
327
|
+
* Deprecated no configured marketplace offers it
|
|
328
|
+
* Fix required declared or enabled, with nothing on disk
|
|
329
|
+
* Unverified the check itself did not complete
|
|
330
|
+
* Current settled — the version was compared, or there was none to compare
|
|
331
|
+
*
|
|
332
|
+
* `Current` is last on purpose: it is the biggest group and the least
|
|
333
|
+
* interesting, and leading with it buries the few rows that need a decision.
|
|
334
|
+
*
|
|
335
|
+
* UNVERIFIED IS NOT CURRENT, and the two must not share a heading. A row whose
|
|
336
|
+
* catalog read failed used to land under `Current` carrying a `?` glyph and a
|
|
337
|
+
* note saying the catalog was unreachable — a row that contradicted the heading
|
|
338
|
+
* above it, while the closing notices insisted those rows are NOT reported as up
|
|
339
|
+
* to date. That is a different claim from "nothing could be compared, so we
|
|
340
|
+
* assume the install is the latest", which IS `current` and reaches it in the
|
|
341
|
+
* classifier, not here.
|
|
342
|
+
*
|
|
343
|
+
* "Unverified" is the word `summaryLine` already prints for this count; it is
|
|
344
|
+
* this report's existing vocabulary, not a new one.
|
|
345
|
+
*/
|
|
346
|
+
function pluginGroup(item: PluginUpdateItem): string {
|
|
347
|
+
if (item.action === "update" || item.action === "repair") return "Update";
|
|
348
|
+
if (item.action === "install") return "Fix required";
|
|
349
|
+
if (item.action === "unknown") {
|
|
350
|
+
return item.reason === "not-offered" ? "Deprecated" : "Unverified";
|
|
351
|
+
}
|
|
352
|
+
return "Current";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const GROUP_ORDER = [
|
|
356
|
+
"Update",
|
|
357
|
+
"Deprecated",
|
|
358
|
+
"Fix required",
|
|
359
|
+
"Unverified",
|
|
360
|
+
"Current",
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Colour and emphasis per GROUP.
|
|
365
|
+
*
|
|
366
|
+
* "Dim the chrome, saturate the signal" is this file's own rule, stated at the
|
|
367
|
+
* top; the signal is the groups an apply would act on. Every colour comes from
|
|
368
|
+
* `brand`, which ui/theme.ts contrast-checks against BOTH a cream and a
|
|
369
|
+
* near-black terminal, so a light theme needs no second palette.
|
|
370
|
+
*/
|
|
371
|
+
const GROUP_KIND: Record<string, ReportBadgeKind> = {
|
|
372
|
+
Update: "update",
|
|
373
|
+
Deprecated: "deprecated",
|
|
374
|
+
"Fix required": "fix",
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
interface GroupStyle {
|
|
378
|
+
/** Heading and glyph, drawn on the page. */
|
|
379
|
+
text: string;
|
|
380
|
+
/** Badge fill and its ink. Absent on a chrome group, which carries no badge. */
|
|
381
|
+
fill?: string;
|
|
382
|
+
ink?: string;
|
|
383
|
+
chrome?: boolean;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A group's colours, for the page this run resolved.
|
|
388
|
+
*
|
|
389
|
+
* `reportBadge` answers per page — pale fill with deep ink on cream, saturated
|
|
390
|
+
* fill with pale ink on near-black — so this is the one place the report stops
|
|
391
|
+
* using a single set of hues for both. `brand` is still right for everything
|
|
392
|
+
* else here: those tokens are chosen to clear 3:1 as INK on either page, which
|
|
393
|
+
* is a different question from what a filled chip should look like.
|
|
394
|
+
*/
|
|
395
|
+
function groupStyle(group: string): GroupStyle {
|
|
396
|
+
const kind = GROUP_KIND[group];
|
|
397
|
+
if (!kind) return { text: brand.muted, chrome: true };
|
|
398
|
+
const ink = reportBadge(kind);
|
|
399
|
+
return { text: ink.text, fill: ink.fill, ink: ink.ink };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** The badge's word: the heading, uppercased. Never a second vocabulary. */
|
|
403
|
+
function groupBadge(group: string): string {
|
|
404
|
+
return group.toUpperCase();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Badge column width, from the labels THIS report shows — a run with nothing
|
|
409
|
+
* deprecated does not pay for the longest word. `width` ignores escape bytes,
|
|
410
|
+
* so measuring an already painted badge is safe.
|
|
411
|
+
*/
|
|
412
|
+
function badgeWidth(items: PluginUpdateItem[]): number {
|
|
413
|
+
return Math.max(
|
|
414
|
+
0,
|
|
415
|
+
...items
|
|
416
|
+
.filter((item) => !groupStyle(pluginGroup(item)).chrome)
|
|
417
|
+
.map((item) => {
|
|
418
|
+
const style = groupStyle(pluginGroup(item));
|
|
419
|
+
return (
|
|
420
|
+
width(
|
|
421
|
+
badge(
|
|
422
|
+
groupBadge(pluginGroup(item)),
|
|
423
|
+
style.fill ?? brand.muted,
|
|
424
|
+
style.ink,
|
|
425
|
+
),
|
|
426
|
+
) + 2
|
|
427
|
+
);
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function pluginRow(
|
|
433
|
+
item: PluginUpdateItem,
|
|
434
|
+
nameWidth: number,
|
|
435
|
+
chipWidth: number,
|
|
436
|
+
): string {
|
|
437
|
+
const group = pluginGroup(item);
|
|
438
|
+
const style = groupStyle(group);
|
|
439
|
+
const name = padEnd(item.pluginId, nameWidth);
|
|
440
|
+
// Only an actionable row is badged; a chrome row gets the same width in
|
|
441
|
+
// spaces, so every version still starts in one column.
|
|
442
|
+
const chip = padEnd(
|
|
443
|
+
style.chrome
|
|
444
|
+
? ""
|
|
445
|
+
: badge(groupBadge(group), style.fill ?? brand.muted, style.ink),
|
|
446
|
+
chipWidth,
|
|
447
|
+
);
|
|
448
|
+
return `${PAD}${fg(style.text, ACTIONS[item.action].glyph)} ${
|
|
449
|
+
style.chrome ? dim(name) : name
|
|
450
|
+
} ${chip}${pluginDetail(item)}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The Plugins section: grouped by state, `Current` last. */
|
|
454
|
+
function pluginSection(items: PluginUpdateItem[], nameWidth: number): string[] {
|
|
455
|
+
if (items.length === 0) return [];
|
|
456
|
+
|
|
457
|
+
const byGroup = new Map<string, PluginUpdateItem[]>();
|
|
458
|
+
for (const item of items) {
|
|
459
|
+
const group = pluginGroup(item);
|
|
460
|
+
const list = byGroup.get(group);
|
|
461
|
+
if (list) list.push(item);
|
|
462
|
+
else byGroup.set(group, [item]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// GROUP_ORDER first, then anything it does not name. A report may never
|
|
466
|
+
// silently drop a row: driving the output purely from a fixed list would
|
|
467
|
+
// render an unexpected group as nothing at all while the header still
|
|
468
|
+
// counted it.
|
|
469
|
+
const ordered = [
|
|
470
|
+
...GROUP_ORDER.filter((g) => byGroup.has(g)),
|
|
471
|
+
...[...byGroup.keys()].filter((g) => !GROUP_ORDER.includes(g)),
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const chipWidth = badgeWidth(items);
|
|
475
|
+
// No bar: it stacked the mix into one strip above groups that already print
|
|
476
|
+
// their own counts, in a form nobody can read a number off.
|
|
477
|
+
const lines: string[] = [
|
|
478
|
+
"",
|
|
479
|
+
`${PAD}${bold("Plugins")} ${dim(String(items.length))}`,
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
for (const group of ordered) {
|
|
483
|
+
const rows = byGroup.get(group);
|
|
484
|
+
if (!rows || rows.length === 0) continue;
|
|
485
|
+
lines.push(
|
|
486
|
+
"",
|
|
487
|
+
`${PAD}${bold(fg(groupStyle(group).text, group))} ${dim(
|
|
488
|
+
String(rows.length),
|
|
489
|
+
)}`,
|
|
490
|
+
);
|
|
491
|
+
for (const row of rows) lines.push(pluginRow(row, nameWidth, chipWidth));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return lines;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function section(
|
|
498
|
+
title: string,
|
|
499
|
+
rows: Array<{ action: ActionKey; name: string; detail: string }>,
|
|
500
|
+
nameWidth: number,
|
|
501
|
+
): string[] {
|
|
502
|
+
if (rows.length === 0) return [];
|
|
503
|
+
const counts = new Map<ActionKey, number>();
|
|
504
|
+
for (const row of rows)
|
|
505
|
+
counts.set(row.action, (counts.get(row.action) ?? 0) + 1);
|
|
506
|
+
const legend = [...counts]
|
|
507
|
+
.map(([action, n]) =>
|
|
508
|
+
fg(ACTIONS[action].color, `${n} ${ACTIONS[action].label.toLowerCase()}`),
|
|
509
|
+
)
|
|
510
|
+
.join(dim(" · "));
|
|
511
|
+
|
|
512
|
+
return [
|
|
513
|
+
"",
|
|
514
|
+
`${PAD}${bold(padEnd(title, 9))} ${dim(
|
|
515
|
+
padStart(String(rows.length), 3),
|
|
516
|
+
)} ${legend}`,
|
|
517
|
+
"",
|
|
518
|
+
...rows.map(
|
|
519
|
+
(row) =>
|
|
520
|
+
`${PAD}${glyph(row.action)} ${rowText(
|
|
521
|
+
row.action,
|
|
522
|
+
padEnd(row.name, nameWidth),
|
|
523
|
+
)} ${chip(row.action)} ${row.detail}`,
|
|
524
|
+
),
|
|
525
|
+
];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function planReport(plan: UpdatePlan): string[] {
|
|
529
|
+
const nameWidth = Math.max(
|
|
530
|
+
12,
|
|
531
|
+
...plan.plugins.map((p) => width(p.pluginId)),
|
|
532
|
+
...plan.bins.map((b) => width(b.name)),
|
|
533
|
+
...plan.skills.map((s) => width(s.name)),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// EVERY planned plugin gets a row. There is no filter here and there must
|
|
537
|
+
// not be one.
|
|
538
|
+
//
|
|
539
|
+
// A previous revision dropped `unknown` + `not-offered` + not-installed rows
|
|
540
|
+
// — a pin naming a plugin no marketplace offers — on the grounds that a
|
|
541
|
+
// notice named them instead. That notice did not exist: the function the
|
|
542
|
+
// comment cited was never written. The row appeared in no table and no
|
|
543
|
+
// notice, while the closing lines still counted it and said "Reasons are
|
|
544
|
+
// shown above", pointing at nothing.
|
|
545
|
+
//
|
|
546
|
+
// A report may never silently drop a row. That is the whole defect this
|
|
547
|
+
// file's grouping was written to end, and a filter here reintroduces it in a
|
|
548
|
+
// form that is harder to see, because the header count agrees with the rows
|
|
549
|
+
// and only the plan knows something is missing.
|
|
550
|
+
return [
|
|
551
|
+
...pluginSection(plan.plugins, nameWidth),
|
|
552
|
+
...section(
|
|
553
|
+
"CLI",
|
|
554
|
+
plan.bins.map((item: BinUpdateItem) => ({
|
|
555
|
+
action: item.action as ActionKey,
|
|
556
|
+
name: item.name,
|
|
557
|
+
detail: binDetail(item, plan),
|
|
558
|
+
})),
|
|
559
|
+
nameWidth,
|
|
560
|
+
),
|
|
561
|
+
...section(
|
|
562
|
+
"Skills",
|
|
563
|
+
plan.skills.map((item: SkillUpdateItem) => ({
|
|
564
|
+
action: item.action as ActionKey,
|
|
565
|
+
name: item.name,
|
|
566
|
+
detail: dim(`${item.ref.repo}/${item.ref.path}`),
|
|
567
|
+
})),
|
|
568
|
+
nameWidth,
|
|
569
|
+
),
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// -- apply -------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
export interface ApplyState {
|
|
576
|
+
/** Items finished, of `total`. */
|
|
577
|
+
done: number;
|
|
578
|
+
total: number;
|
|
579
|
+
/** What is being worked on right now. */
|
|
580
|
+
current: string;
|
|
581
|
+
startedAt: number;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The animated apply frame: a gradient meter that turns green as it fills.
|
|
586
|
+
*
|
|
587
|
+
* The meter's ramp encodes POSITION along the fill, which is what a smooth
|
|
588
|
+
* gradient is for — the count beside it is the label, not the signal.
|
|
589
|
+
*/
|
|
590
|
+
export function applyFrame(
|
|
591
|
+
state: ApplyState,
|
|
592
|
+
tick: number,
|
|
593
|
+
now: number,
|
|
594
|
+
): string[] {
|
|
595
|
+
const pct = state.total === 0 ? 100 : (state.done / state.total) * 100;
|
|
596
|
+
const count = `${state.done}/${state.total}`;
|
|
597
|
+
return [
|
|
598
|
+
"",
|
|
599
|
+
`${PAD}${fg(brand.accent, spinner(tick))} ${meter(pct, BAR * 2)} ${padStart(
|
|
600
|
+
count,
|
|
601
|
+
7,
|
|
602
|
+
)} ${fg(brand.accent, state.current)} ${dim(
|
|
603
|
+
elapsed(now - state.startedAt),
|
|
604
|
+
)}`,
|
|
605
|
+
];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** One finished item, written permanently above the meter. */
|
|
609
|
+
export function applyRow(
|
|
610
|
+
ok: boolean,
|
|
611
|
+
name: string,
|
|
612
|
+
nameWidth: number,
|
|
613
|
+
detail: string,
|
|
614
|
+
ms: number,
|
|
615
|
+
): string {
|
|
616
|
+
const mark = ok ? fg(brand.success, "✓") : fg(brand.danger, "✗");
|
|
617
|
+
return `${PAD}${mark} ${padEnd(name, nameWidth)} ${detail} ${dim(
|
|
618
|
+
padStart(elapsed(ms), 7),
|
|
619
|
+
)}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Closing line: the run's shape as a bar, then the counts that label it.
|
|
624
|
+
*
|
|
625
|
+
* `unchanged` is its own count: an installer that exited 0 while read-back
|
|
626
|
+
* shows nothing moved. Folding it into `changed` is how every run printed
|
|
627
|
+
* "3 changed" while changing nothing; it is not a failure either.
|
|
628
|
+
*/
|
|
629
|
+
export function summaryLine(
|
|
630
|
+
changed: number,
|
|
631
|
+
current: number,
|
|
632
|
+
unknown: number,
|
|
633
|
+
failed: number,
|
|
634
|
+
ms: number,
|
|
635
|
+
unchanged = 0,
|
|
636
|
+
): string[] {
|
|
637
|
+
const bar = stackedBar(BAR * 2, [
|
|
638
|
+
{ value: changed, color: brand.success },
|
|
639
|
+
{ value: current, color: brand.muted },
|
|
640
|
+
{ value: unchanged + unknown, color: brand.warning },
|
|
641
|
+
{ value: failed, color: brand.danger },
|
|
642
|
+
]);
|
|
643
|
+
const parts = [
|
|
644
|
+
changed > 0 ? fg(brand.success, `${changed} changed`) : "",
|
|
645
|
+
unchanged > 0 ? fg(brand.warning, `${unchanged} unchanged`) : "",
|
|
646
|
+
current > 0 ? dim(`${current} current`) : "",
|
|
647
|
+
unknown > 0 ? fg(brand.warning, `${unknown} unverified`) : "",
|
|
648
|
+
failed > 0 ? fg(brand.danger, `${failed} failed`) : "",
|
|
649
|
+
].filter(Boolean);
|
|
650
|
+
return [
|
|
651
|
+
"",
|
|
652
|
+
`${PAD}${bar} ${parts.join(dim(" · "))} ${dim(elapsed(ms))}`,
|
|
653
|
+
"",
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// -- one-off notices ---------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
export function ok(text: string): string {
|
|
660
|
+
return `${PAD}${fg(brand.success, "✓")} ${text}`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function warn(text: string): string {
|
|
664
|
+
return `${PAD}${fg(brand.warning, "⚠")} ${text}`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function fail(text: string): string {
|
|
668
|
+
return `${PAD}${fg(brand.danger, "✗")} ${text}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function info(text: string): string {
|
|
672
|
+
return `${PAD}${fg(brand.info, "›")} ${text}`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export function note(text: string): string {
|
|
676
|
+
return `${PAD} ${dim(text)}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// -- unverified items ----------------------------------------------------------
|
|
680
|
+
//
|
|
681
|
+
// A plugin is `unknown` for several distinct reasons — its marketplace publishes
|
|
682
|
+
// no version, the two versions cannot be ordered, no marketplace offers it, its
|
|
683
|
+
// marketplace was not read, or the catalog fetch failed — and every row above
|
|
684
|
+
// already prints its own. These lines therefore claim only what holds for EVERY
|
|
685
|
+
// reason. They once advised "re-run once the catalog is reachable", which is
|
|
686
|
+
// false for a plugin whose marketplace publishes no version: re-running changes
|
|
687
|
+
// nothing, and the reader went looking for a network fault that did not exist.
|
|
688
|
+
|
|
689
|
+
/** Unverified tools, under the plan. */
|
|
690
|
+
export function unknownToolsNotice(count: number, names: string[]): string {
|
|
691
|
+
return warn(
|
|
692
|
+
`${bold(String(count))} tool(s) could not be checked: ${names.join(", ")} — not applied. Reasons are shown above.`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* `--check` is about to exit 0 while N items went unchecked.
|
|
698
|
+
*
|
|
699
|
+
* The gate does not fail on an unknown — plugin or tool — because most unknowns
|
|
700
|
+
* are permanent (a marketplace that publishes no version, a tool another npm
|
|
701
|
+
* owns), and a gate that can never pass protects nothing. This line is what
|
|
702
|
+
* keeps the exit 0 honest: it is "nothing is KNOWN to be behind", not
|
|
703
|
+
* "everything was verified". It counts plugins and tools together because the
|
|
704
|
+
* two per-kind notices above already name which is which.
|
|
705
|
+
*/
|
|
706
|
+
export function uncheckedGateNotice(count: number): string {
|
|
707
|
+
return warn(
|
|
708
|
+
`${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.`,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** The run ends with nothing to apply because every remaining plugin is unverified. */
|
|
713
|
+
export function nothingToApplyNotice(unknownPlugins: number): string {
|
|
714
|
+
return warn(
|
|
715
|
+
`Nothing to apply. ${unknownPlugins} plugin(s) could not be checked. They are NOT reported as up to date. Reasons are shown above.`,
|
|
716
|
+
);
|
|
717
|
+
}
|