claudeup 4.18.0 → 4.22.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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +92 -0
- package/src/__tests__/alias-shell-writer.test.ts +7 -0
- package/src/__tests__/alias-store.test.ts +77 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +118 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/alias-flags.ts +11 -1
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/alias-shell-writer.ts +382 -8
- package/src/services/alias-store.ts +60 -0
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +16 -11
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/screens/AliasScreen.tsx +491 -359
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/PluginsScreen.tsx +3 -1
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +6 -102
- package/src/ui/state/types.ts +8 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -196
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -764
- package/src/services/alias-store.js +0 -77
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -444
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -346
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1008
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -766
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -543
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
import fs from "fs-extra";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
const CLAUDE_PLUGINS_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
6
|
-
const KNOWN_MARKETPLACES_FILE = path.join(os.homedir(), ".claude", "plugins", "known_marketplaces.json");
|
|
7
|
-
/**
|
|
8
|
-
* Read known_marketplaces.json (Claude Code's internal marketplace tracking)
|
|
9
|
-
*/
|
|
10
|
-
async function readKnownMarketplaces() {
|
|
11
|
-
try {
|
|
12
|
-
if (await fs.pathExists(KNOWN_MARKETPLACES_FILE)) {
|
|
13
|
-
return await fs.readJson(KNOWN_MARKETPLACES_FILE);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// Return empty if can't read
|
|
18
|
-
}
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Get git remote URL from a marketplace directory
|
|
23
|
-
*/
|
|
24
|
-
function getGitRemote(marketplacePath) {
|
|
25
|
-
try {
|
|
26
|
-
const gitDir = path.join(marketplacePath, ".git");
|
|
27
|
-
if (!fs.existsSync(gitDir))
|
|
28
|
-
return undefined;
|
|
29
|
-
const result = execSync("git remote get-url origin", {
|
|
30
|
-
cwd: marketplacePath,
|
|
31
|
-
encoding: "utf-8",
|
|
32
|
-
timeout: 5000,
|
|
33
|
-
}).trim();
|
|
34
|
-
// Convert SSH URL to HTTPS format: git@github.com:user/repo.git -> user/repo
|
|
35
|
-
if (result.startsWith("git@github.com:")) {
|
|
36
|
-
return result.replace("git@github.com:", "").replace(/\.git$/, "");
|
|
37
|
-
}
|
|
38
|
-
// HTTPS URL: https://github.com/user/repo -> user/repo
|
|
39
|
-
if (result.includes("github.com")) {
|
|
40
|
-
const match = result.match(/github\.com[/:]([^/]+\/[^/\s.]+)/);
|
|
41
|
-
if (match)
|
|
42
|
-
return match[1].replace(/\.git$/, "");
|
|
43
|
-
}
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Scan a single marketplace directory and return marketplace info
|
|
52
|
-
*/
|
|
53
|
-
async function scanSingleMarketplace(marketplacePath, marketplaceName) {
|
|
54
|
-
const manifestPath = path.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
55
|
-
if (!(await fs.pathExists(manifestPath)))
|
|
56
|
-
return null;
|
|
57
|
-
try {
|
|
58
|
-
const manifest = await fs.readJson(manifestPath);
|
|
59
|
-
const gitRepo = getGitRemote(marketplacePath);
|
|
60
|
-
const plugins = [];
|
|
61
|
-
const validManifestPlugins = [];
|
|
62
|
-
let manifestModified = false;
|
|
63
|
-
if (manifest.plugins && Array.isArray(manifest.plugins)) {
|
|
64
|
-
for (const plugin of manifest.plugins) {
|
|
65
|
-
// Try to scan plugin directory for component counts
|
|
66
|
-
let agents = [];
|
|
67
|
-
let commands = [];
|
|
68
|
-
let skills = [];
|
|
69
|
-
let mcpServers = [];
|
|
70
|
-
// Handle both string sources (local paths) and object sources (remote URLs)
|
|
71
|
-
const sourceStr = typeof plugin.source === "string" ? plugin.source : null;
|
|
72
|
-
if (sourceStr) {
|
|
73
|
-
const pluginPath = path.join(marketplacePath, sourceStr.replace("./", ""));
|
|
74
|
-
// Remove plugins whose source directory doesn't exist
|
|
75
|
-
if (!(await fs.pathExists(pluginPath))) {
|
|
76
|
-
manifestModified = true;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
// Scan for agents
|
|
81
|
-
const agentsDir = path.join(pluginPath, "agents");
|
|
82
|
-
if (await fs.pathExists(agentsDir)) {
|
|
83
|
-
const agentFiles = await fs.readdir(agentsDir);
|
|
84
|
-
agents = agentFiles
|
|
85
|
-
.filter((f) => f.endsWith(".md"))
|
|
86
|
-
.map((f) => f.replace(".md", ""));
|
|
87
|
-
}
|
|
88
|
-
// Scan for commands
|
|
89
|
-
const commandsDir = path.join(pluginPath, "commands");
|
|
90
|
-
if (await fs.pathExists(commandsDir)) {
|
|
91
|
-
const cmdFiles = await fs.readdir(commandsDir);
|
|
92
|
-
commands = cmdFiles
|
|
93
|
-
.filter((f) => f.endsWith(".md"))
|
|
94
|
-
.map((f) => f.replace(".md", ""));
|
|
95
|
-
}
|
|
96
|
-
// Scan for skills
|
|
97
|
-
const skillsDir = path.join(pluginPath, "skills");
|
|
98
|
-
if (await fs.pathExists(skillsDir)) {
|
|
99
|
-
const skillFiles = await fs.readdir(skillsDir);
|
|
100
|
-
skills = skillFiles.filter((f) => f.endsWith(".md") ||
|
|
101
|
-
fs.statSync(path.join(skillsDir, f)).isDirectory());
|
|
102
|
-
}
|
|
103
|
-
// Scan for MCP servers
|
|
104
|
-
const mcpDir = path.join(pluginPath, "mcp-servers");
|
|
105
|
-
if (await fs.pathExists(mcpDir)) {
|
|
106
|
-
const mcpFiles = await fs.readdir(mcpDir);
|
|
107
|
-
mcpServers = mcpFiles
|
|
108
|
-
.filter((f) => f.endsWith(".json"))
|
|
109
|
-
.map((f) => f.replace(".json", ""));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// Ignore scan errors
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
validManifestPlugins.push(plugin);
|
|
117
|
-
plugins.push({
|
|
118
|
-
name: plugin.name,
|
|
119
|
-
version: plugin.version || "0.0.0",
|
|
120
|
-
description: plugin.description || "",
|
|
121
|
-
source: plugin.source,
|
|
122
|
-
category: plugin.category,
|
|
123
|
-
author: plugin.author,
|
|
124
|
-
strict: plugin.strict,
|
|
125
|
-
lspServers: plugin.lspServers,
|
|
126
|
-
agents,
|
|
127
|
-
commands,
|
|
128
|
-
skills,
|
|
129
|
-
mcpServers,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Update marketplace.json if we removed invalid plugins
|
|
134
|
-
if (manifestModified) {
|
|
135
|
-
manifest.plugins = validManifestPlugins;
|
|
136
|
-
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
name: manifest.name || marketplaceName,
|
|
140
|
-
description: manifest.description || manifest.metadata?.description || "",
|
|
141
|
-
plugins,
|
|
142
|
-
gitRepo,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Scan local marketplace cache and return marketplace info
|
|
151
|
-
* Includes both:
|
|
152
|
-
* - Marketplaces in ~/.claude/plugins/marketplaces/
|
|
153
|
-
* - Directory-based marketplaces from known_marketplaces.json
|
|
154
|
-
*/
|
|
155
|
-
export async function scanLocalMarketplaces() {
|
|
156
|
-
const marketplaces = new Map();
|
|
157
|
-
// 1. Scan marketplaces from the standard directory
|
|
158
|
-
if (await fs.pathExists(CLAUDE_PLUGINS_DIR)) {
|
|
159
|
-
try {
|
|
160
|
-
const entries = await fs.readdir(CLAUDE_PLUGINS_DIR, {
|
|
161
|
-
withFileTypes: true,
|
|
162
|
-
});
|
|
163
|
-
for (const entry of entries) {
|
|
164
|
-
if (!entry.isDirectory())
|
|
165
|
-
continue;
|
|
166
|
-
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, entry.name);
|
|
167
|
-
const marketplace = await scanSingleMarketplace(marketplacePath, entry.name);
|
|
168
|
-
if (marketplace) {
|
|
169
|
-
marketplaces.set(entry.name, marketplace);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// Ignore read errors
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// 2. Add directory-based marketplaces from known_marketplaces.json
|
|
178
|
-
try {
|
|
179
|
-
const known = await readKnownMarketplaces();
|
|
180
|
-
for (const [name, entry] of Object.entries(known)) {
|
|
181
|
-
// Only add if it's a directory source AND not already in the map
|
|
182
|
-
if (entry.source.source === "directory" &&
|
|
183
|
-
entry.installLocation &&
|
|
184
|
-
!marketplaces.has(name)) {
|
|
185
|
-
if (await fs.pathExists(entry.installLocation)) {
|
|
186
|
-
const marketplace = await scanSingleMarketplace(entry.installLocation, name);
|
|
187
|
-
if (marketplace) {
|
|
188
|
-
marketplaces.set(name, marketplace);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// Ignore errors reading known_marketplaces
|
|
196
|
-
}
|
|
197
|
-
return marketplaces;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Get a specific local marketplace by name
|
|
201
|
-
*/
|
|
202
|
-
export async function getLocalMarketplace(name) {
|
|
203
|
-
const marketplaces = await scanLocalMarketplaces();
|
|
204
|
-
return marketplaces.get(name);
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Check if a marketplace exists in local cache
|
|
208
|
-
*/
|
|
209
|
-
export async function hasLocalMarketplace(name) {
|
|
210
|
-
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, name);
|
|
211
|
-
const manifestPath = path.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
212
|
-
return fs.pathExists(manifestPath);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Repair a plugin's plugin.json by adding missing agents/commands/skills arrays
|
|
216
|
-
* based on what files exist on disk.
|
|
217
|
-
*
|
|
218
|
-
* This fixes plugins where the author forgot to add the arrays to plugin.json,
|
|
219
|
-
* causing Claude Code to not load the agents/commands even though files exist.
|
|
220
|
-
*/
|
|
221
|
-
export async function repairPluginJson(pluginPath) {
|
|
222
|
-
const pluginJsonPath = path.join(pluginPath, ".claude-plugin", "plugin.json");
|
|
223
|
-
const pluginName = path.basename(pluginPath);
|
|
224
|
-
const result = {
|
|
225
|
-
repaired: false,
|
|
226
|
-
pluginName,
|
|
227
|
-
added: { agents: [], commands: [], skills: [] },
|
|
228
|
-
};
|
|
229
|
-
if (!(await fs.pathExists(pluginJsonPath))) {
|
|
230
|
-
return result;
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
const pluginJson = await fs.readJson(pluginJsonPath);
|
|
234
|
-
let modified = false;
|
|
235
|
-
// Scan and add agents if array is missing
|
|
236
|
-
const agentsDir = path.join(pluginPath, "agents");
|
|
237
|
-
if (await fs.pathExists(agentsDir)) {
|
|
238
|
-
const files = (await fs.readdir(agentsDir))
|
|
239
|
-
.filter((f) => f.endsWith(".md"))
|
|
240
|
-
.map((f) => `./agents/${f}`);
|
|
241
|
-
if (files.length > 0 && !pluginJson.agents) {
|
|
242
|
-
pluginJson.agents = files;
|
|
243
|
-
result.added.agents = files;
|
|
244
|
-
modified = true;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Scan and add commands if array is missing
|
|
248
|
-
const commandsDir = path.join(pluginPath, "commands");
|
|
249
|
-
if (await fs.pathExists(commandsDir)) {
|
|
250
|
-
const files = (await fs.readdir(commandsDir))
|
|
251
|
-
.filter((f) => f.endsWith(".md"))
|
|
252
|
-
.map((f) => `./commands/${f}`);
|
|
253
|
-
if (files.length > 0 && !pluginJson.commands) {
|
|
254
|
-
pluginJson.commands = files;
|
|
255
|
-
result.added.commands = files;
|
|
256
|
-
modified = true;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
// Scan and add skills if array is missing
|
|
260
|
-
const skillsDir = path.join(pluginPath, "skills");
|
|
261
|
-
if (await fs.pathExists(skillsDir)) {
|
|
262
|
-
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
263
|
-
const skills = entries
|
|
264
|
-
.filter((e) => e.isDirectory() || e.name.endsWith(".md"))
|
|
265
|
-
.map((e) => `./skills/${e.name}`);
|
|
266
|
-
if (skills.length > 0 && !pluginJson.skills) {
|
|
267
|
-
pluginJson.skills = skills;
|
|
268
|
-
result.added.skills = skills;
|
|
269
|
-
modified = true;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (modified) {
|
|
273
|
-
await fs.writeJson(pluginJsonPath, pluginJson, { spaces: 2 });
|
|
274
|
-
result.repaired = true;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
// Ignore repair errors for individual plugins
|
|
279
|
-
}
|
|
280
|
-
return result;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Repair all plugins in a marketplace
|
|
284
|
-
*/
|
|
285
|
-
export async function repairMarketplacePlugins(marketplaceName) {
|
|
286
|
-
const result = {
|
|
287
|
-
marketplace: marketplaceName,
|
|
288
|
-
repaired: [],
|
|
289
|
-
};
|
|
290
|
-
const marketplacePath = path.join(CLAUDE_PLUGINS_DIR, marketplaceName);
|
|
291
|
-
const manifestPath = path.join(marketplacePath, ".claude-plugin", "marketplace.json");
|
|
292
|
-
if (!(await fs.pathExists(manifestPath))) {
|
|
293
|
-
return result;
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
const manifest = await fs.readJson(manifestPath);
|
|
297
|
-
if (manifest.plugins && Array.isArray(manifest.plugins)) {
|
|
298
|
-
for (const plugin of manifest.plugins) {
|
|
299
|
-
if (plugin.source) {
|
|
300
|
-
const pluginPath = path.join(marketplacePath, plugin.source.replace("./", ""));
|
|
301
|
-
const repairResult = await repairPluginJson(pluginPath);
|
|
302
|
-
if (repairResult.repaired) {
|
|
303
|
-
result.repaired.push(repairResult);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// Ignore errors
|
|
311
|
-
}
|
|
312
|
-
return result;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Repair all plugins in all marketplaces
|
|
316
|
-
*/
|
|
317
|
-
export async function repairAllMarketplaces() {
|
|
318
|
-
const results = [];
|
|
319
|
-
if (!(await fs.pathExists(CLAUDE_PLUGINS_DIR))) {
|
|
320
|
-
return results;
|
|
321
|
-
}
|
|
322
|
-
try {
|
|
323
|
-
const entries = await fs.readdir(CLAUDE_PLUGINS_DIR, {
|
|
324
|
-
withFileTypes: true,
|
|
325
|
-
});
|
|
326
|
-
for (const entry of entries) {
|
|
327
|
-
if (!entry.isDirectory())
|
|
328
|
-
continue;
|
|
329
|
-
const repairResult = await repairMarketplacePlugins(entry.name);
|
|
330
|
-
if (repairResult.repaired.length > 0) {
|
|
331
|
-
results.push(repairResult);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
// Return empty on error
|
|
337
|
-
}
|
|
338
|
-
return results;
|
|
339
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Marketplace.json fetcher with offline fallback.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `plugin-manager.ts` so the fetch + fallback logic can be
|
|
5
|
-
* unit-tested without dragging in claude-settings.ts's module-level
|
|
6
|
-
* `os.homedir()` constants (which contaminate the test suite when
|
|
7
|
-
* imported via plugin-manager.ts).
|
|
8
|
-
*/
|
|
9
|
-
import { isValidGitHubRepo } from "../utils/string-utils.js";
|
|
10
|
-
// Session-level cache for fetched marketplace data (no TTL - persists until explicit refresh)
|
|
11
|
-
const marketplaceCache = new Map();
|
|
12
|
-
export function clearMarketplaceCache() {
|
|
13
|
-
marketplaceCache.clear();
|
|
14
|
-
}
|
|
15
|
-
export async function fetchMarketplacePlugins(marketplaceName, repo) {
|
|
16
|
-
// Check cache first - session-level, no TTL
|
|
17
|
-
const cached = marketplaceCache.get(marketplaceName);
|
|
18
|
-
if (cached) {
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
|
-
// Validate repo format to prevent SSRF
|
|
22
|
-
if (!isValidGitHubRepo(repo)) {
|
|
23
|
-
console.error(`Invalid GitHub repo format: ${repo}`);
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
// Fetch marketplace.json from GitHub
|
|
28
|
-
const url = `https://raw.githubusercontent.com/${repo}/main/.claude-plugin/marketplace.json`;
|
|
29
|
-
const response = await fetch(url, {
|
|
30
|
-
signal: AbortSignal.timeout(10000), // 10s timeout
|
|
31
|
-
});
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
console.error(`Failed to fetch marketplace: ${response.status}`);
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
// Validate content-type
|
|
37
|
-
const contentType = response.headers.get("content-type");
|
|
38
|
-
if (contentType &&
|
|
39
|
-
!contentType.includes("application/json") &&
|
|
40
|
-
!contentType.includes("text/plain")) {
|
|
41
|
-
console.error(`Invalid content-type for marketplace: ${contentType}`);
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
const data = (await response.json());
|
|
45
|
-
const plugins = [];
|
|
46
|
-
if (data.plugins && Array.isArray(data.plugins)) {
|
|
47
|
-
for (const plugin of data.plugins) {
|
|
48
|
-
plugins.push({
|
|
49
|
-
name: plugin.name,
|
|
50
|
-
version: plugin.version || null,
|
|
51
|
-
description: plugin.description || "",
|
|
52
|
-
category: plugin.category,
|
|
53
|
-
author: plugin.author,
|
|
54
|
-
homepage: plugin.homepage,
|
|
55
|
-
tags: plugin.tags,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Cache the result (session-level)
|
|
60
|
-
marketplaceCache.set(marketplaceName, plugins);
|
|
61
|
-
return plugins;
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
console.error(`Error fetching marketplace ${marketplaceName}:`, error);
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolve marketplace plugins with offline fallback.
|
|
70
|
-
*
|
|
71
|
-
* Calls `fetchMarketplacePlugins` (which hits GitHub) and, when it returns
|
|
72
|
-
* empty AND a local marketplace cache exists for the same name, falls back
|
|
73
|
-
* to the cached plugin manifest. This prevents every installed plugin from
|
|
74
|
-
* being flagged `isOrphaned: true` (and rendered "deprecated") whenever the
|
|
75
|
-
* machine can't reach `raw.githubusercontent.com` — the local cache already
|
|
76
|
-
* has the data, it just wasn't being consulted as a fallback.
|
|
77
|
-
*
|
|
78
|
-
* The two shapes are nearly identical; we drop a couple of LocalMarketplacePlugin
|
|
79
|
-
* fields (lspServers, agents, etc.) that MarketplacePlugin doesn't carry
|
|
80
|
-
* since the caller doesn't read them at this layer.
|
|
81
|
-
*/
|
|
82
|
-
export async function resolveMarketplacePlugins(mpName, repo, localMarketplaces) {
|
|
83
|
-
const remote = await fetchMarketplacePlugins(mpName, repo);
|
|
84
|
-
if (remote.length > 0)
|
|
85
|
-
return remote;
|
|
86
|
-
const local = localMarketplaces.get(mpName);
|
|
87
|
-
if (!local || local.plugins.length === 0)
|
|
88
|
-
return remote;
|
|
89
|
-
return local.plugins.map((p) => ({
|
|
90
|
-
name: p.name,
|
|
91
|
-
version: p.version,
|
|
92
|
-
description: p.description,
|
|
93
|
-
category: p.category,
|
|
94
|
-
author: p.author,
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Marketplace sync service
|
|
3
|
-
*
|
|
4
|
-
* Auto-registers marketplaces that are referenced by `enabledPlugins`
|
|
5
|
-
* in any settings scope (global, project, local) but are not installed
|
|
6
|
-
* locally. Fixes the "new employee clones repo with committed .claude/
|
|
7
|
-
* settings but has never run `claude plugin marketplace add`" scenario.
|
|
8
|
-
*
|
|
9
|
-
* IMPORTANT: Only uses `marketplace add` (never `marketplace update`).
|
|
10
|
-
* Claude Code's `marketplace update` calls cacheMarketplaceFromGit()
|
|
11
|
-
* which deletes the marketplace directory before re-cloning. If the
|
|
12
|
-
* clone fails (network, auth, timeout), the directory stays permanently
|
|
13
|
-
* deleted. See ai-docs/plugin-marketplace-bug-investigation.md.
|
|
14
|
-
*/
|
|
15
|
-
import fs from "fs-extra";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import os from "node:os";
|
|
18
|
-
import { getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, isMarketplaceRegistered, } from "./claude-settings.js";
|
|
19
|
-
import { parsePluginId } from "../utils/string-utils.js";
|
|
20
|
-
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
21
|
-
import { addMarketplace } from "./claude-cli.js";
|
|
22
|
-
const MARKETPLACES_DIR = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
23
|
-
/**
|
|
24
|
-
* Collect unique marketplace names from enabled plugins across all settings
|
|
25
|
-
* scopes. When `projectPath` is provided, project and local scopes are
|
|
26
|
-
* included; otherwise only the global scope is scanned.
|
|
27
|
-
*/
|
|
28
|
-
export async function getReferencedMarketplaces(projectPath) {
|
|
29
|
-
const marketplaceNames = new Set();
|
|
30
|
-
const allPluginIds = new Set();
|
|
31
|
-
try {
|
|
32
|
-
const global = await getGlobalEnabledPlugins();
|
|
33
|
-
for (const id of Object.keys(global))
|
|
34
|
-
allPluginIds.add(id);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
/* skip if unreadable */
|
|
38
|
-
}
|
|
39
|
-
if (projectPath) {
|
|
40
|
-
try {
|
|
41
|
-
const project = await getEnabledPlugins(projectPath);
|
|
42
|
-
for (const id of Object.keys(project))
|
|
43
|
-
allPluginIds.add(id);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
/* skip if unreadable */
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const local = await getLocalEnabledPlugins(projectPath);
|
|
50
|
-
for (const id of Object.keys(local))
|
|
51
|
-
allPluginIds.add(id);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
/* skip if unreadable */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
for (const pluginId of allPluginIds) {
|
|
58
|
-
const parsed = parsePluginId(pluginId);
|
|
59
|
-
if (parsed)
|
|
60
|
-
marketplaceNames.add(parsed.marketplace);
|
|
61
|
-
}
|
|
62
|
-
return marketplaceNames;
|
|
63
|
-
}
|
|
64
|
-
/** Re-exported for convenience — canonical implementation lives in claude-settings. */
|
|
65
|
-
export { isMarketplaceRegistered };
|
|
66
|
-
/**
|
|
67
|
-
* Register any referenced marketplaces that are missing locally.
|
|
68
|
-
* Looks up the GitHub repo in `defaultMarketplaces` and calls
|
|
69
|
-
* `claude plugin marketplace add` for each. Always writes to the global
|
|
70
|
-
* registry (`~/.claude/plugins/known_marketplaces.json`) — scope only
|
|
71
|
-
* controls which settings are scanned to build the reference list.
|
|
72
|
-
*
|
|
73
|
-
* Returns the names of marketplaces that were successfully added.
|
|
74
|
-
*/
|
|
75
|
-
export async function autoAddMissingMarketplaces(projectPath) {
|
|
76
|
-
const referenced = await getReferencedMarketplaces(projectPath);
|
|
77
|
-
const added = [];
|
|
78
|
-
for (const mpName of referenced) {
|
|
79
|
-
const mpDir = path.join(MARKETPLACES_DIR, mpName);
|
|
80
|
-
if (await fs.pathExists(mpDir))
|
|
81
|
-
continue;
|
|
82
|
-
const defaultMp = defaultMarketplaces.find((m) => m.name === mpName);
|
|
83
|
-
if (!defaultMp?.source.repo)
|
|
84
|
-
continue;
|
|
85
|
-
try {
|
|
86
|
-
await addMarketplace(defaultMp.source.repo);
|
|
87
|
-
added.push(mpName);
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.warn(`⚠ Failed to auto-add marketplace ${mpName}:`, error instanceof Error ? error.message : "Unknown error");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return added;
|
|
94
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Official Model Context Protocol Registry
|
|
2
|
-
// API Docs: https://registry.modelcontextprotocol.io
|
|
3
|
-
const MCP_REGISTRY_API = "https://registry.modelcontextprotocol.io/v0";
|
|
4
|
-
export async function searchMcpServers(options = {}) {
|
|
5
|
-
const { query = "", limit = 20, cursor } = options;
|
|
6
|
-
const params = new URLSearchParams();
|
|
7
|
-
if (query)
|
|
8
|
-
params.set("query", query);
|
|
9
|
-
params.set("limit", String(limit));
|
|
10
|
-
if (cursor)
|
|
11
|
-
params.set("cursor", cursor);
|
|
12
|
-
const url = `${MCP_REGISTRY_API}/servers?${params.toString()}`;
|
|
13
|
-
const response = await fetch(url, {
|
|
14
|
-
headers: {
|
|
15
|
-
Accept: "application/json",
|
|
16
|
-
},
|
|
17
|
-
});
|
|
18
|
-
if (!response.ok) {
|
|
19
|
-
throw new Error(`MCP Registry API error: ${response.status} ${response.statusText}`);
|
|
20
|
-
}
|
|
21
|
-
const data = await response.json();
|
|
22
|
-
// The official registry returns data in a nested format:
|
|
23
|
-
// { servers: [{ server: {...}, _meta: {...} }] }
|
|
24
|
-
// Convert it to match our expected McpRegistryResponse format
|
|
25
|
-
const servers = (data.servers || [])
|
|
26
|
-
.map((item) => {
|
|
27
|
-
const server = item.server || item; // Handle both nested and flat structures
|
|
28
|
-
const meta = item._meta?.["io.modelcontextprotocol.registry/official"];
|
|
29
|
-
// Get URL from remotes (HTTP) or construct from packages
|
|
30
|
-
let url = "";
|
|
31
|
-
if (server.remotes?.length > 0) {
|
|
32
|
-
url = server.remotes[0].url;
|
|
33
|
-
}
|
|
34
|
-
else if (server.packages?.length > 0) {
|
|
35
|
-
// For package-based servers, use the package identifier as reference
|
|
36
|
-
const pkg = server.packages[0];
|
|
37
|
-
url = `${pkg.registryType}:${pkg.identifier}`;
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
name: server.name,
|
|
41
|
-
url,
|
|
42
|
-
short_description: server.description || "No description",
|
|
43
|
-
version: server.version,
|
|
44
|
-
source_code_url: server.repository?.url,
|
|
45
|
-
package_registry: server.packages?.[0]?.registryType,
|
|
46
|
-
published_at: meta?.publishedAt,
|
|
47
|
-
};
|
|
48
|
-
})
|
|
49
|
-
// Filter out servers without name or URL
|
|
50
|
-
.filter((s) => s.name && s.url)
|
|
51
|
-
// Sort by publish date (newest first)
|
|
52
|
-
.sort((a, b) => {
|
|
53
|
-
if (!a.published_at)
|
|
54
|
-
return 1;
|
|
55
|
-
if (!b.published_at)
|
|
56
|
-
return -1;
|
|
57
|
-
return (new Date(b.published_at).getTime() - new Date(a.published_at).getTime());
|
|
58
|
-
});
|
|
59
|
-
return {
|
|
60
|
-
servers,
|
|
61
|
-
next_cursor: data.metadata?.nextCursor || data.next_cursor,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export async function getPopularServers(limit = 20) {
|
|
65
|
-
const response = await searchMcpServers({ limit });
|
|
66
|
-
// Already sorted by publish date in searchMcpServers
|
|
67
|
-
return response.servers;
|
|
68
|
-
}
|
|
69
|
-
export function formatDate(dateStr) {
|
|
70
|
-
if (!dateStr)
|
|
71
|
-
return "";
|
|
72
|
-
const date = new Date(dateStr);
|
|
73
|
-
const now = new Date();
|
|
74
|
-
const diffMs = now.getTime() - date.getTime();
|
|
75
|
-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
76
|
-
if (diffDays === 0)
|
|
77
|
-
return "today";
|
|
78
|
-
if (diffDays === 1)
|
|
79
|
-
return "yesterday";
|
|
80
|
-
if (diffDays < 7)
|
|
81
|
-
return `${diffDays}d ago`;
|
|
82
|
-
if (diffDays < 30)
|
|
83
|
-
return `${Math.floor(diffDays / 7)}w ago`;
|
|
84
|
-
if (diffDays < 365)
|
|
85
|
-
return `${Math.floor(diffDays / 30)}mo ago`;
|
|
86
|
-
return `${Math.floor(diffDays / 365)}y ago`;
|
|
87
|
-
}
|