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,242 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
|
|
5
|
+
const CLAUDE_PLUGINS_DIR = path.join(
|
|
6
|
+
os.homedir(),
|
|
7
|
+
".claude",
|
|
8
|
+
"plugins",
|
|
9
|
+
"marketplaces",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a required environment variable for a plugin's MCP server
|
|
14
|
+
*/
|
|
15
|
+
export interface PluginEnvRequirement {
|
|
16
|
+
/** The environment variable name (e.g., "FIGMA_ACCESS_TOKEN") */
|
|
17
|
+
name: string;
|
|
18
|
+
/** User-friendly label (defaults to name if not provided) */
|
|
19
|
+
label: string;
|
|
20
|
+
/** The MCP server name this env var is for */
|
|
21
|
+
serverName: string;
|
|
22
|
+
/** Whether this env var is required (all detected vars are considered required) */
|
|
23
|
+
required: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extract ${VAR_NAME} patterns from a value
|
|
28
|
+
* @param value - The value to search for env var references
|
|
29
|
+
* @returns Array of env var names found
|
|
30
|
+
*/
|
|
31
|
+
function extractEnvVarReferences(value: unknown): string[] {
|
|
32
|
+
if (typeof value !== "string") return [];
|
|
33
|
+
|
|
34
|
+
const matches = value.match(/\$\{([A-Z_][A-Z0-9_]*)\}/g);
|
|
35
|
+
if (!matches) return [];
|
|
36
|
+
|
|
37
|
+
return matches
|
|
38
|
+
.map((match) => {
|
|
39
|
+
// Extract VAR_NAME from ${VAR_NAME}
|
|
40
|
+
const innerMatch = match.match(/\$\{([A-Z_][A-Z0-9_]*)\}/);
|
|
41
|
+
return innerMatch ? innerMatch[1] : "";
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Recursively extract all env var references from an object
|
|
48
|
+
*/
|
|
49
|
+
function extractAllEnvVarReferences(obj: unknown): string[] {
|
|
50
|
+
const vars: string[] = [];
|
|
51
|
+
|
|
52
|
+
if (typeof obj === "string") {
|
|
53
|
+
vars.push(...extractEnvVarReferences(obj));
|
|
54
|
+
} else if (Array.isArray(obj)) {
|
|
55
|
+
for (const item of obj) {
|
|
56
|
+
vars.push(...extractAllEnvVarReferences(item));
|
|
57
|
+
}
|
|
58
|
+
} else if (obj && typeof obj === "object") {
|
|
59
|
+
for (const value of Object.values(obj)) {
|
|
60
|
+
vars.push(...extractAllEnvVarReferences(value));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return vars;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface McpServerConfig {
|
|
68
|
+
command?: string;
|
|
69
|
+
args?: string[];
|
|
70
|
+
env?: Record<string, string>;
|
|
71
|
+
type?: "http";
|
|
72
|
+
url?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the path to a plugin's mcp-config.json in the marketplace cache
|
|
77
|
+
* @param marketplaceName - Name of the marketplace (e.g., "magus")
|
|
78
|
+
* @param pluginSource - Plugin source path from marketplace.json (e.g., "./plugins/frontend")
|
|
79
|
+
* @returns Full path to mcp-config.json or undefined if not found
|
|
80
|
+
*/
|
|
81
|
+
function getPluginMcpConfigPath(
|
|
82
|
+
marketplaceName: string,
|
|
83
|
+
pluginSource?: string,
|
|
84
|
+
): string | undefined {
|
|
85
|
+
if (!pluginSource) return undefined;
|
|
86
|
+
|
|
87
|
+
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
|
|
88
|
+
const pluginPath = path.join(
|
|
89
|
+
marketplacePath,
|
|
90
|
+
pluginSource.replace(/^\.\//, ""),
|
|
91
|
+
);
|
|
92
|
+
const mcpConfigPath = path.join(pluginPath, "mcp-servers", "mcp-config.json");
|
|
93
|
+
|
|
94
|
+
return mcpConfigPath;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read and parse a plugin's MCP configuration
|
|
99
|
+
* @param marketplaceName - Name of the marketplace
|
|
100
|
+
* @param pluginSource - Plugin source path from marketplace.json
|
|
101
|
+
* @returns Parsed MCP config or empty object if not found
|
|
102
|
+
*/
|
|
103
|
+
export async function readPluginMcpConfig(
|
|
104
|
+
marketplaceName: string,
|
|
105
|
+
pluginSource?: string,
|
|
106
|
+
): Promise<Record<string, McpServerConfig>> {
|
|
107
|
+
const configPath = getPluginMcpConfigPath(marketplaceName, pluginSource);
|
|
108
|
+
if (!configPath) return {};
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (await fs.pathExists(configPath)) {
|
|
112
|
+
const content = await fs.readJson(configPath);
|
|
113
|
+
// Filter out comment fields (keys starting with _)
|
|
114
|
+
const filtered: Record<string, McpServerConfig> = {};
|
|
115
|
+
for (const [key, value] of Object.entries(content)) {
|
|
116
|
+
if (!key.startsWith("_") && value && typeof value === "object") {
|
|
117
|
+
filtered[key] = value as McpServerConfig;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return filtered;
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Failed to read plugin MCP config: ${error}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate user-friendly label from env var name
|
|
131
|
+
* @param varName - Environment variable name (e.g., "FIGMA_ACCESS_TOKEN")
|
|
132
|
+
* @returns Human-readable label (e.g., "Figma Access Token")
|
|
133
|
+
*/
|
|
134
|
+
function generateLabel(varName: string): string {
|
|
135
|
+
return varName
|
|
136
|
+
.split("_")
|
|
137
|
+
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
|
|
138
|
+
.join(" ");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get all environment variable requirements for a plugin
|
|
143
|
+
* Extracts ${VAR_NAME} patterns from the plugin's mcp-config.json
|
|
144
|
+
*
|
|
145
|
+
* @param marketplaceName - Name of the marketplace (e.g., "magus")
|
|
146
|
+
* @param pluginSource - Plugin source path from marketplace.json (e.g., "./plugins/frontend")
|
|
147
|
+
* @returns Array of required env vars with metadata
|
|
148
|
+
*/
|
|
149
|
+
export async function getPluginEnvRequirements(
|
|
150
|
+
marketplaceName: string,
|
|
151
|
+
pluginSource?: string,
|
|
152
|
+
): Promise<PluginEnvRequirement[]> {
|
|
153
|
+
const mcpConfig = await readPluginMcpConfig(marketplaceName, pluginSource);
|
|
154
|
+
const requirements: PluginEnvRequirement[] = [];
|
|
155
|
+
const seenVars = new Set<string>();
|
|
156
|
+
|
|
157
|
+
for (const [serverName, config] of Object.entries(mcpConfig)) {
|
|
158
|
+
// Extract from env object
|
|
159
|
+
if (config.env) {
|
|
160
|
+
for (const value of Object.values(config.env)) {
|
|
161
|
+
const vars = extractEnvVarReferences(value);
|
|
162
|
+
for (const varName of vars) {
|
|
163
|
+
if (!seenVars.has(varName)) {
|
|
164
|
+
seenVars.add(varName);
|
|
165
|
+
requirements.push({
|
|
166
|
+
name: varName,
|
|
167
|
+
label: generateLabel(varName),
|
|
168
|
+
serverName,
|
|
169
|
+
required: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Also check args array for env var references (e.g., sqlite with ${DATABASE_PATH})
|
|
177
|
+
if (config.args) {
|
|
178
|
+
const argsVars = extractAllEnvVarReferences(config.args);
|
|
179
|
+
for (const varName of argsVars) {
|
|
180
|
+
if (!seenVars.has(varName)) {
|
|
181
|
+
seenVars.add(varName);
|
|
182
|
+
requirements.push({
|
|
183
|
+
name: varName,
|
|
184
|
+
label: generateLabel(varName),
|
|
185
|
+
serverName,
|
|
186
|
+
required: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return requirements;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get plugin source path from local marketplace cache
|
|
198
|
+
* @param marketplaceName - Name of the marketplace
|
|
199
|
+
* @param pluginName - Name of the plugin
|
|
200
|
+
* @returns Plugin source path or undefined
|
|
201
|
+
*/
|
|
202
|
+
export async function getPluginSourcePath(
|
|
203
|
+
marketplaceName: string,
|
|
204
|
+
pluginName: string,
|
|
205
|
+
): Promise<string | undefined> {
|
|
206
|
+
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
|
|
207
|
+
const manifestPath = path.join(
|
|
208
|
+
marketplacePath,
|
|
209
|
+
".claude-plugin",
|
|
210
|
+
"marketplace.json",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
if (await fs.pathExists(manifestPath)) {
|
|
215
|
+
const manifest = await fs.readJson(manifestPath);
|
|
216
|
+
if (manifest.plugins && Array.isArray(manifest.plugins)) {
|
|
217
|
+
const plugin = manifest.plugins.find(
|
|
218
|
+
(p: { name: string }) => p.name === pluginName,
|
|
219
|
+
);
|
|
220
|
+
return plugin?.source;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// Return undefined if can't read
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all MCP server names from a plugin's config
|
|
232
|
+
* @param marketplaceName - Name of the marketplace
|
|
233
|
+
* @param pluginSource - Plugin source path
|
|
234
|
+
* @returns Array of MCP server names
|
|
235
|
+
*/
|
|
236
|
+
export async function getPluginMcpServerNames(
|
|
237
|
+
marketplaceName: string,
|
|
238
|
+
pluginSource?: string,
|
|
239
|
+
): Promise<string[]> {
|
|
240
|
+
const mcpConfig = await readPluginMcpConfig(marketplaceName, pluginSource);
|
|
241
|
+
return Object.keys(mcpConfig);
|
|
242
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalisation for the `releases` array published in marketplace manifests.
|
|
3
|
+
*
|
|
4
|
+
* Lives apart from both readers because both need it: `marketplace-fetcher.ts`
|
|
5
|
+
* (manifest over the network) and `local-marketplace.ts` (same manifest from the
|
|
6
|
+
* on-disk cache). Putting it in either would make one import the other, and
|
|
7
|
+
* marketplace-fetcher was split out precisely to avoid importing local-marketplace,
|
|
8
|
+
* whose module-level `os.homedir()` constants contaminate the test suite.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PluginRelease } from "../types/index.js";
|
|
12
|
+
|
|
13
|
+
/** Most release entries the panel will show, however many the manifest declares. */
|
|
14
|
+
export const MAX_RELEASES = 5;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Coerce a manifest `releases` value into renderable shape, or undefined.
|
|
18
|
+
*
|
|
19
|
+
* The remote manifest is fetched over the network from a repo whose contents we do
|
|
20
|
+
* not control at read time, so nothing about its shape may be assumed — a number
|
|
21
|
+
* where a string belongs, or a ten-thousand-entry array, would otherwise reach the
|
|
22
|
+
* renderer. Entries missing a version or summary are dropped rather than rendered
|
|
23
|
+
* blank, and the array is capped so one manifest cannot flood the detail panel.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeReleases(input: unknown): PluginRelease[] | undefined {
|
|
26
|
+
if (!Array.isArray(input)) return undefined;
|
|
27
|
+
|
|
28
|
+
const out: PluginRelease[] = [];
|
|
29
|
+
for (const raw of input.slice(0, MAX_RELEASES)) {
|
|
30
|
+
if (!raw || typeof raw !== "object") continue;
|
|
31
|
+
const r = raw as Record<string, unknown>;
|
|
32
|
+
if (typeof r.version !== "string" || typeof r.summary !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
if (!r.version.trim() || !r.summary.trim()) continue;
|
|
35
|
+
out.push({
|
|
36
|
+
version: r.version.trim(),
|
|
37
|
+
...(typeof r.date === "string" && r.date.trim()
|
|
38
|
+
? { date: r.date.trim() }
|
|
39
|
+
: {}),
|
|
40
|
+
...(Array.isArray(r.kinds)
|
|
41
|
+
? { kinds: r.kinds.filter((k): k is string => typeof k === "string") }
|
|
42
|
+
: {}),
|
|
43
|
+
summary: r.summary.trim(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return out.length > 0 ? out : undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read plugin-declared binary dependencies from plugin.json `requires.bin`.
|
|
3
|
+
*
|
|
4
|
+
* A plugin declares the binaries it needs on PATH (e.g. terminal@magus needs
|
|
5
|
+
* `tmux` and `tmux-mcp`). `magus install` resolves these transitively so a
|
|
6
|
+
* teammate who enables a plugin also gets its binaries — closing the class of
|
|
7
|
+
* bug where a plugin's MCP server silently fails because its binary is missing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import type {
|
|
14
|
+
BinInstaller,
|
|
15
|
+
BinRequirement,
|
|
16
|
+
PluginRequires,
|
|
17
|
+
} from "../types/index.js";
|
|
18
|
+
import { goBinaryName } from "../utils/go-module.js";
|
|
19
|
+
import { parsePluginId } from "../utils/string-utils.js";
|
|
20
|
+
import { getLocalMarketplacesInfo } from "./plugin-manager.js";
|
|
21
|
+
|
|
22
|
+
const MARKETPLACES_DIR = (): string =>
|
|
23
|
+
path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
24
|
+
|
|
25
|
+
const VALID_INSTALLERS: readonly BinInstaller[] = [
|
|
26
|
+
"bun",
|
|
27
|
+
"brew",
|
|
28
|
+
"npm",
|
|
29
|
+
"pip",
|
|
30
|
+
"go",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a raw `requires.bin` entry into a complete BinRequirement,
|
|
35
|
+
* filling in defaults. Returns null for entries that can't be made valid
|
|
36
|
+
* (missing name, or an unrecognized installer).
|
|
37
|
+
*/
|
|
38
|
+
function normalizeBin(raw: unknown): BinRequirement | null {
|
|
39
|
+
if (!raw || typeof raw !== "object") return null;
|
|
40
|
+
const o = raw as Record<string, unknown>;
|
|
41
|
+
const name = typeof o.name === "string" ? o.name.trim() : "";
|
|
42
|
+
if (!name) return null;
|
|
43
|
+
|
|
44
|
+
const via = o.via as BinInstaller;
|
|
45
|
+
if (!VALID_INSTALLERS.includes(via)) return null;
|
|
46
|
+
|
|
47
|
+
const bin: BinRequirement = {
|
|
48
|
+
name,
|
|
49
|
+
via,
|
|
50
|
+
check:
|
|
51
|
+
typeof o.check === "string" && o.check.trim()
|
|
52
|
+
? o.check.trim()
|
|
53
|
+
: `${name} --version`,
|
|
54
|
+
};
|
|
55
|
+
if (typeof o.package === "string") bin.package = o.package;
|
|
56
|
+
if (typeof o.formula === "string") bin.formula = o.formula;
|
|
57
|
+
if (typeof o.module === "string") bin.module = o.module;
|
|
58
|
+
if (typeof o.version === "string") bin.version = o.version;
|
|
59
|
+
return bin;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Split a package/module spec into its base and optional version. Handles
|
|
63
|
+
* scoped npm (@scope/pkg@1.2.0) and go modules (host/path@latest). */
|
|
64
|
+
function splitVersion(spec: string): { base: string; version?: string } {
|
|
65
|
+
const at = spec.lastIndexOf("@");
|
|
66
|
+
// at > 0 avoids treating a leading npm scope "@scope/..." as a version.
|
|
67
|
+
if (at > 0) return { base: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
68
|
+
return { base: spec };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const basename = (p: string): string => p.split("/").pop() ?? p;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map a plugin.json `setup` block (the established convention: browser-use uses
|
|
75
|
+
* `pip`, terminal uses `go` + `required`) to BinRequirements.
|
|
76
|
+
* setup: { bun|npm|pip: string[], go: string[], brew: string[], required: string[] }
|
|
77
|
+
*/
|
|
78
|
+
function parseSetup(setup: unknown): BinRequirement[] {
|
|
79
|
+
if (!setup || typeof setup !== "object") return [];
|
|
80
|
+
const s = setup as Record<string, unknown>;
|
|
81
|
+
const bins: BinRequirement[] = [];
|
|
82
|
+
|
|
83
|
+
const pkgManager = (key: "bun" | "npm" | "pip"): void => {
|
|
84
|
+
for (const spec of (s[key] as string[] | undefined) ?? []) {
|
|
85
|
+
if (typeof spec !== "string") continue;
|
|
86
|
+
const { base, version } = splitVersion(spec);
|
|
87
|
+
bins.push({
|
|
88
|
+
name: basename(base),
|
|
89
|
+
via: key,
|
|
90
|
+
package: base,
|
|
91
|
+
version,
|
|
92
|
+
check: `${basename(base)} --version`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
pkgManager("bun");
|
|
97
|
+
pkgManager("npm");
|
|
98
|
+
pkgManager("pip");
|
|
99
|
+
|
|
100
|
+
// Not basename(): a Go module's trailing /vN is its major version, not the
|
|
101
|
+
// binary. `tmux-mcp/v2` installs `tmux-mcp`; naming it `v2` here made every
|
|
102
|
+
// downstream presence check look for a binary that never exists.
|
|
103
|
+
for (const spec of (s.go as string[] | undefined) ?? []) {
|
|
104
|
+
if (typeof spec !== "string") continue;
|
|
105
|
+
const { base, version } = splitVersion(spec);
|
|
106
|
+
const name = goBinaryName(base);
|
|
107
|
+
bins.push({
|
|
108
|
+
name,
|
|
109
|
+
via: "go",
|
|
110
|
+
module: base,
|
|
111
|
+
version,
|
|
112
|
+
check: `${name} --version`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const spec of (s.brew as string[] | undefined) ?? []) {
|
|
116
|
+
if (typeof spec !== "string") continue;
|
|
117
|
+
bins.push({
|
|
118
|
+
name: spec,
|
|
119
|
+
via: "brew",
|
|
120
|
+
formula: spec,
|
|
121
|
+
check: `${spec} --version`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// `required` names a system tool that must be present; brew is the assumed
|
|
125
|
+
// installer (matches the "system tools via brew" decision).
|
|
126
|
+
for (const name of (s.required as string[] | undefined) ?? []) {
|
|
127
|
+
if (typeof name !== "string") continue;
|
|
128
|
+
bins.push({ name, via: "brew", formula: name, check: `${name} --version` });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return bins;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a plugin.json's binary requirements from BOTH the established `setup`
|
|
136
|
+
* block and the richer explicit `requires.bin` form. On a name collision the
|
|
137
|
+
* explicit `requires.bin` entry wins (it can override installer/check/version).
|
|
138
|
+
*/
|
|
139
|
+
export function parsePluginRequires(pluginJson: unknown): PluginRequires {
|
|
140
|
+
if (!pluginJson || typeof pluginJson !== "object") return {};
|
|
141
|
+
const obj = pluginJson as Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
const fromSetup = parseSetup(obj.setup);
|
|
144
|
+
|
|
145
|
+
const requires = obj.requires;
|
|
146
|
+
const binRaw =
|
|
147
|
+
requires && typeof requires === "object"
|
|
148
|
+
? (requires as Record<string, unknown>).bin
|
|
149
|
+
: undefined;
|
|
150
|
+
const fromRequires = Array.isArray(binRaw)
|
|
151
|
+
? binRaw.map(normalizeBin).filter((b): b is BinRequirement => b !== null)
|
|
152
|
+
: [];
|
|
153
|
+
|
|
154
|
+
// Merge, requires.bin winning by name.
|
|
155
|
+
const byName = new Map<string, BinRequirement>();
|
|
156
|
+
for (const b of fromSetup) byName.set(b.name, b);
|
|
157
|
+
for (const b of fromRequires) byName.set(b.name, b);
|
|
158
|
+
|
|
159
|
+
const bin = [...byName.values()];
|
|
160
|
+
return bin.length > 0 ? { bin } : {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Read `requires` from a plugin.json at a known plugin directory. */
|
|
164
|
+
export async function readPluginRequires(
|
|
165
|
+
pluginPath: string,
|
|
166
|
+
): Promise<PluginRequires> {
|
|
167
|
+
// `.claude-plugin/plugin.json` FIRST — that is the only location Claude Code's
|
|
168
|
+
// runtime loader reads, and the only one its documentation describes. The root
|
|
169
|
+
// fallback stays for third-party plugins whose authors put it there; such a manifest
|
|
170
|
+
// is inert as far as Claude Code is concerned, but magus can still read metadata
|
|
171
|
+
// out of it rather than reporting nothing.
|
|
172
|
+
const manifestPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
|
|
173
|
+
try {
|
|
174
|
+
if (await fs.pathExists(manifestPath)) {
|
|
175
|
+
return parsePluginRequires(await fs.readJson(manifestPath));
|
|
176
|
+
}
|
|
177
|
+
const rootLevel = path.join(pluginPath, "plugin.json");
|
|
178
|
+
if (await fs.pathExists(rootLevel)) {
|
|
179
|
+
return parsePluginRequires(await fs.readJson(rootLevel));
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Unreadable/malformed — treat as no requirements.
|
|
183
|
+
}
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a plugin id (name@marketplace) to its on-disk source directory in
|
|
189
|
+
* the local marketplace cache. Returns null if the marketplace/plugin/source
|
|
190
|
+
* can't be located. Mirrors doctor.ts's resolvePluginPath.
|
|
191
|
+
*/
|
|
192
|
+
export async function resolvePluginPath(
|
|
193
|
+
pluginId: string,
|
|
194
|
+
): Promise<string | null> {
|
|
195
|
+
const parsed = parsePluginId(pluginId);
|
|
196
|
+
if (!parsed) return null;
|
|
197
|
+
const { pluginName, marketplace } = parsed;
|
|
198
|
+
|
|
199
|
+
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
200
|
+
const localMp = localMarketplaces.get(marketplace);
|
|
201
|
+
if (!localMp) return null;
|
|
202
|
+
|
|
203
|
+
const localPlugin = localMp.plugins.find((p) => p.name === pluginName);
|
|
204
|
+
if (!localPlugin?.source || typeof localPlugin.source !== "string") {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const pluginPath = path.join(
|
|
209
|
+
MARKETPLACES_DIR(),
|
|
210
|
+
marketplace,
|
|
211
|
+
localPlugin.source.replace("./", ""),
|
|
212
|
+
);
|
|
213
|
+
return (await fs.pathExists(pluginPath)) ? pluginPath : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the binary requirements a plugin declares. Returns an empty array
|
|
218
|
+
* when the plugin can't be located or declares none.
|
|
219
|
+
*/
|
|
220
|
+
export async function resolvePluginBinRequirements(
|
|
221
|
+
pluginId: string,
|
|
222
|
+
): Promise<BinRequirement[]> {
|
|
223
|
+
const pluginPath = await resolvePluginPath(pluginId);
|
|
224
|
+
if (!pluginPath) return [];
|
|
225
|
+
const requires = await readPluginRequires(pluginPath);
|
|
226
|
+
return requires.bin ?? [];
|
|
227
|
+
}
|