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
package/src/ui/App.tsx
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { useRenderer } from "@opentui/react";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
5
|
+
import {
|
|
6
|
+
migrateMarketplaceRename,
|
|
7
|
+
recoverMarketplaceSettings,
|
|
8
|
+
} from "../services/claude-settings.js";
|
|
9
|
+
import { checkGitignore } from "../services/gitignore-check.js";
|
|
10
|
+
import {
|
|
11
|
+
applyAllSafeFixes,
|
|
12
|
+
loadGitignoreState,
|
|
13
|
+
} from "../services/gitignore-service.js";
|
|
14
|
+
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
15
|
+
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
16
|
+
import {
|
|
17
|
+
type VersionCheckResult,
|
|
18
|
+
checkForUpdates,
|
|
19
|
+
getCurrentVersion,
|
|
20
|
+
} from "../services/version-check.js";
|
|
21
|
+
import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
22
|
+
import { ModalContainer } from "./components/modals/index.js";
|
|
23
|
+
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
24
|
+
import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
|
|
25
|
+
import {
|
|
26
|
+
AliasScreen,
|
|
27
|
+
CliToolsScreen,
|
|
28
|
+
GitignoreScreen,
|
|
29
|
+
McpRegistryScreen,
|
|
30
|
+
McpScreen,
|
|
31
|
+
ModelsScreen,
|
|
32
|
+
PluginsScreen,
|
|
33
|
+
ProfilesScreen,
|
|
34
|
+
SettingsScreen,
|
|
35
|
+
SkillsScreen,
|
|
36
|
+
StylesScreen,
|
|
37
|
+
} from "./screens/index.js";
|
|
38
|
+
import {
|
|
39
|
+
AppProvider,
|
|
40
|
+
useApp,
|
|
41
|
+
useModal,
|
|
42
|
+
useNavigation,
|
|
43
|
+
} from "./state/AppContext.js";
|
|
44
|
+
import {
|
|
45
|
+
DimensionsProvider,
|
|
46
|
+
useDimensions,
|
|
47
|
+
} from "./state/DimensionsContext.js";
|
|
48
|
+
import type { Screen } from "./state/types.js";
|
|
49
|
+
import { theme } from "./theme.js";
|
|
50
|
+
|
|
51
|
+
export const VERSION = getCurrentVersion();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Router Component
|
|
55
|
+
* Renders the current screen based on route
|
|
56
|
+
*/
|
|
57
|
+
function Router() {
|
|
58
|
+
const { state } = useApp();
|
|
59
|
+
const { currentRoute } = state;
|
|
60
|
+
|
|
61
|
+
switch (currentRoute.screen) {
|
|
62
|
+
case "plugins":
|
|
63
|
+
return <PluginsScreen />;
|
|
64
|
+
case "mcp":
|
|
65
|
+
return <McpScreen />;
|
|
66
|
+
case "mcp-registry":
|
|
67
|
+
return <McpRegistryScreen />;
|
|
68
|
+
case "settings":
|
|
69
|
+
return <SettingsScreen />;
|
|
70
|
+
case "cli-tools":
|
|
71
|
+
return <CliToolsScreen />;
|
|
72
|
+
case "profiles":
|
|
73
|
+
return <ProfilesScreen />;
|
|
74
|
+
case "skills":
|
|
75
|
+
return <SkillsScreen />;
|
|
76
|
+
case "styles":
|
|
77
|
+
return <StylesScreen />;
|
|
78
|
+
case "gitignore":
|
|
79
|
+
return <GitignoreScreen />;
|
|
80
|
+
case "alias":
|
|
81
|
+
return <AliasScreen />;
|
|
82
|
+
case "models":
|
|
83
|
+
return <ModelsScreen />;
|
|
84
|
+
default:
|
|
85
|
+
return <PluginsScreen />;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* GlobalKeyHandler Component
|
|
91
|
+
* Handles global keyboard shortcuts (1-8, Tab, Escape, q, Ctrl+C, ?, Shift+D)
|
|
92
|
+
* Does not render anything (returns null)
|
|
93
|
+
*/
|
|
94
|
+
function GlobalKeyHandler({
|
|
95
|
+
onDebugToggle,
|
|
96
|
+
onExit,
|
|
97
|
+
}: { onDebugToggle: () => void; onExit: () => void }): null {
|
|
98
|
+
const { state } = useApp();
|
|
99
|
+
const { navigateToScreen } = useNavigation();
|
|
100
|
+
const modal = useModal();
|
|
101
|
+
const renderer = useRenderer();
|
|
102
|
+
|
|
103
|
+
useKeyboardHandler((input, key) => {
|
|
104
|
+
// Debug key (Shift+D) - always available
|
|
105
|
+
if (input === "D" && key.shift) {
|
|
106
|
+
onDebugToggle();
|
|
107
|
+
// Write debug info to file
|
|
108
|
+
const debugInfo = {
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
terminal: { rows: renderer.height, columns: renderer.width },
|
|
111
|
+
state: {
|
|
112
|
+
currentRoute: state.currentRoute,
|
|
113
|
+
isSearching: state.isSearching,
|
|
114
|
+
modal: state.modal ? { type: state.modal.type } : null,
|
|
115
|
+
plugins: {
|
|
116
|
+
scope: state.plugins.scope,
|
|
117
|
+
selectedIndex: state.plugins.selectedIndex,
|
|
118
|
+
searchQuery: state.plugins.searchQuery,
|
|
119
|
+
marketplacesStatus: state.plugins.marketplaces.status,
|
|
120
|
+
pluginsStatus: state.plugins.plugins.status,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
"/tmp/magus-debug.json",
|
|
126
|
+
JSON.stringify(debugInfo, null, 2),
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Don't handle keys when modal is open or searching
|
|
132
|
+
if (state.modal || state.isSearching) return;
|
|
133
|
+
|
|
134
|
+
// Global navigation shortcuts (1-9, then 0) - include mcp-registry as it's
|
|
135
|
+
// a sub-screen of mcp
|
|
136
|
+
const isTopLevel = [
|
|
137
|
+
"plugins",
|
|
138
|
+
"mcp",
|
|
139
|
+
"mcp-registry",
|
|
140
|
+
"settings",
|
|
141
|
+
"cli-tools",
|
|
142
|
+
"profiles",
|
|
143
|
+
"skills",
|
|
144
|
+
"styles",
|
|
145
|
+
"gitignore",
|
|
146
|
+
"alias",
|
|
147
|
+
"models",
|
|
148
|
+
].includes(state.currentRoute.screen);
|
|
149
|
+
|
|
150
|
+
if (isTopLevel) {
|
|
151
|
+
if (input === "1") navigateToScreen("plugins");
|
|
152
|
+
else if (input === "2") navigateToScreen("skills");
|
|
153
|
+
else if (input === "3") navigateToScreen("mcp");
|
|
154
|
+
else if (input === "4") navigateToScreen("settings");
|
|
155
|
+
else if (input === "5") navigateToScreen("profiles");
|
|
156
|
+
else if (input === "6") navigateToScreen("cli-tools");
|
|
157
|
+
else if (input === "7") navigateToScreen("gitignore");
|
|
158
|
+
else if (input === "8") navigateToScreen("alias");
|
|
159
|
+
else if (input === "9") navigateToScreen("styles");
|
|
160
|
+
else if (input === "0") navigateToScreen("models");
|
|
161
|
+
|
|
162
|
+
// Tab navigation cycling
|
|
163
|
+
if (key.tab) {
|
|
164
|
+
const screens: Screen[] = [
|
|
165
|
+
"plugins",
|
|
166
|
+
"skills",
|
|
167
|
+
"mcp",
|
|
168
|
+
"settings",
|
|
169
|
+
"profiles",
|
|
170
|
+
"cli-tools",
|
|
171
|
+
"gitignore",
|
|
172
|
+
"alias",
|
|
173
|
+
"styles",
|
|
174
|
+
"models",
|
|
175
|
+
];
|
|
176
|
+
const currentIndex = screens.indexOf(
|
|
177
|
+
state.currentRoute.screen as Screen,
|
|
178
|
+
);
|
|
179
|
+
if (currentIndex !== -1) {
|
|
180
|
+
const nextIndex = key.shift
|
|
181
|
+
? (currentIndex - 1 + screens.length) % screens.length
|
|
182
|
+
: (currentIndex + 1) % screens.length;
|
|
183
|
+
navigateToScreen(screens[nextIndex]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// q or Ctrl+C quits from anywhere — no need to bounce to home first.
|
|
189
|
+
if (input === "q" || (key.ctrl && input === "c")) {
|
|
190
|
+
onExit();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Escape goes back: registry → mcp, any other tab → plugins home,
|
|
195
|
+
// already-home → exit. But when a screen has a focused sub-view open
|
|
196
|
+
// (e.g. the Alias flag-detail editor), that sub-view owns Escape and
|
|
197
|
+
// backs out to itself — don't let the global handler jump to home.
|
|
198
|
+
// Scoped to the Escape branch ONLY so q/Ctrl+C still quit from anywhere.
|
|
199
|
+
if (key.escape) {
|
|
200
|
+
if (state.detailActive) return;
|
|
201
|
+
if (state.currentRoute.screen === "plugins") {
|
|
202
|
+
onExit();
|
|
203
|
+
} else if (state.currentRoute.screen === "mcp-registry") {
|
|
204
|
+
navigateToScreen("mcp");
|
|
205
|
+
} else {
|
|
206
|
+
navigateToScreen("plugins");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ? for help
|
|
211
|
+
if (input === "?") {
|
|
212
|
+
modal.message(
|
|
213
|
+
"magus Help",
|
|
214
|
+
`Navigation
|
|
215
|
+
↑/↓ or j/k Move selection
|
|
216
|
+
Enter Select / Toggle
|
|
217
|
+
Escape Back (to home)
|
|
218
|
+
q / Ctrl+C Quit (from anywhere)
|
|
219
|
+
? This help
|
|
220
|
+
|
|
221
|
+
Quick Navigation
|
|
222
|
+
1 Plugins 4 Settings 7 Git State 0 Models
|
|
223
|
+
2 Skills 5 Profiles 8 Alias
|
|
224
|
+
3 MCP Servers 6 CLI Tools 9 Styles
|
|
225
|
+
|
|
226
|
+
Plugin Actions
|
|
227
|
+
u Update d Uninstall
|
|
228
|
+
a Update All r Refresh
|
|
229
|
+
|
|
230
|
+
MCP Servers
|
|
231
|
+
/ Search local + remote
|
|
232
|
+
r Browse MCP registry
|
|
233
|
+
|
|
234
|
+
Styles
|
|
235
|
+
Space Tick / untick a style
|
|
236
|
+
a Apply the selection to this project
|
|
237
|
+
x Reset the selection to what is live
|
|
238
|
+
c Clear the active output style
|
|
239
|
+
|
|
240
|
+
Models
|
|
241
|
+
a / Enter Route this project through the selected preset
|
|
242
|
+
c Turn model tiers off
|
|
243
|
+
r Re-read the config`,
|
|
244
|
+
"info",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* UpdateBanner Component
|
|
254
|
+
* Shows version update notification
|
|
255
|
+
*/
|
|
256
|
+
function UpdateBanner({ result }: { result: VersionCheckResult }) {
|
|
257
|
+
if (!result.updateAvailable) return null;
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
261
|
+
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
262
|
+
<strong> UPDATE </strong>
|
|
263
|
+
</text>
|
|
264
|
+
<text fg={theme.colors.warning}>
|
|
265
|
+
{" "}
|
|
266
|
+
v{result.currentVersion} → v{result.latestVersion}
|
|
267
|
+
</text>
|
|
268
|
+
<text fg={theme.colors.muted}> Run: </text>
|
|
269
|
+
<text fg={theme.colors.info}>magus upgrade</text>
|
|
270
|
+
</box>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* ProgressIndicator Component
|
|
276
|
+
* Shows progress bar when operations are running
|
|
277
|
+
*/
|
|
278
|
+
function ProgressIndicator({
|
|
279
|
+
message,
|
|
280
|
+
current,
|
|
281
|
+
total,
|
|
282
|
+
}: {
|
|
283
|
+
message: string;
|
|
284
|
+
current?: number;
|
|
285
|
+
total?: number;
|
|
286
|
+
}) {
|
|
287
|
+
return (
|
|
288
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
289
|
+
<ProgressBar message={message} current={current} total={total} />
|
|
290
|
+
</box>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* AppContentInner Component
|
|
296
|
+
* Main app layout with all UI elements
|
|
297
|
+
*/
|
|
298
|
+
interface AppContentInnerProps {
|
|
299
|
+
showDebug: boolean;
|
|
300
|
+
onDebugToggle: () => void;
|
|
301
|
+
updateInfo: VersionCheckResult | null;
|
|
302
|
+
onExit: () => void;
|
|
303
|
+
recoveryReport: string | null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function AppContentInner({
|
|
307
|
+
showDebug,
|
|
308
|
+
onDebugToggle,
|
|
309
|
+
updateInfo,
|
|
310
|
+
onExit,
|
|
311
|
+
recoveryReport,
|
|
312
|
+
}: AppContentInnerProps) {
|
|
313
|
+
const { state } = useApp();
|
|
314
|
+
const { progress } = state;
|
|
315
|
+
const dimensions = useDimensions();
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<box flexDirection="column" height={dimensions.terminalHeight}>
|
|
319
|
+
{updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
|
|
320
|
+
{recoveryReport && (
|
|
321
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
322
|
+
<text fg={theme.colors.success}>✓ Fixed: {recoveryReport}</text>
|
|
323
|
+
</box>
|
|
324
|
+
)}
|
|
325
|
+
{showDebug && (
|
|
326
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
327
|
+
<text fg={theme.colors.muted}>
|
|
328
|
+
DEBUG: {dimensions.terminalWidth}x{dimensions.terminalHeight} |
|
|
329
|
+
content={dimensions.contentHeight} | screen=
|
|
330
|
+
{state.currentRoute.screen}
|
|
331
|
+
</text>
|
|
332
|
+
</box>
|
|
333
|
+
)}
|
|
334
|
+
{progress && <ProgressIndicator {...progress} />}
|
|
335
|
+
<box
|
|
336
|
+
flexDirection="column"
|
|
337
|
+
height={dimensions.contentHeight}
|
|
338
|
+
paddingLeft={1}
|
|
339
|
+
paddingRight={1}
|
|
340
|
+
>
|
|
341
|
+
<Router />
|
|
342
|
+
</box>
|
|
343
|
+
<GlobalKeyHandler onDebugToggle={onDebugToggle} onExit={onExit} />
|
|
344
|
+
<ModalContainer />
|
|
345
|
+
</box>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* AppContent Component
|
|
351
|
+
* Wraps app with DimensionsProvider and manages state for debug/updates
|
|
352
|
+
*/
|
|
353
|
+
interface AppContentProps {
|
|
354
|
+
onExit: () => void;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function AppContent({ onExit }: AppContentProps) {
|
|
358
|
+
const { state, dispatch } = useApp();
|
|
359
|
+
const { progress } = state;
|
|
360
|
+
const [showDebug, setShowDebug] = useState(false);
|
|
361
|
+
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
362
|
+
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
363
|
+
const [gitignoreData, setGitignoreData] = useState<{
|
|
364
|
+
violationCount: number;
|
|
365
|
+
safeCount: number;
|
|
366
|
+
} | null>(null);
|
|
367
|
+
const gitignoreModal = useGitignoreModal();
|
|
368
|
+
|
|
369
|
+
// Check for updates on startup, then re-check periodically (force-bypassing
|
|
370
|
+
// the session cache) so the banner reflects a release that lands while
|
|
371
|
+
// magus is left open — otherwise it shows the stale startup value.
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
checkForUpdates()
|
|
374
|
+
.then(setUpdateInfo)
|
|
375
|
+
.catch(() => {});
|
|
376
|
+
const id = setInterval(
|
|
377
|
+
() => {
|
|
378
|
+
checkForUpdates(true)
|
|
379
|
+
.then(setUpdateInfo)
|
|
380
|
+
.catch(() => {});
|
|
381
|
+
},
|
|
382
|
+
15 * 60 * 1000, // 15 min
|
|
383
|
+
);
|
|
384
|
+
return () => clearInterval(id);
|
|
385
|
+
}, []);
|
|
386
|
+
|
|
387
|
+
// Auto-dismiss recovery banner after 5 seconds
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (!recoveryReport) return;
|
|
390
|
+
const timer = setTimeout(() => setRecoveryReport(null), 5000);
|
|
391
|
+
return () => clearTimeout(timer);
|
|
392
|
+
}, [recoveryReport]);
|
|
393
|
+
|
|
394
|
+
// Show gitignore modal — wait for the modal slot to be free so we don't
|
|
395
|
+
// stomp on another modal that may have just been shown.
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
if (!gitignoreData) return;
|
|
398
|
+
if (state.modal) return;
|
|
399
|
+
gitignoreModal.show({
|
|
400
|
+
violationCount: gitignoreData.violationCount,
|
|
401
|
+
safeCount: gitignoreData.safeCount,
|
|
402
|
+
onFixSafe: async () => {
|
|
403
|
+
const s = await loadGitignoreState(state.projectPath);
|
|
404
|
+
await applyAllSafeFixes(state.projectPath, s.violations);
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
setGitignoreData(null);
|
|
408
|
+
}, [gitignoreData, gitignoreModal, state.modal, state.projectPath]);
|
|
409
|
+
|
|
410
|
+
// Auto-refresh marketplaces on startup
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
const noRefresh = process.argv.includes("--no-refresh");
|
|
413
|
+
if (noRefresh) return;
|
|
414
|
+
|
|
415
|
+
dispatch({
|
|
416
|
+
type: "SHOW_PROGRESS",
|
|
417
|
+
state: { message: "Scanning marketplaces..." },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
421
|
+
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
422
|
+
|
|
423
|
+
// Auto-add marketplaces referenced by enabledPlugins but missing from
|
|
424
|
+
// the local registry. Fixes the "fresh clone of a project whose
|
|
425
|
+
// .claude/settings.json enables @magus plugins but this machine has
|
|
426
|
+
// never run `claude plugin marketplace add`" case.
|
|
427
|
+
(async () => {
|
|
428
|
+
try {
|
|
429
|
+
if (!(await isClaudeAvailable())) return;
|
|
430
|
+
const added = await autoAddMissingMarketplaces(process.cwd());
|
|
431
|
+
if (added.length > 0) {
|
|
432
|
+
setRecoveryReport(`added marketplace(s): ${added.join(", ")}`);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// non-fatal: user can still install marketplaces manually
|
|
436
|
+
}
|
|
437
|
+
})();
|
|
438
|
+
|
|
439
|
+
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
440
|
+
recoverMarketplaceSettings()
|
|
441
|
+
.then(async (recovery) => {
|
|
442
|
+
const parts: string[] = [];
|
|
443
|
+
if (recovery.reregistered.length > 0) {
|
|
444
|
+
// Update the marketplace clone now that the source is fixed
|
|
445
|
+
const { updateMarketplace } = await import(
|
|
446
|
+
"../services/claude-cli.js"
|
|
447
|
+
);
|
|
448
|
+
for (const mp of recovery.reregistered) {
|
|
449
|
+
try {
|
|
450
|
+
await updateMarketplace(mp);
|
|
451
|
+
parts.push(`${mp} refreshed`);
|
|
452
|
+
} catch {
|
|
453
|
+
parts.push(`${mp} (update failed)`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (recovery.enabledAutoUpdate.length > 0) {
|
|
458
|
+
parts.push(`auto-update: ${recovery.enabledAutoUpdate.join(", ")}`);
|
|
459
|
+
}
|
|
460
|
+
if (recovery.removed.length > 0) {
|
|
461
|
+
parts.push(`removed: ${recovery.removed.join(", ")}`);
|
|
462
|
+
}
|
|
463
|
+
if (parts.length > 0) {
|
|
464
|
+
setRecoveryReport(parts.join(" | "));
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.catch(() => {}); // non-fatal
|
|
468
|
+
|
|
469
|
+
// Check for gitignore manifest violations and surface a popup if any.
|
|
470
|
+
// Loads twice (once for count, once when user picks "fix safe") because
|
|
471
|
+
// the second load happens after edits — keeps detection cheap on startup.
|
|
472
|
+
(async () => {
|
|
473
|
+
try {
|
|
474
|
+
const result = await checkGitignore(process.cwd());
|
|
475
|
+
if (result.violationCount > 0) {
|
|
476
|
+
const s = await loadGitignoreState(process.cwd());
|
|
477
|
+
const safeCount = s.violations.filter(
|
|
478
|
+
(v) => v.severity === "safe",
|
|
479
|
+
).length;
|
|
480
|
+
setGitignoreData({
|
|
481
|
+
violationCount: result.violationCount,
|
|
482
|
+
safeCount,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
// non-fatal
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
489
|
+
|
|
490
|
+
repairAllMarketplaces()
|
|
491
|
+
.then(async () => {
|
|
492
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
493
|
+
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
494
|
+
})
|
|
495
|
+
.catch(() => {
|
|
496
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
497
|
+
});
|
|
498
|
+
}, [dispatch]);
|
|
499
|
+
|
|
500
|
+
// Count transient banners for dimension calculation
|
|
501
|
+
const transientBannerCount = recoveryReport ? 1 : 0;
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<DimensionsProvider
|
|
505
|
+
showProgress={!!progress}
|
|
506
|
+
showDebug={showDebug}
|
|
507
|
+
showUpdateBanner={!!updateInfo?.updateAvailable}
|
|
508
|
+
transientBannerCount={transientBannerCount}
|
|
509
|
+
>
|
|
510
|
+
<AppContentInner
|
|
511
|
+
showDebug={showDebug}
|
|
512
|
+
onDebugToggle={() => setShowDebug((s) => !s)}
|
|
513
|
+
updateInfo={updateInfo}
|
|
514
|
+
onExit={onExit}
|
|
515
|
+
recoveryReport={recoveryReport}
|
|
516
|
+
/>
|
|
517
|
+
</DimensionsProvider>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* App Component (Root)
|
|
523
|
+
* Entry point for the OpenTUI application
|
|
524
|
+
* Wraps everything with AppProvider
|
|
525
|
+
*/
|
|
526
|
+
interface AppProps {
|
|
527
|
+
onExit: () => void;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function App({ onExit }: AppProps) {
|
|
531
|
+
return (
|
|
532
|
+
<AppProvider>
|
|
533
|
+
<AppContent onExit={onExit} />
|
|
534
|
+
</AppProvider>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export default App;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalogNotice.ts — the text of the Plugins screen's warning line.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from PluginsScreen for the same reason as pluginsAdapter: so it can
|
|
5
|
+
* be tested without importing a React/OpenTUI screen. That is not merely tidier —
|
|
6
|
+
* importing the screen into a test pulls the renderer into the shared test process
|
|
7
|
+
* and broke fifteen unrelated tests.
|
|
8
|
+
*
|
|
9
|
+
* What this does NOT warn about, deliberately
|
|
10
|
+
* -------------------------------------------
|
|
11
|
+
* `autoUpdate: false` and a marketplace clone sitting behind its remote are both
|
|
12
|
+
* NORMAL here. Plugins are not auto-updated by policy; the UI's job is to show
|
|
13
|
+
* that an update exists, not to advance the install source. An earlier version of
|
|
14
|
+
* this banner reported both as problems and suggested
|
|
15
|
+
* `claude plugin marketplace update`, which would have defeated the policy it was
|
|
16
|
+
* complaining about. A warning that fires on intended behaviour trains the user to
|
|
17
|
+
* ignore the line.
|
|
18
|
+
*
|
|
19
|
+
* The one thing worth warning about is an update check that did not happen —
|
|
20
|
+
* because the alternative is claiming a plugin is current when nobody looked.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { formatWait } from "../../services/github-budget.js";
|
|
24
|
+
import type { MarketplaceFetchFailure } from "../../services/marketplace-fetcher.js";
|
|
25
|
+
|
|
26
|
+
export interface CatalogNoticeInput {
|
|
27
|
+
/** Catalogs that could not be read from any authoritative source. */
|
|
28
|
+
failures: MarketplaceFetchFailure[];
|
|
29
|
+
/** Installed plugins whose version could not be verified against upstream. */
|
|
30
|
+
unverifiedPlugins: number;
|
|
31
|
+
/** Terminal columns available for the line. */
|
|
32
|
+
width: number;
|
|
33
|
+
/** For the countdown; injected so tests are not clock-dependent. */
|
|
34
|
+
now?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One line naming why some versions on screen could not be verified — or `null`
|
|
39
|
+
* when everything was actually checked.
|
|
40
|
+
*
|
|
41
|
+
* Returns `null` when there is nothing wrong: a banner that is always present is
|
|
42
|
+
* a banner nobody reads.
|
|
43
|
+
*
|
|
44
|
+
* Segments are added in priority order and dropped from the end when they do not
|
|
45
|
+
* fit `width`. Listing every failing marketplace overflowed an 80-column pane and
|
|
46
|
+
* took the retry hint with it — a warning truncated mid-sentence is barely better
|
|
47
|
+
* than no warning.
|
|
48
|
+
*/
|
|
49
|
+
export function buildCatalogNoticeText({
|
|
50
|
+
failures,
|
|
51
|
+
unverifiedPlugins,
|
|
52
|
+
width,
|
|
53
|
+
now = Date.now(),
|
|
54
|
+
}: CatalogNoticeInput): string | null {
|
|
55
|
+
if (failures.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
const segments: string[] = [];
|
|
58
|
+
|
|
59
|
+
// Lead with the reason, not the names: "rate limit" tells the user to wait,
|
|
60
|
+
// "timed out" tells them to check the network. That distinction is the whole
|
|
61
|
+
// value of classifying the failure, and it costs few characters.
|
|
62
|
+
const kinds = new Set(failures.map((f) => f.kind));
|
|
63
|
+
const rateLimited = kinds.has("rate-limited");
|
|
64
|
+
const reason = rateLimited
|
|
65
|
+
? "GitHub rate limit"
|
|
66
|
+
: kinds.has("timeout")
|
|
67
|
+
? "GitHub timed out"
|
|
68
|
+
: (failures[0]?.detail ?? "fetch failed");
|
|
69
|
+
|
|
70
|
+
const noun = failures.length === 1 ? "catalog" : "catalogs";
|
|
71
|
+
// Naming them only pays off when there are one or two; past that the count
|
|
72
|
+
// carries the same information in a fraction of the width.
|
|
73
|
+
const named =
|
|
74
|
+
failures.length <= 2
|
|
75
|
+
? `: ${failures.map((f) => f.marketplace).join(", ")}`
|
|
76
|
+
: "";
|
|
77
|
+
segments.push(`${failures.length} ${noun} unchecked (${reason})${named}`);
|
|
78
|
+
|
|
79
|
+
if (unverifiedPlugins > 0) {
|
|
80
|
+
segments.push(
|
|
81
|
+
`${unverifiedPlugins} version${unverifiedPlugins === 1 ? "" : "s"} unverified`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The countdown replaces the bare retry hint when we know when the window
|
|
86
|
+
// reopens. Answering "when can I try again" is the point: "try later" leaves
|
|
87
|
+
// the user to guess, and guessing wrong spends another request against the
|
|
88
|
+
// limit. Soonest retry across all failures, since that is when anything at all
|
|
89
|
+
// becomes possible again.
|
|
90
|
+
const retryAt = failures
|
|
91
|
+
.map((f) => f.retryAt)
|
|
92
|
+
.filter((t): t is number => typeof t === "number" && t > now)
|
|
93
|
+
.sort((a, b) => a - b)[0];
|
|
94
|
+
|
|
95
|
+
const hint = retryAt
|
|
96
|
+
? ` · retrying in ${formatWait(retryAt - now)}`
|
|
97
|
+
: " · r to retry";
|
|
98
|
+
|
|
99
|
+
// Column budget. `⚠` renders double-width in most terminals while
|
|
100
|
+
// `String.length` counts it as one, so one column is held back for it.
|
|
101
|
+
const maxColumns = Math.max(1, width - 1);
|
|
102
|
+
|
|
103
|
+
// The retry affordance is reserved out of the budget rather than appended: it
|
|
104
|
+
// is the only action the line offers, so it is the last thing that may be cut.
|
|
105
|
+
// No floor on what remains — a floor is what let a 40-column pane overflow by
|
|
106
|
+
// two, which is the whole failure mode this budget exists to prevent.
|
|
107
|
+
const messageBudget = maxColumns - hint.length;
|
|
108
|
+
|
|
109
|
+
let line = `⚠ ${segments[0]}`;
|
|
110
|
+
for (const segment of segments.slice(1)) {
|
|
111
|
+
const candidate = `${line} · ${segment}`;
|
|
112
|
+
if (candidate.length > messageBudget) break;
|
|
113
|
+
line = candidate;
|
|
114
|
+
}
|
|
115
|
+
if (line.length > messageBudget) {
|
|
116
|
+
line = messageBudget > 1 ? `${line.slice(0, messageBudget - 1)}…` : "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Absurdly narrow pane: the hint alone does not fit. Truncate it rather than
|
|
120
|
+
// return a line that overflows and corrupts the layout.
|
|
121
|
+
return `${line}${hint}`.slice(0, maxColumns);
|
|
122
|
+
}
|