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,657 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import type { PluginRelease } from "../types/index.js";
|
|
6
|
+
import { normalizeReleases } from "./plugin-releases.js";
|
|
7
|
+
|
|
8
|
+
export interface LocalMarketplacePlugin {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
category?: string;
|
|
14
|
+
author?: { name: string; email?: string };
|
|
15
|
+
/** Recent release history, as published in the marketplace manifest. */
|
|
16
|
+
releases?: PluginRelease[];
|
|
17
|
+
// Extended info from plugin.json or marketplace.json
|
|
18
|
+
strict?: boolean;
|
|
19
|
+
lspServers?: Record<string, unknown>;
|
|
20
|
+
agents?: string[];
|
|
21
|
+
commands?: string[];
|
|
22
|
+
skills?: string[];
|
|
23
|
+
mcpServers?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LocalMarketplace {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
plugins: LocalMarketplacePlugin[];
|
|
30
|
+
gitRepo?: string; // Extracted from git remote
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CLAUDE_PLUGINS_DIR = path.join(
|
|
34
|
+
os.homedir(),
|
|
35
|
+
".claude",
|
|
36
|
+
"plugins",
|
|
37
|
+
"marketplaces",
|
|
38
|
+
);
|
|
39
|
+
const KNOWN_MARKETPLACES_FILE = path.join(
|
|
40
|
+
os.homedir(),
|
|
41
|
+
".claude",
|
|
42
|
+
"plugins",
|
|
43
|
+
"known_marketplaces.json",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where Claude Code keeps a marketplace's clone, whether or not it exists.
|
|
48
|
+
*
|
|
49
|
+
* Exported so a reader can prefer the clone over the network. Anything already
|
|
50
|
+
* added is on disk in full, and reading it costs no GitHub request — which
|
|
51
|
+
* matters because an unauthenticated client gets 60 an hour for every repo on
|
|
52
|
+
* screen combined.
|
|
53
|
+
*/
|
|
54
|
+
export function getMarketplaceClonePath(marketplaceName: string): string {
|
|
55
|
+
return path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface KnownMarketplaceEntry {
|
|
59
|
+
source: { source: string; url?: string; repo?: string };
|
|
60
|
+
installLocation: string;
|
|
61
|
+
lastUpdated: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type KnownMarketplaces = Record<string, KnownMarketplaceEntry>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read known_marketplaces.json, or null when it cannot be read honestly.
|
|
68
|
+
*
|
|
69
|
+
* The null matters. {@link readKnownMarketplaces} collapses "absent",
|
|
70
|
+
* "unparseable" and "genuinely empty" into `{}`, which is fine for a lookup and
|
|
71
|
+
* catastrophic for a filter: intersecting the marketplaces directory against an
|
|
72
|
+
* empty registry deletes every marketplace, and every installed plugin then
|
|
73
|
+
* reports as no longer published. A registry we could not read is UNKNOWN, and
|
|
74
|
+
* unknown must not render as a determinate negative.
|
|
75
|
+
*/
|
|
76
|
+
async function readKnownMarketplacesOrNull(): Promise<KnownMarketplaces | null> {
|
|
77
|
+
try {
|
|
78
|
+
if (!(await fs.pathExists(KNOWN_MARKETPLACES_FILE))) return null;
|
|
79
|
+
const parsed = await fs.readJson(KNOWN_MARKETPLACES_FILE);
|
|
80
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return parsed as KnownMarketplaces;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read known_marketplaces.json (Claude Code's internal marketplace tracking)
|
|
91
|
+
*/
|
|
92
|
+
async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
|
|
93
|
+
return (await readKnownMarketplacesOrNull()) ?? {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Directory names a parsed `known_marketplaces.json` registers, or null when it
|
|
98
|
+
* says nothing usable.
|
|
99
|
+
*
|
|
100
|
+
* Pure, and exported, because `scanLocalMarketplaces` resolves its paths from
|
|
101
|
+
* `os.homedir()` at module load and cannot be pointed at a temp directory. The
|
|
102
|
+
* decision this encodes is the part worth testing; the IO around it is not.
|
|
103
|
+
*
|
|
104
|
+
* Keyed by BOTH the registry key and the basename of `installLocation`: the two
|
|
105
|
+
* are normally equal and nothing enforces it, and dropping a real marketplace
|
|
106
|
+
* is far worse than keeping a stray directory.
|
|
107
|
+
*
|
|
108
|
+
* An empty object is an ANSWER — nothing is registered — and filters everything
|
|
109
|
+
* out. Only unusable input is null. The distinction lives in
|
|
110
|
+
* {@link readKnownMarketplacesOrNull}, which returns null for an absent file,
|
|
111
|
+
* so a machine that has never added a marketplace stays permissive.
|
|
112
|
+
*/
|
|
113
|
+
export function registeredMarketplaceNames(
|
|
114
|
+
parsed: unknown,
|
|
115
|
+
): Set<string> | null {
|
|
116
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const names = new Set<string>();
|
|
120
|
+
for (const [name, entry] of Object.entries(
|
|
121
|
+
parsed as Record<string, unknown>,
|
|
122
|
+
)) {
|
|
123
|
+
names.add(name);
|
|
124
|
+
const loc = (entry as { installLocation?: unknown })?.installLocation;
|
|
125
|
+
if (typeof loc === "string" && loc.length > 0) {
|
|
126
|
+
names.add(path.basename(loc));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function registeredDirNames(): Promise<Set<string> | null> {
|
|
133
|
+
return registeredMarketplaceNames(await readKnownMarketplacesOrNull());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get git remote URL from a marketplace directory
|
|
138
|
+
*/
|
|
139
|
+
function getGitRemote(marketplacePath: string): string | undefined {
|
|
140
|
+
try {
|
|
141
|
+
const gitDir = path.join(marketplacePath, ".git");
|
|
142
|
+
if (!fs.existsSync(gitDir)) return undefined;
|
|
143
|
+
|
|
144
|
+
const result = execSync("git remote get-url origin", {
|
|
145
|
+
cwd: marketplacePath,
|
|
146
|
+
encoding: "utf-8",
|
|
147
|
+
timeout: 5000,
|
|
148
|
+
}).trim();
|
|
149
|
+
|
|
150
|
+
// Convert SSH URL to HTTPS format: git@github.com:user/repo.git -> user/repo
|
|
151
|
+
if (result.startsWith("git@github.com:")) {
|
|
152
|
+
return result.replace("git@github.com:", "").replace(/\.git$/, "");
|
|
153
|
+
}
|
|
154
|
+
// HTTPS URL: https://github.com/user/repo -> user/repo
|
|
155
|
+
if (result.includes("github.com")) {
|
|
156
|
+
const match = result.match(/github\.com[/:]([^/]+\/[^/\s.]+)/);
|
|
157
|
+
if (match) return match[1].replace(/\.git$/, "");
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
} catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scan a single marketplace directory and return marketplace info
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* Names of the skills under `skillsDir`, recursing through grouping directories.
|
|
170
|
+
*
|
|
171
|
+
* A skill is a directory holding a SKILL.md, and it may sit one or more levels
|
|
172
|
+
* down: dev nests its skills by category under `skills/frontend/`,
|
|
173
|
+
* `skills/backend/` and so on, and mattpocock groups its 37 under
|
|
174
|
+
* `skills/engineering/`. Reading only the top level counted the *groups* — dev
|
|
175
|
+
* once reported 9 skills against 43 on disk, mattpocock 5 against 37 — and that
|
|
176
|
+
* count is what the plugin detail panel prints. dev also ships a `knowledge/`
|
|
177
|
+
* tree beside `skills/`: reference manuals reached by path, holding no SKILL.md
|
|
178
|
+
* and registering nothing, so this scan correctly never sees them.
|
|
179
|
+
*
|
|
180
|
+
* Depth is capped because this runs over every plugin of every cloned
|
|
181
|
+
* marketplace on each list load, and a skill nested four levels below `skills/`
|
|
182
|
+
* does not exist in practice.
|
|
183
|
+
*/
|
|
184
|
+
export async function scanSkillDirs(
|
|
185
|
+
skillsDir: string,
|
|
186
|
+
depth = 0,
|
|
187
|
+
): Promise<string[]> {
|
|
188
|
+
if (depth > 3) return [];
|
|
189
|
+
if (!(await fs.pathExists(skillsDir))) return [];
|
|
190
|
+
|
|
191
|
+
let entries: import("node:fs").Dirent[];
|
|
192
|
+
try {
|
|
193
|
+
entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const found: string[] = [];
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
if (!entry.isDirectory()) continue;
|
|
201
|
+
const child = path.join(skillsDir, entry.name);
|
|
202
|
+
if (await fs.pathExists(path.join(child, "SKILL.md"))) {
|
|
203
|
+
found.push(entry.name);
|
|
204
|
+
} else {
|
|
205
|
+
found.push(...(await scanSkillDirs(child, depth + 1)));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return found;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Names of the MCP servers a plugin declares.
|
|
213
|
+
*
|
|
214
|
+
* Two locations are in use, and only one of them used to be read. An
|
|
215
|
+
* `mcp-servers/` directory of one JSON per server was the only thing scanned;
|
|
216
|
+
* a root `.mcp.json`, which is the form `plugin.json` points at with
|
|
217
|
+
* `"mcpServers": "./.mcp.json"`, was invisible. In this marketplace that is 5
|
|
218
|
+
* plugins of 7, mnemex among them — and mnemex ships *nothing else*, so it
|
|
219
|
+
* reported zero components of every kind and read as an empty plugin.
|
|
220
|
+
*
|
|
221
|
+
* Inside `.mcp.json`, both a bare `{ "<name>": {...} }` map and the
|
|
222
|
+
* `{ "mcpServers": {...} }` wrapper appear in the wild.
|
|
223
|
+
*/
|
|
224
|
+
export async function scanMcpServers(pluginPath: string): Promise<string[]> {
|
|
225
|
+
const found: string[] = [];
|
|
226
|
+
|
|
227
|
+
const mcpDir = path.join(pluginPath, "mcp-servers");
|
|
228
|
+
if (await fs.pathExists(mcpDir)) {
|
|
229
|
+
try {
|
|
230
|
+
const files = await fs.readdir(mcpDir);
|
|
231
|
+
found.push(
|
|
232
|
+
...files
|
|
233
|
+
.filter((f) => f.endsWith(".json"))
|
|
234
|
+
.map((f) => f.replace(".json", "")),
|
|
235
|
+
);
|
|
236
|
+
} catch {
|
|
237
|
+
// Ignore scan errors
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const mcpFile = path.join(pluginPath, ".mcp.json");
|
|
242
|
+
if (await fs.pathExists(mcpFile)) {
|
|
243
|
+
try {
|
|
244
|
+
const raw = (await fs.readJson(mcpFile)) as Record<string, unknown>;
|
|
245
|
+
const servers =
|
|
246
|
+
raw.mcpServers && typeof raw.mcpServers === "object"
|
|
247
|
+
? (raw.mcpServers as Record<string, unknown>)
|
|
248
|
+
: raw;
|
|
249
|
+
for (const name of Object.keys(servers)) {
|
|
250
|
+
if (!found.includes(name)) found.push(name);
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Unreadable or malformed. The file exists, so the plugin does declare a
|
|
254
|
+
// server — report one rather than dropping to zero, which is the exact
|
|
255
|
+
// "ships nothing" misreport this function exists to fix.
|
|
256
|
+
if (found.length === 0) found.push("mcp");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return found;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function scanSingleMarketplace(
|
|
264
|
+
marketplacePath: string,
|
|
265
|
+
marketplaceName: string,
|
|
266
|
+
): Promise<LocalMarketplace | null> {
|
|
267
|
+
const manifestPath = path.join(
|
|
268
|
+
marketplacePath,
|
|
269
|
+
".claude-plugin",
|
|
270
|
+
"marketplace.json",
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (!(await fs.pathExists(manifestPath))) return null;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const manifest = await fs.readJson(manifestPath);
|
|
277
|
+
const gitRepo = getGitRemote(marketplacePath);
|
|
278
|
+
|
|
279
|
+
const plugins: LocalMarketplacePlugin[] = [];
|
|
280
|
+
const validManifestPlugins: typeof manifest.plugins = [];
|
|
281
|
+
let manifestModified = false;
|
|
282
|
+
|
|
283
|
+
if (manifest.plugins && Array.isArray(manifest.plugins)) {
|
|
284
|
+
for (const plugin of manifest.plugins) {
|
|
285
|
+
// Try to scan plugin directory for component counts
|
|
286
|
+
let agents: string[] = [];
|
|
287
|
+
let commands: string[] = [];
|
|
288
|
+
let skills: string[] = [];
|
|
289
|
+
let mcpServers: string[] = [];
|
|
290
|
+
|
|
291
|
+
// Handle both string sources (local paths) and object sources (remote URLs)
|
|
292
|
+
const sourceStr =
|
|
293
|
+
typeof plugin.source === "string" ? plugin.source : null;
|
|
294
|
+
|
|
295
|
+
if (sourceStr) {
|
|
296
|
+
const pluginPath = path.join(
|
|
297
|
+
marketplacePath,
|
|
298
|
+
sourceStr.replace("./", ""),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Remove plugins whose source directory doesn't exist
|
|
302
|
+
if (!(await fs.pathExists(pluginPath))) {
|
|
303
|
+
manifestModified = true;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Scan for agents
|
|
309
|
+
const agentsDir = path.join(pluginPath, "agents");
|
|
310
|
+
if (await fs.pathExists(agentsDir)) {
|
|
311
|
+
const agentFiles = await fs.readdir(agentsDir);
|
|
312
|
+
agents = agentFiles
|
|
313
|
+
.filter((f) => f.endsWith(".md"))
|
|
314
|
+
.map((f) => f.replace(".md", ""));
|
|
315
|
+
}
|
|
316
|
+
// Scan for commands
|
|
317
|
+
const commandsDir = path.join(pluginPath, "commands");
|
|
318
|
+
if (await fs.pathExists(commandsDir)) {
|
|
319
|
+
const cmdFiles = await fs.readdir(commandsDir);
|
|
320
|
+
commands = cmdFiles
|
|
321
|
+
.filter((f) => f.endsWith(".md"))
|
|
322
|
+
.map((f) => f.replace(".md", ""));
|
|
323
|
+
}
|
|
324
|
+
// Scan for skills
|
|
325
|
+
skills = await scanSkillDirs(path.join(pluginPath, "skills"));
|
|
326
|
+
// Scan for MCP servers
|
|
327
|
+
mcpServers = await scanMcpServers(pluginPath);
|
|
328
|
+
} catch {
|
|
329
|
+
// Ignore scan errors
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
validManifestPlugins.push(plugin);
|
|
334
|
+
plugins.push({
|
|
335
|
+
name: plugin.name,
|
|
336
|
+
version: plugin.version || "0.0.0",
|
|
337
|
+
description: plugin.description || "",
|
|
338
|
+
source: plugin.source,
|
|
339
|
+
category: plugin.category,
|
|
340
|
+
author: plugin.author,
|
|
341
|
+
releases: normalizeReleases(plugin.releases),
|
|
342
|
+
strict: plugin.strict,
|
|
343
|
+
lspServers: plugin.lspServers,
|
|
344
|
+
agents,
|
|
345
|
+
commands,
|
|
346
|
+
skills,
|
|
347
|
+
mcpServers,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Update marketplace.json if we removed invalid plugins
|
|
353
|
+
if (manifestModified) {
|
|
354
|
+
manifest.plugins = validManifestPlugins;
|
|
355
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
name: manifest.name || marketplaceName,
|
|
360
|
+
description: manifest.description || manifest.metadata?.description || "",
|
|
361
|
+
plugins,
|
|
362
|
+
gitRepo,
|
|
363
|
+
};
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Scan local marketplace cache and return marketplace info
|
|
371
|
+
* Includes both:
|
|
372
|
+
* - Marketplaces in ~/.claude/plugins/marketplaces/
|
|
373
|
+
* - Directory-based marketplaces from known_marketplaces.json
|
|
374
|
+
*/
|
|
375
|
+
export async function scanLocalMarketplaces(): Promise<
|
|
376
|
+
Map<string, LocalMarketplace>
|
|
377
|
+
> {
|
|
378
|
+
const marketplaces = new Map<string, LocalMarketplace>();
|
|
379
|
+
|
|
380
|
+
// A directory is not a marketplace; a REGISTRY ENTRY is. Null means the
|
|
381
|
+
// registry could not be read, and then every directory is kept — see
|
|
382
|
+
// `registeredDirNames`.
|
|
383
|
+
//
|
|
384
|
+
// Claude Code refreshes a marketplace by delete-then-clone and leaves
|
|
385
|
+
// `<name>.bak` behind. An unfiltered scan treated that backup as a real
|
|
386
|
+
// marketplace, which cost the reporter twice: `magus.bak` shadowed the real
|
|
387
|
+
// `magus` in `getAllMarketplaces()` (deduped by repo, first-come) so the real
|
|
388
|
+
// one vanished from the plugin list entirely, and later it became the target
|
|
389
|
+
// of a nonsense label — `kanban@magus … moved → magus.bak`. `claude plugin
|
|
390
|
+
// marketplace list` never showed it, because Claude Code reads the registry.
|
|
391
|
+
const registered = await registeredDirNames();
|
|
392
|
+
|
|
393
|
+
// 1. Scan marketplaces from the standard directory
|
|
394
|
+
if (await fs.pathExists(CLAUDE_PLUGINS_DIR)) {
|
|
395
|
+
try {
|
|
396
|
+
const entries = await fs.readdir(CLAUDE_PLUGINS_DIR, {
|
|
397
|
+
withFileTypes: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
for (const entry of entries) {
|
|
401
|
+
if (!entry.isDirectory()) continue;
|
|
402
|
+
if (registered && !registered.has(entry.name)) continue;
|
|
403
|
+
|
|
404
|
+
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, entry.name);
|
|
405
|
+
const marketplace = await scanSingleMarketplace(
|
|
406
|
+
marketplacePath,
|
|
407
|
+
entry.name,
|
|
408
|
+
);
|
|
409
|
+
if (marketplace) {
|
|
410
|
+
marketplaces.set(entry.name, marketplace);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// Ignore read errors
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 2. Add directory-based marketplaces from known_marketplaces.json
|
|
419
|
+
try {
|
|
420
|
+
const known = await readKnownMarketplaces();
|
|
421
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
422
|
+
// Only add if it's a directory source AND not already in the map
|
|
423
|
+
if (
|
|
424
|
+
entry.source.source === "directory" &&
|
|
425
|
+
entry.installLocation &&
|
|
426
|
+
!marketplaces.has(name)
|
|
427
|
+
) {
|
|
428
|
+
if (await fs.pathExists(entry.installLocation)) {
|
|
429
|
+
const marketplace = await scanSingleMarketplace(
|
|
430
|
+
entry.installLocation,
|
|
431
|
+
name,
|
|
432
|
+
);
|
|
433
|
+
if (marketplace) {
|
|
434
|
+
marketplaces.set(name, marketplace);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
// Ignore errors reading known_marketplaces
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return marketplaces;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get a specific local marketplace by name
|
|
448
|
+
*/
|
|
449
|
+
export async function getLocalMarketplace(
|
|
450
|
+
name: string,
|
|
451
|
+
): Promise<LocalMarketplace | undefined> {
|
|
452
|
+
const marketplaces = await scanLocalMarketplaces();
|
|
453
|
+
return marketplaces.get(name);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Check if a marketplace exists in local cache
|
|
458
|
+
*/
|
|
459
|
+
export async function hasLocalMarketplace(name: string): Promise<boolean> {
|
|
460
|
+
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, name);
|
|
461
|
+
const manifestPath = path.join(
|
|
462
|
+
marketplacePath,
|
|
463
|
+
".claude-plugin",
|
|
464
|
+
"marketplace.json",
|
|
465
|
+
);
|
|
466
|
+
return fs.pathExists(manifestPath);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface RefreshResult {
|
|
470
|
+
name: string;
|
|
471
|
+
success: boolean;
|
|
472
|
+
updated: boolean;
|
|
473
|
+
error?: string;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export interface RefreshProgress {
|
|
477
|
+
current: number;
|
|
478
|
+
total: number;
|
|
479
|
+
name: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export type ProgressCallback = (progress: RefreshProgress) => void;
|
|
483
|
+
|
|
484
|
+
export interface RepairResult {
|
|
485
|
+
repaired: boolean;
|
|
486
|
+
pluginName: string;
|
|
487
|
+
added: {
|
|
488
|
+
agents: string[];
|
|
489
|
+
commands: string[];
|
|
490
|
+
skills: string[];
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Repair a plugin's plugin.json by adding missing agents/commands/skills arrays
|
|
496
|
+
* based on what files exist on disk.
|
|
497
|
+
*
|
|
498
|
+
* This fixes plugins where the author forgot to add the arrays to plugin.json,
|
|
499
|
+
* causing Claude Code to not load the agents/commands even though files exist.
|
|
500
|
+
*/
|
|
501
|
+
export async function repairPluginJson(
|
|
502
|
+
pluginPath: string,
|
|
503
|
+
): Promise<RepairResult> {
|
|
504
|
+
const pluginJsonPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
|
|
505
|
+
const pluginName = path.basename(pluginPath);
|
|
506
|
+
|
|
507
|
+
const result: RepairResult = {
|
|
508
|
+
repaired: false,
|
|
509
|
+
pluginName,
|
|
510
|
+
added: { agents: [], commands: [], skills: [] },
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
if (!(await fs.pathExists(pluginJsonPath))) {
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const pluginJson = await fs.readJson(pluginJsonPath);
|
|
519
|
+
let modified = false;
|
|
520
|
+
|
|
521
|
+
// Scan and add agents if array is missing
|
|
522
|
+
const agentsDir = path.join(pluginPath, "agents");
|
|
523
|
+
if (await fs.pathExists(agentsDir)) {
|
|
524
|
+
const files = (await fs.readdir(agentsDir))
|
|
525
|
+
.filter((f) => f.endsWith(".md"))
|
|
526
|
+
.map((f) => `./agents/${f}`);
|
|
527
|
+
|
|
528
|
+
if (files.length > 0 && !pluginJson.agents) {
|
|
529
|
+
pluginJson.agents = files;
|
|
530
|
+
result.added.agents = files;
|
|
531
|
+
modified = true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Scan and add commands if array is missing
|
|
536
|
+
const commandsDir = path.join(pluginPath, "commands");
|
|
537
|
+
if (await fs.pathExists(commandsDir)) {
|
|
538
|
+
const files = (await fs.readdir(commandsDir))
|
|
539
|
+
.filter((f) => f.endsWith(".md"))
|
|
540
|
+
.map((f) => `./commands/${f}`);
|
|
541
|
+
|
|
542
|
+
if (files.length > 0 && !pluginJson.commands) {
|
|
543
|
+
pluginJson.commands = files;
|
|
544
|
+
result.added.commands = files;
|
|
545
|
+
modified = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Scan and add skills if array is missing
|
|
550
|
+
const skillsDir = path.join(pluginPath, "skills");
|
|
551
|
+
if (await fs.pathExists(skillsDir)) {
|
|
552
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
553
|
+
const skills = entries
|
|
554
|
+
.filter((e) => e.isDirectory() || e.name.endsWith(".md"))
|
|
555
|
+
.map((e) => `./skills/${e.name}`);
|
|
556
|
+
|
|
557
|
+
if (skills.length > 0 && !pluginJson.skills) {
|
|
558
|
+
pluginJson.skills = skills;
|
|
559
|
+
result.added.skills = skills;
|
|
560
|
+
modified = true;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (modified) {
|
|
565
|
+
await fs.writeJson(pluginJsonPath, pluginJson, { spaces: 2 });
|
|
566
|
+
result.repaired = true;
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
// Ignore repair errors for individual plugins
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export interface RepairMarketplaceResult {
|
|
576
|
+
marketplace: string;
|
|
577
|
+
repaired: RepairResult[];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Repair all plugins in a marketplace
|
|
582
|
+
*/
|
|
583
|
+
export async function repairMarketplacePlugins(
|
|
584
|
+
marketplaceName: string,
|
|
585
|
+
): Promise<RepairMarketplaceResult> {
|
|
586
|
+
const result: RepairMarketplaceResult = {
|
|
587
|
+
marketplace: marketplaceName,
|
|
588
|
+
repaired: [],
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
|
|
592
|
+
const manifestPath = path.join(
|
|
593
|
+
marketplacePath,
|
|
594
|
+
".claude-plugin",
|
|
595
|
+
"marketplace.json",
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (!(await fs.pathExists(manifestPath))) {
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const manifest = await fs.readJson(manifestPath);
|
|
604
|
+
|
|
605
|
+
if (manifest.plugins && Array.isArray(manifest.plugins)) {
|
|
606
|
+
for (const plugin of manifest.plugins) {
|
|
607
|
+
if (plugin.source) {
|
|
608
|
+
const pluginPath = path.join(
|
|
609
|
+
marketplacePath,
|
|
610
|
+
plugin.source.replace("./", ""),
|
|
611
|
+
);
|
|
612
|
+
const repairResult = await repairPluginJson(pluginPath);
|
|
613
|
+
|
|
614
|
+
if (repairResult.repaired) {
|
|
615
|
+
result.repaired.push(repairResult);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
// Ignore errors
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Repair all plugins in all marketplaces
|
|
629
|
+
*/
|
|
630
|
+
export async function repairAllMarketplaces(): Promise<
|
|
631
|
+
RepairMarketplaceResult[]
|
|
632
|
+
> {
|
|
633
|
+
const results: RepairMarketplaceResult[] = [];
|
|
634
|
+
|
|
635
|
+
if (!(await fs.pathExists(CLAUDE_PLUGINS_DIR))) {
|
|
636
|
+
return results;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
const entries = await fs.readdir(CLAUDE_PLUGINS_DIR, {
|
|
641
|
+
withFileTypes: true,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
for (const entry of entries) {
|
|
645
|
+
if (!entry.isDirectory()) continue;
|
|
646
|
+
|
|
647
|
+
const repairResult = await repairMarketplacePlugins(entry.name);
|
|
648
|
+
if (repairResult.repaired.length > 0) {
|
|
649
|
+
results.push(repairResult);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
// Return empty on error
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return results;
|
|
657
|
+
}
|