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,367 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import React, { useEffect, useCallback, useState, useRef } from "react";
|
|
4
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
5
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
7
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
8
|
+
|
|
9
|
+
import { cliTools } from "../../data/cli-tools.js";
|
|
10
|
+
import { loadActiveClosure } from "../../services/active-closure.js";
|
|
11
|
+
import { cliToolUninstall } from "../../services/cli-tool-commands.js";
|
|
12
|
+
import {
|
|
13
|
+
type InstallInfo,
|
|
14
|
+
PROBE_CONCURRENCY,
|
|
15
|
+
REAL_TOOL_PROBE_IO,
|
|
16
|
+
detectInstallMethods,
|
|
17
|
+
probeTools,
|
|
18
|
+
specForCatalogTool,
|
|
19
|
+
} from "../../services/tool-probe.js";
|
|
20
|
+
import type { Command } from "../../utils/command-utils.js";
|
|
21
|
+
import { formatCommand } from "../../utils/command-utils.js";
|
|
22
|
+
import {
|
|
23
|
+
type CliToolRow,
|
|
24
|
+
buildToolRow,
|
|
25
|
+
countToolRows,
|
|
26
|
+
isInstalled,
|
|
27
|
+
} from "../adapters/cliToolsAdapter.js";
|
|
28
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
29
|
+
import {
|
|
30
|
+
renderCliToolDetail,
|
|
31
|
+
renderCliToolRow,
|
|
32
|
+
} from "../renderers/cliToolRenderers.js";
|
|
33
|
+
import { theme } from "../theme.js";
|
|
34
|
+
|
|
35
|
+
const execFileAsync = promisify(execFile);
|
|
36
|
+
|
|
37
|
+
// ─── Session cache ─────────────────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// The probe itself lives in services/tool-probe.ts — the same one `magus
|
|
40
|
+
// update` uses — so this screen and the CLI cannot disagree about a tool. It
|
|
41
|
+
// used to carry a private copy: a `/bin/bash` `which`, an `npm view` per tool,
|
|
42
|
+
// its own version comparator, and a string-form `exec` of each check command.
|
|
43
|
+
|
|
44
|
+
let cachedRows: CliToolRow[] | null = null;
|
|
45
|
+
let cacheInitialized = false;
|
|
46
|
+
/** Bumped by every fetch, so a refresh discards the answers of the one before. */
|
|
47
|
+
let fetchGeneration = 0;
|
|
48
|
+
|
|
49
|
+
function clearCliToolsCache(): void {
|
|
50
|
+
cachedRows = null;
|
|
51
|
+
cacheInitialized = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Rows before anything is known: the catalogue mapping, checking. */
|
|
55
|
+
function initialRows(): CliToolRow[] {
|
|
56
|
+
return cliTools.map((tool) =>
|
|
57
|
+
buildToolRow(tool, specForCatalogTool(tool, null, undefined), undefined),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `fn` over `items`, at most `limit` at a time, results in input order. */
|
|
62
|
+
async function mapLimited<T, R>(
|
|
63
|
+
items: readonly T[],
|
|
64
|
+
limit: number,
|
|
65
|
+
fn: (item: T) => Promise<R>,
|
|
66
|
+
): Promise<R[]> {
|
|
67
|
+
const out: R[] = new Array(items.length);
|
|
68
|
+
let next = 0;
|
|
69
|
+
const worker = async () => {
|
|
70
|
+
while (next < items.length) {
|
|
71
|
+
const i = next++;
|
|
72
|
+
const item = items[i];
|
|
73
|
+
if (item !== undefined) out[i] = await fn(item);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
await Promise.all(
|
|
77
|
+
Array.from({ length: Math.min(limit, items.length) }, worker),
|
|
78
|
+
);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Component ─────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function CliToolsScreen() {
|
|
85
|
+
const { state, dispatch } = useApp();
|
|
86
|
+
const { cliTools: cliToolsState } = state;
|
|
87
|
+
const modal = useModal();
|
|
88
|
+
const dimensions = useDimensions();
|
|
89
|
+
|
|
90
|
+
const [rows, setRows] = useState<CliToolRow[]>(
|
|
91
|
+
() => cachedRows || initialRows(),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const rowsRef = useRef(rows);
|
|
95
|
+
rowsRef.current = rows;
|
|
96
|
+
|
|
97
|
+
const setAndCache = useCallback(
|
|
98
|
+
(update: (prev: CliToolRow[]) => CliToolRow[]) => {
|
|
99
|
+
setRows((prev) => {
|
|
100
|
+
const next = update(prev);
|
|
101
|
+
cachedRows = next;
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Detection first — it decides which installer each tool is probed through
|
|
110
|
+
* (§4.5 of the design: the closure's spec, else the detected installer, else
|
|
111
|
+
* the catalogue) — then one shared probe, rows filling in as tools settle.
|
|
112
|
+
*/
|
|
113
|
+
const fetchVersionInfo = useCallback(async () => {
|
|
114
|
+
const generation = ++fetchGeneration;
|
|
115
|
+
const projectPath = state.projectPath || process.cwd();
|
|
116
|
+
const [active, installs] = await Promise.all([
|
|
117
|
+
// Full resolution, not `resolveBins: false`: a catalogue tool may be
|
|
118
|
+
// declared by a PLUGIN's requires.bin, and parity needs the spec the
|
|
119
|
+
// CLI plans with. `resolveBins: false` would still keep the profile's
|
|
120
|
+
// own cliTools, but lose every plugin-declared bin and its pin.
|
|
121
|
+
loadActiveClosure(projectPath),
|
|
122
|
+
mapLimited(cliTools, PROBE_CONCURRENCY, (tool) =>
|
|
123
|
+
detectInstallMethods(tool, REAL_TOOL_PROBE_IO),
|
|
124
|
+
),
|
|
125
|
+
]);
|
|
126
|
+
if (generation !== fetchGeneration) return;
|
|
127
|
+
|
|
128
|
+
const closureBins = active?.closure.bins ?? null;
|
|
129
|
+
const planned = cliTools.map((tool, i) => ({
|
|
130
|
+
tool,
|
|
131
|
+
install: installs[i],
|
|
132
|
+
spec: specForCatalogTool(tool, closureBins, installs[i]),
|
|
133
|
+
}));
|
|
134
|
+
setAndCache(() =>
|
|
135
|
+
planned.map((p) => buildToolRow(p.tool, p.spec, undefined, p.install)),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await probeTools(
|
|
139
|
+
planned.map((p) => p.spec),
|
|
140
|
+
REAL_TOOL_PROBE_IO,
|
|
141
|
+
{
|
|
142
|
+
onResult: (facts, { index }) => {
|
|
143
|
+
if (generation !== fetchGeneration) return;
|
|
144
|
+
const p = planned[index];
|
|
145
|
+
if (!p) return;
|
|
146
|
+
const row = buildToolRow(p.tool, p.spec, facts, p.install);
|
|
147
|
+
setAndCache((prev) => {
|
|
148
|
+
const next = [...prev];
|
|
149
|
+
next[index] = row;
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}, [setAndCache, state.projectPath]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!cacheInitialized) {
|
|
159
|
+
cacheInitialized = true;
|
|
160
|
+
cachedRows = rows;
|
|
161
|
+
void fetchVersionInfo();
|
|
162
|
+
}
|
|
163
|
+
}, [fetchVersionInfo, rows]);
|
|
164
|
+
|
|
165
|
+
useKeyboard((event) => {
|
|
166
|
+
if (state.isSearching || state.modal) return;
|
|
167
|
+
|
|
168
|
+
if (event.name === "up" || event.name === "k") {
|
|
169
|
+
const newIndex = Math.max(0, cliToolsState.selectedIndex - 1);
|
|
170
|
+
dispatch({ type: "CLITOOLS_SELECT", index: newIndex });
|
|
171
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
172
|
+
const newIndex = Math.min(
|
|
173
|
+
rows.length - 1,
|
|
174
|
+
cliToolsState.selectedIndex + 1,
|
|
175
|
+
);
|
|
176
|
+
dispatch({ type: "CLITOOLS_SELECT", index: newIndex });
|
|
177
|
+
} else if (event.name === "r") {
|
|
178
|
+
handleRefresh();
|
|
179
|
+
} else if (event.name === "a") {
|
|
180
|
+
handleUpdateAll();
|
|
181
|
+
} else if (event.name === "c") {
|
|
182
|
+
handleResolveConflict();
|
|
183
|
+
} else if (event.name === "enter" || event.name === "return") {
|
|
184
|
+
handleInstall();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const handleRefresh = () => {
|
|
189
|
+
clearCliToolsCache();
|
|
190
|
+
setRows(initialRows());
|
|
191
|
+
cacheInitialized = true;
|
|
192
|
+
void fetchVersionInfo();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Run one installer command and report success.
|
|
197
|
+
*
|
|
198
|
+
* `execFile`, so there is no shell to re-parse a package name — the same
|
|
199
|
+
* reason `utils/run.ts` exists. It does NOT call that shared `runCommand`:
|
|
200
|
+
* that one inherits stdio, and a TUI must never hand the terminal to a
|
|
201
|
+
* child. Output is captured and discarded here; the modal is the progress
|
|
202
|
+
* indicator.
|
|
203
|
+
*/
|
|
204
|
+
const runCommand = async (
|
|
205
|
+
command: Command,
|
|
206
|
+
): Promise<{ ok: boolean; error?: string }> => {
|
|
207
|
+
try {
|
|
208
|
+
await execFileAsync(command.cmd, [...command.args], { timeout: 60000 });
|
|
209
|
+
return { ok: true };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
212
|
+
return { ok: false, error: msg };
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handleInstall = async () => {
|
|
217
|
+
const row = rows[cliToolsState.selectedIndex];
|
|
218
|
+
if (!row || row.checking) return;
|
|
219
|
+
|
|
220
|
+
const { tool, status, updateCommand, installCommand } = row;
|
|
221
|
+
const installed = isInstalled(status);
|
|
222
|
+
// Enter on an `unknown` row still offers the detected-method command:
|
|
223
|
+
// an explicit user action, not a plan. The detail panel shows why the
|
|
224
|
+
// row is unverified beside it.
|
|
225
|
+
const action = !installed
|
|
226
|
+
? "Installing"
|
|
227
|
+
: status?.state === "upgrade" || status?.state === "unknown"
|
|
228
|
+
? "Updating"
|
|
229
|
+
: "Reinstalling";
|
|
230
|
+
// Both sides come from the row, which built them from the SAME rule its
|
|
231
|
+
// state came from — a declared tool's pin included.
|
|
232
|
+
const command = installed && updateCommand ? updateCommand : installCommand;
|
|
233
|
+
|
|
234
|
+
modal.loading(`${action} ${tool.displayName}...`);
|
|
235
|
+
const result = await runCommand(command);
|
|
236
|
+
modal.hideModal();
|
|
237
|
+
|
|
238
|
+
if (result.ok) {
|
|
239
|
+
handleRefresh();
|
|
240
|
+
} else {
|
|
241
|
+
await modal.message(
|
|
242
|
+
"Error",
|
|
243
|
+
`Failed to ${action.toLowerCase()} ${tool.displayName}.\n\nTry running manually:\n${formatCommand(command)}`,
|
|
244
|
+
"error",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleResolveConflict = async () => {
|
|
250
|
+
const row = rows[cliToolsState.selectedIndex];
|
|
251
|
+
const install: InstallInfo | undefined = row?.install;
|
|
252
|
+
if (!row || !install || install.all.length < 2) return;
|
|
253
|
+
|
|
254
|
+
const { tool } = row;
|
|
255
|
+
const allMethods = install.all;
|
|
256
|
+
|
|
257
|
+
// Let user pick which install to keep
|
|
258
|
+
const options = allMethods.map((method) => ({
|
|
259
|
+
label: `Keep ${method}${method === install.primary ? " (active)" : ""}, remove others`,
|
|
260
|
+
value: method,
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const keep = await modal.select(
|
|
264
|
+
`Resolve ${tool.displayName} conflict`,
|
|
265
|
+
`Installed via ${allMethods.join(" + ")}. Which to keep?`,
|
|
266
|
+
options,
|
|
267
|
+
);
|
|
268
|
+
if (keep === null) return;
|
|
269
|
+
|
|
270
|
+
const toRemove = allMethods.filter((m) => m !== keep);
|
|
271
|
+
modal.loading(`Removing ${toRemove.join(", ")} install(s)...`);
|
|
272
|
+
|
|
273
|
+
const errors: string[] = [];
|
|
274
|
+
for (const method of toRemove) {
|
|
275
|
+
const cmd = cliToolUninstall(tool, method, install.brewFormula);
|
|
276
|
+
if (!cmd) continue;
|
|
277
|
+
const result = await runCommand(cmd);
|
|
278
|
+
if (!result.ok) errors.push(`${method}: ${formatCommand(cmd)}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
modal.hideModal();
|
|
282
|
+
|
|
283
|
+
if (errors.length > 0) {
|
|
284
|
+
await modal.message(
|
|
285
|
+
"Partial",
|
|
286
|
+
`Some removals failed. Try manually:\n\n${errors.join("\n")}`,
|
|
287
|
+
"error",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
handleRefresh();
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const handleUpdateAll = async () => {
|
|
295
|
+
// Only rows the shared classifier says are behind. An `unknown` is never
|
|
296
|
+
// swept up in a bulk action — that is exactly the "cannot tell, so run
|
|
297
|
+
// it anyway" the CLI no longer does either.
|
|
298
|
+
const updatable = rows.filter((r) => r.status?.state === "upgrade");
|
|
299
|
+
if (updatable.length === 0) {
|
|
300
|
+
await modal.message(
|
|
301
|
+
"Up to Date",
|
|
302
|
+
"No tool is known to be behind.",
|
|
303
|
+
"info",
|
|
304
|
+
);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
modal.loading(`Updating ${updatable.length} tool(s)...`);
|
|
309
|
+
|
|
310
|
+
for (const row of updatable) {
|
|
311
|
+
await runCommand(row.updateCommand ?? row.installCommand);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
modal.hideModal();
|
|
315
|
+
handleRefresh();
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const selectedRow = rows[cliToolsState.selectedIndex];
|
|
319
|
+
|
|
320
|
+
const counts = countToolRows(rows);
|
|
321
|
+
const statusContent = (
|
|
322
|
+
<text fg={theme.colors.text}>
|
|
323
|
+
<span fg={theme.colors.muted}>Installed: </span>
|
|
324
|
+
<span fg={theme.colors.info}>
|
|
325
|
+
{counts.installed}/{rows.length}
|
|
326
|
+
</span>
|
|
327
|
+
{counts.updates > 0 && (
|
|
328
|
+
<>
|
|
329
|
+
<span fg={theme.colors.muted}> │ Updates: </span>
|
|
330
|
+
<span fg={theme.colors.warning}>{counts.updates}</span>
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
{counts.unverified > 0 && (
|
|
334
|
+
<>
|
|
335
|
+
<span fg={theme.colors.muted}> │ Unverified: </span>
|
|
336
|
+
<span fg={theme.colors.warning}>{counts.unverified}</span>
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
</text>
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<ScreenLayout
|
|
344
|
+
title="magus CLI Tools"
|
|
345
|
+
currentScreen="cli-tools"
|
|
346
|
+
statusLine={statusContent}
|
|
347
|
+
footerHints={[
|
|
348
|
+
{ keys: ["Enter"], label: "install" },
|
|
349
|
+
{ keys: ["a"], label: "update all" },
|
|
350
|
+
{ keys: ["c"], label: "fix conflict" },
|
|
351
|
+
{ keys: ["r"], label: "refresh" },
|
|
352
|
+
]}
|
|
353
|
+
listPanel={
|
|
354
|
+
<ScrollableList
|
|
355
|
+
items={rows}
|
|
356
|
+
selectedIndex={cliToolsState.selectedIndex}
|
|
357
|
+
renderItem={renderCliToolRow}
|
|
358
|
+
maxHeight={dimensions.listPanelHeight}
|
|
359
|
+
getKey={(row) => row.tool.name}
|
|
360
|
+
/>
|
|
361
|
+
}
|
|
362
|
+
detailPanel={renderCliToolDetail(selectedRow)}
|
|
363
|
+
/>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export default CliToolsScreen;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { SETTINGS_CATALOG } from "../../data/settings-catalog.js";
|
|
3
|
+
import {
|
|
4
|
+
discoverOutputStyles,
|
|
5
|
+
readAllSettingsBothScopes,
|
|
6
|
+
writeSettingValue,
|
|
7
|
+
} from "../../services/settings-manager.js";
|
|
8
|
+
import {
|
|
9
|
+
type SettingsBrowserItem,
|
|
10
|
+
buildSettingsBrowserItems,
|
|
11
|
+
} from "../adapters/settingsAdapter.js";
|
|
12
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
13
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
14
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
15
|
+
import {
|
|
16
|
+
renderSettingDetail,
|
|
17
|
+
renderSettingRow,
|
|
18
|
+
} from "../renderers/settingsRenderers.js";
|
|
19
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
20
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
21
|
+
import { theme } from "../theme.js";
|
|
22
|
+
|
|
23
|
+
export function SettingsScreen() {
|
|
24
|
+
const { state, dispatch } = useApp();
|
|
25
|
+
const { settings } = state;
|
|
26
|
+
const modal = useModal();
|
|
27
|
+
const dimensions = useDimensions();
|
|
28
|
+
|
|
29
|
+
const fetchData = useCallback(async () => {
|
|
30
|
+
dispatch({ type: "SETTINGS_DATA_LOADING" });
|
|
31
|
+
try {
|
|
32
|
+
// Populate dynamic output style options from installed plugins
|
|
33
|
+
const outputStyleSetting = SETTINGS_CATALOG.find(
|
|
34
|
+
(s) => s.id === "output-style",
|
|
35
|
+
);
|
|
36
|
+
if (outputStyleSetting && outputStyleSetting.type === "select") {
|
|
37
|
+
outputStyleSetting.options = await discoverOutputStyles(
|
|
38
|
+
state.projectPath,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const values = await readAllSettingsBothScopes(
|
|
43
|
+
SETTINGS_CATALOG,
|
|
44
|
+
state.projectPath,
|
|
45
|
+
);
|
|
46
|
+
dispatch({ type: "SETTINGS_DATA_SUCCESS", values });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
dispatch({
|
|
49
|
+
type: "SETTINGS_DATA_ERROR",
|
|
50
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}, [dispatch, state.projectPath]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
fetchData();
|
|
57
|
+
}, [fetchData]);
|
|
58
|
+
|
|
59
|
+
const listItems = useMemo((): SettingsBrowserItem[] => {
|
|
60
|
+
if (settings.values.status !== "success") return [];
|
|
61
|
+
return buildSettingsBrowserItems(settings.values.data);
|
|
62
|
+
}, [settings.values]);
|
|
63
|
+
|
|
64
|
+
const handleScopeChange = async (scope: "user" | "project") => {
|
|
65
|
+
const item = listItems[settings.selectedIndex];
|
|
66
|
+
if (!item || item.kind !== "setting") return;
|
|
67
|
+
|
|
68
|
+
const { setting, scopedValues } = item;
|
|
69
|
+
const currentValue =
|
|
70
|
+
scope === "user" ? scopedValues.user : scopedValues.project;
|
|
71
|
+
|
|
72
|
+
if (setting.type === "boolean") {
|
|
73
|
+
const currentBool =
|
|
74
|
+
currentValue === "true" ||
|
|
75
|
+
currentValue === "1" ||
|
|
76
|
+
(currentValue === undefined && setting.defaultValue === "true");
|
|
77
|
+
const newValue = currentBool ? "false" : "true";
|
|
78
|
+
try {
|
|
79
|
+
await writeSettingValue(setting, newValue, scope, state.projectPath);
|
|
80
|
+
await fetchData();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
83
|
+
}
|
|
84
|
+
} else if (setting.type === "select" && setting.options) {
|
|
85
|
+
const options = setting.options.map((o) => ({
|
|
86
|
+
label: o.label + (currentValue === o.value ? " (current)" : ""),
|
|
87
|
+
value: o.value,
|
|
88
|
+
}));
|
|
89
|
+
const currentIndex = setting.options.findIndex(
|
|
90
|
+
(o) => o.value === currentValue,
|
|
91
|
+
);
|
|
92
|
+
if (currentValue !== undefined) {
|
|
93
|
+
options.push({ label: "Clear (use default)", value: "__clear__" });
|
|
94
|
+
}
|
|
95
|
+
const selected = await modal.select(
|
|
96
|
+
`${setting.name} — ${scope}`,
|
|
97
|
+
setting.description,
|
|
98
|
+
options,
|
|
99
|
+
currentIndex >= 0 ? currentIndex : undefined,
|
|
100
|
+
);
|
|
101
|
+
if (selected === null) return;
|
|
102
|
+
try {
|
|
103
|
+
const val =
|
|
104
|
+
selected === "__clear__" ? undefined : selected || undefined;
|
|
105
|
+
await writeSettingValue(setting, val, scope, state.projectPath);
|
|
106
|
+
await fetchData();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// String type: if already set, clear it; if unset, show input modal
|
|
112
|
+
if (currentValue !== undefined && currentValue !== "") {
|
|
113
|
+
try {
|
|
114
|
+
await writeSettingValue(setting, undefined, scope, state.projectPath);
|
|
115
|
+
await fetchData();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const newValue = await modal.input(
|
|
121
|
+
`${setting.name} — ${scope}`,
|
|
122
|
+
setting.description,
|
|
123
|
+
currentValue || setting.defaultValue || "",
|
|
124
|
+
);
|
|
125
|
+
if (newValue === null) return;
|
|
126
|
+
try {
|
|
127
|
+
await writeSettingValue(
|
|
128
|
+
setting,
|
|
129
|
+
newValue || undefined,
|
|
130
|
+
scope,
|
|
131
|
+
state.projectPath,
|
|
132
|
+
);
|
|
133
|
+
await fetchData();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
await modal.message("Error", `Failed to update: ${error}`, "error");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
useKeyboard((event) => {
|
|
142
|
+
if (state.isSearching || state.modal) return;
|
|
143
|
+
|
|
144
|
+
if (event.name === "up" || event.name === "k") {
|
|
145
|
+
const newIndex = Math.max(0, settings.selectedIndex - 1);
|
|
146
|
+
dispatch({ type: "SETTINGS_SELECT", index: newIndex });
|
|
147
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
148
|
+
const newIndex = Math.min(
|
|
149
|
+
Math.max(0, listItems.length - 1),
|
|
150
|
+
settings.selectedIndex + 1,
|
|
151
|
+
);
|
|
152
|
+
dispatch({ type: "SETTINGS_SELECT", index: newIndex });
|
|
153
|
+
} else if (event.name === "u") {
|
|
154
|
+
handleScopeChange("user");
|
|
155
|
+
} else if (event.name === "p") {
|
|
156
|
+
handleScopeChange("project");
|
|
157
|
+
} else if (event.name === "enter") {
|
|
158
|
+
handleScopeChange("project");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const selectedItem = listItems[settings.selectedIndex];
|
|
163
|
+
|
|
164
|
+
const renderDetail = () => {
|
|
165
|
+
if (settings.values.status === "loading") {
|
|
166
|
+
return <text fg={theme.colors.muted}>Loading settings...</text>;
|
|
167
|
+
}
|
|
168
|
+
if (settings.values.status === "error") {
|
|
169
|
+
return <text fg={theme.colors.danger}>Failed to load settings</text>;
|
|
170
|
+
}
|
|
171
|
+
return renderSettingDetail(selectedItem);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const totalSet =
|
|
175
|
+
settings.values.status === "success"
|
|
176
|
+
? Array.from(settings.values.data.values()).filter(
|
|
177
|
+
(v) => v.user !== undefined || v.project !== undefined,
|
|
178
|
+
).length
|
|
179
|
+
: 0;
|
|
180
|
+
|
|
181
|
+
const statusContent = (
|
|
182
|
+
<text fg={theme.colors.text}>
|
|
183
|
+
<span fg={theme.colors.muted}>Settings: </span>
|
|
184
|
+
<span fg={theme.colors.info}>{totalSet} configured</span>
|
|
185
|
+
<span fg={theme.colors.muted}> │ u:user p:project</span>
|
|
186
|
+
</text>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<ScreenLayout
|
|
191
|
+
title="magus Settings"
|
|
192
|
+
currentScreen="settings"
|
|
193
|
+
statusLine={statusContent}
|
|
194
|
+
footerHints={[
|
|
195
|
+
{ keys: ["↑", "↓"], label: "nav" },
|
|
196
|
+
{ keys: ["u"], label: "user scope" },
|
|
197
|
+
{ keys: ["p"], label: "project scope" },
|
|
198
|
+
{ keys: ["Enter"], label: "project" },
|
|
199
|
+
]}
|
|
200
|
+
listPanel={
|
|
201
|
+
settings.values.status !== "success" ? (
|
|
202
|
+
<text fg={theme.colors.muted}>
|
|
203
|
+
{settings.values.status === "loading"
|
|
204
|
+
? "Loading..."
|
|
205
|
+
: "Error loading settings"}
|
|
206
|
+
</text>
|
|
207
|
+
) : (
|
|
208
|
+
<ScrollableList
|
|
209
|
+
items={listItems}
|
|
210
|
+
selectedIndex={settings.selectedIndex}
|
|
211
|
+
renderItem={renderSettingRow}
|
|
212
|
+
maxHeight={dimensions.listPanelHeight}
|
|
213
|
+
/>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
detailPanel={renderDetail()}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export default SettingsScreen;
|