agentinit 1.12.1 → 1.13.1
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/CHANGELOG.md +22 -0
- package/README.md +11 -1
- package/dist/cli.js +1050 -12659
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +239 -79
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +93 -88
- package/dist/commands/skills.js.map +1 -1
- package/dist/core/marketplaceRegistry.d.ts.map +1 -1
- package/dist/core/marketplaceRegistry.js +7 -0
- package/dist/core/marketplaceRegistry.js.map +1 -1
- package/dist/core/pluginManager.d.ts +39 -1
- package/dist/core/pluginManager.d.ts.map +1 -1
- package/dist/core/pluginManager.js +472 -41
- package/dist/core/pluginManager.js.map +1 -1
- package/dist/core/skillsManager.d.ts +1 -0
- package/dist/core/skillsManager.d.ts.map +1 -1
- package/dist/core/skillsManager.js +35 -6
- package/dist/core/skillsManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +23 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, join, basename } from 'path';
|
|
1
|
+
import { resolve, join, basename, relative, dirname } from 'path';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import matter from 'gray-matter';
|
|
@@ -7,6 +7,24 @@ import { AgentManager } from './agentManager.js';
|
|
|
7
7
|
import { MCPFilter } from './mcpFilter.js';
|
|
8
8
|
import { MARKETPLACES, getMarketplace as lookupMarketplace, getMarketplaceIds as lookupMarketplaceIds } from './marketplaceRegistry.js';
|
|
9
9
|
import { SkillsManager } from './skillsManager.js';
|
|
10
|
+
export class MarketplacePluginNotFoundError extends Error {
|
|
11
|
+
pluginName;
|
|
12
|
+
marketplaceId;
|
|
13
|
+
marketplaceName;
|
|
14
|
+
suggestions;
|
|
15
|
+
constructor(pluginName, marketplaceId, marketplaceName, suggestions) {
|
|
16
|
+
let msg = `Plugin "${pluginName}" not found in ${marketplaceName} marketplace.`;
|
|
17
|
+
if (suggestions.length > 0) {
|
|
18
|
+
msg += ` Did you mean: ${suggestions.join(', ')}?`;
|
|
19
|
+
}
|
|
20
|
+
super(msg);
|
|
21
|
+
this.name = 'MarketplacePluginNotFoundError';
|
|
22
|
+
this.pluginName = pluginName;
|
|
23
|
+
this.marketplaceId = marketplaceId;
|
|
24
|
+
this.marketplaceName = marketplaceName;
|
|
25
|
+
this.suggestions = suggestions;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
10
28
|
function getMarketplaceCacheDir(registryId) {
|
|
11
29
|
return join(homedir(), '.agentinit', 'marketplace-cache', registryId);
|
|
12
30
|
}
|
|
@@ -16,13 +34,390 @@ function getRegistryPath(projectPath, global) {
|
|
|
16
34
|
}
|
|
17
35
|
return join(projectPath, '.agentinit', 'plugins.json');
|
|
18
36
|
}
|
|
37
|
+
function getClaudeInstalledPluginsPath() {
|
|
38
|
+
return join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
39
|
+
}
|
|
19
40
|
export class PluginManager {
|
|
20
41
|
agentManager;
|
|
21
42
|
skillsManager;
|
|
43
|
+
preparedInstallContexts = new Map();
|
|
22
44
|
constructor(agentManager) {
|
|
23
45
|
this.agentManager = agentManager || new AgentManager();
|
|
24
46
|
this.skillsManager = new SkillsManager(this.agentManager);
|
|
25
47
|
}
|
|
48
|
+
getPreparedInstallKey(source, from) {
|
|
49
|
+
return `${from || ''}\n${source}`;
|
|
50
|
+
}
|
|
51
|
+
async cleanupLoadedPluginContext(context) {
|
|
52
|
+
if (context?.tempDir) {
|
|
53
|
+
await fs.rm(context.tempDir, { recursive: true, force: true }).catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async storePreparedInstallContext(source, from, context) {
|
|
57
|
+
const key = this.getPreparedInstallKey(source, from);
|
|
58
|
+
const existing = this.preparedInstallContexts.get(key);
|
|
59
|
+
this.preparedInstallContexts.set(key, context);
|
|
60
|
+
if (existing && existing !== context) {
|
|
61
|
+
await this.cleanupLoadedPluginContext(existing);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
takePreparedInstallContext(source, from) {
|
|
65
|
+
const key = this.getPreparedInstallKey(source, from);
|
|
66
|
+
const existing = this.preparedInstallContexts.get(key) || null;
|
|
67
|
+
if (existing) {
|
|
68
|
+
this.preparedInstallContexts.delete(key);
|
|
69
|
+
}
|
|
70
|
+
return existing;
|
|
71
|
+
}
|
|
72
|
+
async discardPreparedPlugin(source, options = {}) {
|
|
73
|
+
const context = this.takePreparedInstallContext(source, options.from);
|
|
74
|
+
await this.cleanupLoadedPluginContext(context);
|
|
75
|
+
}
|
|
76
|
+
parseGitHubRepo(url) {
|
|
77
|
+
const normalized = url.replace(/\.git$/, '');
|
|
78
|
+
const match = normalized.match(/github\.com[:/]([^/]+)\/([^/]+)/i);
|
|
79
|
+
if (!match) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const [, owner, repo] = match;
|
|
83
|
+
if (!owner || !repo) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
deriveGitHubFallbackSource(source, marketplaceId) {
|
|
92
|
+
const repoShorthandMatch = source.match(/^([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)$/);
|
|
93
|
+
if (repoShorthandMatch) {
|
|
94
|
+
const [, owner, repo] = repoShorthandMatch;
|
|
95
|
+
return {
|
|
96
|
+
type: 'github',
|
|
97
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
98
|
+
owner,
|
|
99
|
+
repo,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!marketplaceId || !/^[a-zA-Z0-9._-]+$/.test(source)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const registry = this.getMarketplace(marketplaceId);
|
|
106
|
+
const registryRepo = registry ? this.parseGitHubRepo(registry.repoUrl) : null;
|
|
107
|
+
if (!registryRepo) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
type: 'github',
|
|
112
|
+
url: `https://github.com/${registryRepo.owner}/${source}.git`,
|
|
113
|
+
owner: registryRepo.owner,
|
|
114
|
+
repo: source,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
formatGitHubRepoUrl(source) {
|
|
118
|
+
if (!source.owner || !source.repo) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return `https://github.com/${source.owner}/${source.repo}`;
|
|
122
|
+
}
|
|
123
|
+
async resolvePreparedPluginDir(pluginDir, source) {
|
|
124
|
+
const claudeMarketplaceManifestPath = join(pluginDir, '.claude-plugin', 'marketplace.json');
|
|
125
|
+
if (!(await fileExists(claudeMarketplaceManifestPath))) {
|
|
126
|
+
return { pluginDir, warnings: [] };
|
|
127
|
+
}
|
|
128
|
+
const manifestContent = await readFileIfExists(claudeMarketplaceManifestPath);
|
|
129
|
+
if (!manifestContent) {
|
|
130
|
+
return { pluginDir, warnings: [] };
|
|
131
|
+
}
|
|
132
|
+
let manifest;
|
|
133
|
+
try {
|
|
134
|
+
manifest = JSON.parse(manifestContent);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new Error(`Invalid .claude-plugin/marketplace.json in ${pluginDir}`);
|
|
138
|
+
}
|
|
139
|
+
const entries = Array.isArray(manifest.plugins)
|
|
140
|
+
? manifest.plugins.filter((entry) => typeof entry?.name === 'string' && typeof entry?.source === 'string')
|
|
141
|
+
: [];
|
|
142
|
+
if (entries.length === 0) {
|
|
143
|
+
throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
|
|
144
|
+
}
|
|
145
|
+
const [firstEntry] = entries;
|
|
146
|
+
if (!firstEntry) {
|
|
147
|
+
throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
|
|
148
|
+
}
|
|
149
|
+
let selectedEntry = firstEntry;
|
|
150
|
+
if (entries.length > 1) {
|
|
151
|
+
const candidates = new Set([source.pluginName, source.repo]
|
|
152
|
+
.filter((value) => !!value)
|
|
153
|
+
.flatMap(value => [value, basename(value)]));
|
|
154
|
+
const matched = entries.find(entry => candidates.has(entry.name) || candidates.has(basename(entry.source)));
|
|
155
|
+
if (!matched) {
|
|
156
|
+
throw new Error(`Repository "${pluginDir}" is a Claude marketplace bundle with multiple plugins. Select one of: ${entries.map(entry => entry.name).join(', ')}.`);
|
|
157
|
+
}
|
|
158
|
+
selectedEntry = matched;
|
|
159
|
+
}
|
|
160
|
+
const selectedPluginDir = resolve(pluginDir, selectedEntry.source);
|
|
161
|
+
const relativePath = relative(resolve(pluginDir), selectedPluginDir);
|
|
162
|
+
if (relativePath.startsWith('..') ||
|
|
163
|
+
relativePath.includes('/../') ||
|
|
164
|
+
relativePath.includes('\\..\\')) {
|
|
165
|
+
throw new Error(`Invalid bundled plugin source path "${selectedEntry.source}" in ${claudeMarketplaceManifestPath}`);
|
|
166
|
+
}
|
|
167
|
+
if (!(await isDirectory(selectedPluginDir))) {
|
|
168
|
+
throw new Error(`Bundled plugin path not found: ${selectedPluginDir}`);
|
|
169
|
+
}
|
|
170
|
+
const sourceLabel = this.formatGitHubRepoUrl(source) || pluginDir;
|
|
171
|
+
return {
|
|
172
|
+
pluginDir: selectedPluginDir,
|
|
173
|
+
warnings: [
|
|
174
|
+
`Source "${sourceLabel}" is a Claude Code marketplace bundle; using bundled plugin "${selectedEntry.name}".`,
|
|
175
|
+
],
|
|
176
|
+
claudeBundle: {
|
|
177
|
+
bundleName: manifest.name || selectedEntry.name,
|
|
178
|
+
pluginName: selectedEntry.name,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async loadPluginFromDirectory(pluginDir, source, warnings = []) {
|
|
183
|
+
const preparedPlugin = await this.resolvePreparedPluginDir(pluginDir, source);
|
|
184
|
+
const parsedPlugin = await this.parsePlugin(preparedPlugin.pluginDir, source);
|
|
185
|
+
return {
|
|
186
|
+
...parsedPlugin,
|
|
187
|
+
warnings: [...warnings, ...preparedPlugin.warnings, ...parsedPlugin.warnings],
|
|
188
|
+
resolvedPluginDir: preparedPlugin.pluginDir,
|
|
189
|
+
...(preparedPlugin.claudeBundle ? { nativeClaudeBundle: preparedPlugin.claudeBundle } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async loadPluginContext(source, options = {}) {
|
|
193
|
+
const resolved = this.resolveSource(source, { from: options.from });
|
|
194
|
+
let effectiveSource = resolved;
|
|
195
|
+
let pluginDir;
|
|
196
|
+
let tempDir = null;
|
|
197
|
+
const resolutionWarnings = [];
|
|
198
|
+
if (resolved.type === 'marketplace') {
|
|
199
|
+
try {
|
|
200
|
+
pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (!(error instanceof MarketplacePluginNotFoundError)) {
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const fallbackSource = this.deriveGitHubFallbackSource(source, resolved.marketplace);
|
|
207
|
+
if (!fallbackSource || !fallbackSource.url) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const fallbackUrl = this.formatGitHubRepoUrl(fallbackSource) || fallbackSource.url.replace(/\.git$/, '');
|
|
211
|
+
resolutionWarnings.push(error.message);
|
|
212
|
+
resolutionWarnings.push(`Marketplace lookup failed; trying unverified GitHub repository ${fallbackUrl} instead.`);
|
|
213
|
+
try {
|
|
214
|
+
tempDir = await this.skillsManager.cloneRepo(fallbackSource.url);
|
|
215
|
+
}
|
|
216
|
+
catch (fallbackError) {
|
|
217
|
+
throw new Error(`${error.message} Tried unverified GitHub repository ${fallbackUrl} but failed: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}`);
|
|
218
|
+
}
|
|
219
|
+
effectiveSource = {
|
|
220
|
+
...fallbackSource,
|
|
221
|
+
...(resolved.pluginName ? { pluginName: resolved.pluginName } : {}),
|
|
222
|
+
};
|
|
223
|
+
pluginDir = tempDir;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (resolved.type === 'github') {
|
|
227
|
+
if (!resolved.url)
|
|
228
|
+
throw new Error(`Invalid source: ${source}`);
|
|
229
|
+
tempDir = await this.skillsManager.cloneRepo(resolved.url);
|
|
230
|
+
pluginDir = tempDir;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
pluginDir = resolve(resolved.path || source);
|
|
234
|
+
if (!(await fileExists(pluginDir))) {
|
|
235
|
+
throw new Error(`Local path not found: ${pluginDir}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
plugin: await this.loadPluginFromDirectory(pluginDir, effectiveSource, resolutionWarnings),
|
|
240
|
+
effectiveSource,
|
|
241
|
+
tempDir,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async buildPluginInspection(context) {
|
|
245
|
+
const nativeTarget = await this.getClaudeNativeInstallTarget(context.plugin, context.plugin.resolvedPluginDir);
|
|
246
|
+
return {
|
|
247
|
+
plugin: context.plugin,
|
|
248
|
+
nativePreview: nativeTarget ? this.toNativePluginPreview(nativeTarget) : null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async preparePluginInstall(source, options = {}) {
|
|
252
|
+
const context = await this.loadPluginContext(source, options);
|
|
253
|
+
try {
|
|
254
|
+
const inspection = await this.buildPluginInspection(context);
|
|
255
|
+
await this.storePreparedInstallContext(source, options.from, context);
|
|
256
|
+
return inspection;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
await this.cleanupLoadedPluginContext(context);
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async inspectPlugin(source, options = {}) {
|
|
264
|
+
const context = await this.loadPluginContext(source, options);
|
|
265
|
+
try {
|
|
266
|
+
return await this.buildPluginInspection(context);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await this.cleanupLoadedPluginContext(context);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
slugifyPluginNamespace(value) {
|
|
273
|
+
return value
|
|
274
|
+
.trim()
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
277
|
+
.replace(/^-+|-+$/g, '') || 'plugin';
|
|
278
|
+
}
|
|
279
|
+
async getClaudeNativeFeatureKinds(pluginDir, manifest) {
|
|
280
|
+
const featureChecks = await Promise.all([
|
|
281
|
+
(async () => !!manifest.commands || await isDirectory(join(pluginDir, 'commands')))(),
|
|
282
|
+
(async () => !!manifest.hooks || await isDirectory(join(pluginDir, 'hooks')))(),
|
|
283
|
+
(async () => !!manifest.agents || await isDirectory(join(pluginDir, 'agents')))(),
|
|
284
|
+
isDirectory(join(pluginDir, 'prompts')),
|
|
285
|
+
isDirectory(join(pluginDir, 'schemas')),
|
|
286
|
+
isDirectory(join(pluginDir, 'scripts')),
|
|
287
|
+
isDirectory(join(pluginDir, 'templates')),
|
|
288
|
+
]);
|
|
289
|
+
return [
|
|
290
|
+
...(featureChecks[0] ? ['commands'] : []),
|
|
291
|
+
...(featureChecks[1] ? ['hooks'] : []),
|
|
292
|
+
...(featureChecks[2] ? ['agents'] : []),
|
|
293
|
+
...(featureChecks[3] ? ['prompts'] : []),
|
|
294
|
+
...(featureChecks[4] ? ['schemas'] : []),
|
|
295
|
+
...(featureChecks[5] ? ['scripts'] : []),
|
|
296
|
+
...(featureChecks[6] ? ['templates'] : []),
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
async getClaudeNativeInstallTarget(plugin, pluginDir) {
|
|
300
|
+
if (plugin.format !== 'claude') {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const manifestContent = await readFileIfExists(join(pluginDir, '.claude-plugin', 'plugin.json'));
|
|
304
|
+
if (!manifestContent) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
let manifest;
|
|
308
|
+
try {
|
|
309
|
+
manifest = JSON.parse(manifestContent);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const features = await this.getClaudeNativeFeatureKinds(pluginDir, manifest);
|
|
315
|
+
if (features.length === 0) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const baseNamespace = plugin.nativeClaudeBundle?.bundleName
|
|
319
|
+
|| plugin.source.marketplace
|
|
320
|
+
|| (plugin.source.owner && plugin.source.repo ? `${plugin.source.owner}-${plugin.source.repo}` : plugin.name);
|
|
321
|
+
const namespace = `agentinit-${this.slugifyPluginNamespace(baseNamespace)}`;
|
|
322
|
+
const versionDir = this.slugifyPluginNamespace(plugin.version || '0.0.0');
|
|
323
|
+
return {
|
|
324
|
+
namespace,
|
|
325
|
+
pluginKey: `${plugin.name}@${namespace}`,
|
|
326
|
+
installPath: join(homedir(), '.claude', 'plugins', 'cache', namespace, plugin.name, versionDir),
|
|
327
|
+
features,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async readClaudeInstalledPlugins() {
|
|
331
|
+
const path = getClaudeInstalledPluginsPath();
|
|
332
|
+
const content = await readFileIfExists(path);
|
|
333
|
+
if (!content) {
|
|
334
|
+
return { version: 2, plugins: {} };
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(content);
|
|
338
|
+
if (!parsed || parsed.version !== 2 || typeof parsed.plugins !== 'object' || parsed.plugins === null) {
|
|
339
|
+
return { version: 2, plugins: {} };
|
|
340
|
+
}
|
|
341
|
+
return parsed;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return { version: 2, plugins: {} };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async saveClaudeInstalledPlugins(state) {
|
|
348
|
+
await writeFile(getClaudeInstalledPluginsPath(), JSON.stringify(state, null, 2));
|
|
349
|
+
}
|
|
350
|
+
async installNativeClaudePlugin(plugin, pluginDir, agents) {
|
|
351
|
+
const installed = [];
|
|
352
|
+
const skipped = [];
|
|
353
|
+
const warnings = [];
|
|
354
|
+
const nativeTarget = await this.getClaudeNativeInstallTarget(plugin, pluginDir);
|
|
355
|
+
if (!nativeTarget) {
|
|
356
|
+
return { installed, skipped, warnings };
|
|
357
|
+
}
|
|
358
|
+
const hasClaudeTarget = agents.some(agent => agent.id === 'claude');
|
|
359
|
+
const featureLabel = nativeTarget.features.join(', ');
|
|
360
|
+
if (!hasClaudeTarget) {
|
|
361
|
+
skipped.push({
|
|
362
|
+
agent: 'claude',
|
|
363
|
+
reason: `Claude Code was not selected; skipped native plugin components (${featureLabel}).`,
|
|
364
|
+
});
|
|
365
|
+
warnings.push(`Claude Code-native plugin components detected (${featureLabel}), but no Claude Code target was selected; skipped native install.`);
|
|
366
|
+
return { installed, skipped, warnings };
|
|
367
|
+
}
|
|
368
|
+
warnings.push(`Claude Code-native plugin components detected (${featureLabel}); they will only work in Claude Code and install into ~/.claude/plugins.`);
|
|
369
|
+
const claudeInstalled = await this.readClaudeInstalledPlugins();
|
|
370
|
+
const conflictingKey = Object.keys(claudeInstalled.plugins).find(key => key !== nativeTarget.pluginKey && key.startsWith(`${plugin.name}@`));
|
|
371
|
+
if (conflictingKey) {
|
|
372
|
+
skipped.push({
|
|
373
|
+
agent: 'claude',
|
|
374
|
+
reason: `Claude plugin "${plugin.name}" is already installed as ${conflictingKey}; skipped native install to avoid duplicates.`,
|
|
375
|
+
});
|
|
376
|
+
warnings.push(`Skipped native Claude plugin install because Claude already has "${plugin.name}" installed as ${conflictingKey}.`);
|
|
377
|
+
return { installed, skipped, warnings };
|
|
378
|
+
}
|
|
379
|
+
await fs.rm(nativeTarget.installPath, { recursive: true, force: true }).catch(() => { });
|
|
380
|
+
await fs.mkdir(dirname(nativeTarget.installPath), { recursive: true });
|
|
381
|
+
await fs.cp(pluginDir, nativeTarget.installPath, { recursive: true, dereference: true });
|
|
382
|
+
const now = new Date().toISOString();
|
|
383
|
+
claudeInstalled.plugins[nativeTarget.pluginKey] = [{
|
|
384
|
+
scope: 'user',
|
|
385
|
+
installPath: nativeTarget.installPath,
|
|
386
|
+
version: plugin.version,
|
|
387
|
+
installedAt: now,
|
|
388
|
+
lastUpdated: now,
|
|
389
|
+
}];
|
|
390
|
+
await this.saveClaudeInstalledPlugins(claudeInstalled);
|
|
391
|
+
installed.push({
|
|
392
|
+
agent: 'claude',
|
|
393
|
+
pluginKey: nativeTarget.pluginKey,
|
|
394
|
+
installPath: nativeTarget.installPath,
|
|
395
|
+
});
|
|
396
|
+
warnings.push('Reload plugins in Claude Code with /reload-plugins to activate native plugin components.');
|
|
397
|
+
return { installed, skipped, warnings };
|
|
398
|
+
}
|
|
399
|
+
toNativePluginPreview(target) {
|
|
400
|
+
return {
|
|
401
|
+
agent: 'claude',
|
|
402
|
+
pluginKey: target.pluginKey,
|
|
403
|
+
installPath: target.installPath,
|
|
404
|
+
features: target.features,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async removeNativeClaudePlugin(component) {
|
|
408
|
+
const claudeInstalled = await this.readClaudeInstalledPlugins();
|
|
409
|
+
const entries = claudeInstalled.plugins[component.pluginKey] || [];
|
|
410
|
+
const remainingEntries = entries.filter(entry => entry.installPath !== component.installPath);
|
|
411
|
+
if (remainingEntries.length > 0) {
|
|
412
|
+
claudeInstalled.plugins[component.pluginKey] = remainingEntries;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
delete claudeInstalled.plugins[component.pluginKey];
|
|
416
|
+
}
|
|
417
|
+
await this.saveClaudeInstalledPlugins(claudeInstalled);
|
|
418
|
+
await fs.rm(component.installPath, { recursive: true, force: true }).catch(() => { });
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
26
421
|
// ── Source Resolution ──────────────────────────────────────────────
|
|
27
422
|
/**
|
|
28
423
|
* Resolve a source string into a PluginSource.
|
|
@@ -182,11 +577,7 @@ export class PluginManager {
|
|
|
182
577
|
.filter(p => p.name.includes(name) || name.includes(p.name))
|
|
183
578
|
.map(p => p.name)
|
|
184
579
|
.slice(0, 5);
|
|
185
|
-
|
|
186
|
-
if (suggestions.length > 0) {
|
|
187
|
-
msg += ` Did you mean: ${suggestions.join(', ')}?`;
|
|
188
|
-
}
|
|
189
|
-
throw new Error(msg);
|
|
580
|
+
throw new MarketplacePluginNotFoundError(name, registryId, registry.name, suggestions);
|
|
190
581
|
}
|
|
191
582
|
/**
|
|
192
583
|
* List all plugins in a marketplace, optionally filtered
|
|
@@ -201,7 +592,13 @@ export class PluginManager {
|
|
|
201
592
|
const fullDir = join(cacheDir, dir);
|
|
202
593
|
if (!(await isDirectory(fullDir)))
|
|
203
594
|
continue;
|
|
204
|
-
const cat = dir === 'plugins'
|
|
595
|
+
const cat = dir === 'plugins'
|
|
596
|
+
? 'official'
|
|
597
|
+
: dir === 'external_plugins'
|
|
598
|
+
? 'community'
|
|
599
|
+
: dir.startsWith('skills/.')
|
|
600
|
+
? dir.slice('skills/.'.length)
|
|
601
|
+
: dir;
|
|
205
602
|
if (category && cat !== category)
|
|
206
603
|
continue;
|
|
207
604
|
const entries = await listFiles(fullDir);
|
|
@@ -312,10 +709,10 @@ export class PluginManager {
|
|
|
312
709
|
const mcpServers = await this.parseMcpJson(pluginDir);
|
|
313
710
|
// Warn about agent-specific features
|
|
314
711
|
if (await isDirectory(join(pluginDir, 'hooks')) || manifest.hooks) {
|
|
315
|
-
warnings.push('Hooks (hooks/) are Claude Code-specific
|
|
712
|
+
warnings.push('Hooks (hooks/) are Claude Code-specific');
|
|
316
713
|
}
|
|
317
714
|
if (await isDirectory(join(pluginDir, 'agents')) || manifest.agents) {
|
|
318
|
-
warnings.push('Agent definitions (agents/) are Claude Code-specific
|
|
715
|
+
warnings.push('Agent definitions (agents/) are Claude Code-specific');
|
|
319
716
|
}
|
|
320
717
|
return {
|
|
321
718
|
name: manifest.name,
|
|
@@ -499,34 +896,18 @@ ${body.trim()}
|
|
|
499
896
|
* This is the main one-liner entry point.
|
|
500
897
|
*/
|
|
501
898
|
async installPlugin(source, projectPath, options = {}) {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
let tempDir = null;
|
|
505
|
-
// 1. Resolve source to a local directory
|
|
506
|
-
if (resolved.type === 'marketplace') {
|
|
507
|
-
pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
|
|
508
|
-
}
|
|
509
|
-
else if (resolved.type === 'github') {
|
|
510
|
-
if (!resolved.url)
|
|
511
|
-
throw new Error(`Invalid source: ${source}`);
|
|
512
|
-
tempDir = await this.skillsManager.cloneRepo(resolved.url);
|
|
513
|
-
pluginDir = tempDir;
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
pluginDir = resolve(resolved.path || source);
|
|
517
|
-
if (!(await fileExists(pluginDir))) {
|
|
518
|
-
throw new Error(`Local path not found: ${pluginDir}`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
899
|
+
const context = this.takePreparedInstallContext(source, options.from)
|
|
900
|
+
|| await this.loadPluginContext(source, { from: options.from });
|
|
521
901
|
try {
|
|
522
902
|
// 2. Parse plugin
|
|
523
|
-
const plugin =
|
|
903
|
+
const plugin = context.plugin;
|
|
524
904
|
// 3. If --list, return early with contents
|
|
525
905
|
if (options.list) {
|
|
526
906
|
return {
|
|
527
907
|
plugin,
|
|
528
908
|
skills: { installed: [], skipped: [] },
|
|
529
909
|
mcpServers: { applied: [], skipped: [] },
|
|
910
|
+
nativePlugins: { installed: [], skipped: [] },
|
|
530
911
|
warnings: plugin.warnings,
|
|
531
912
|
};
|
|
532
913
|
}
|
|
@@ -537,6 +918,7 @@ ${body.trim()}
|
|
|
537
918
|
plugin,
|
|
538
919
|
skills: { installed: [], skipped: plugin.skills.map(s => ({ name: s.name, reason: 'No target agents found' })) },
|
|
539
920
|
mcpServers: { applied: [], skipped: plugin.mcpServers.map(s => ({ name: s.name, reason: 'No target agents found' })) },
|
|
921
|
+
nativePlugins: { installed: [], skipped: [] },
|
|
540
922
|
warnings: plugin.warnings,
|
|
541
923
|
};
|
|
542
924
|
}
|
|
@@ -544,21 +926,25 @@ ${body.trim()}
|
|
|
544
926
|
const skillResult = await this.installPluginSkills(plugin, projectPath, agents, options);
|
|
545
927
|
// 6. Apply MCP servers per agent
|
|
546
928
|
const mcpResult = await this.applyPluginMcpServers(plugin, projectPath, agents, options.global);
|
|
547
|
-
// 7.
|
|
548
|
-
|
|
929
|
+
// 7. Install agent-native plugin payloads when supported.
|
|
930
|
+
const nativePluginResult = await this.installNativeClaudePlugin(plugin, plugin.resolvedPluginDir, agents);
|
|
931
|
+
const installWarnings = [...plugin.warnings, ...nativePluginResult.warnings];
|
|
932
|
+
// 8. Save to registry only when the install actually applied components.
|
|
933
|
+
if (skillResult.installed.length > 0 || mcpResult.applied.length > 0 || nativePluginResult.installed.length > 0) {
|
|
549
934
|
const installed = {
|
|
550
935
|
name: plugin.name,
|
|
551
936
|
version: plugin.version,
|
|
552
937
|
description: plugin.description,
|
|
553
|
-
source:
|
|
938
|
+
source: context.effectiveSource,
|
|
554
939
|
format: plugin.format,
|
|
555
940
|
installedAt: new Date().toISOString(),
|
|
556
941
|
scope: options.global ? 'global' : 'project',
|
|
557
942
|
components: {
|
|
558
943
|
skills: skillResult.installed,
|
|
559
944
|
mcpServers: mcpResult.applied,
|
|
945
|
+
nativePlugins: nativePluginResult.installed,
|
|
560
946
|
},
|
|
561
|
-
warnings:
|
|
947
|
+
warnings: installWarnings,
|
|
562
948
|
};
|
|
563
949
|
await this.addToRegistry(installed, projectPath, options.global);
|
|
564
950
|
}
|
|
@@ -566,13 +952,12 @@ ${body.trim()}
|
|
|
566
952
|
plugin,
|
|
567
953
|
skills: skillResult,
|
|
568
954
|
mcpServers: mcpResult,
|
|
569
|
-
|
|
955
|
+
nativePlugins: nativePluginResult,
|
|
956
|
+
warnings: installWarnings,
|
|
570
957
|
};
|
|
571
958
|
}
|
|
572
959
|
finally {
|
|
573
|
-
|
|
574
|
-
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
575
|
-
}
|
|
960
|
+
await this.cleanupLoadedPluginContext(context);
|
|
576
961
|
}
|
|
577
962
|
}
|
|
578
963
|
/**
|
|
@@ -765,7 +1150,8 @@ ${body.trim()}
|
|
|
765
1150
|
if (options.agents && options.agents.length > 0) {
|
|
766
1151
|
const agentSet = new Set(options.agents);
|
|
767
1152
|
plugins = plugins.filter(p => p.components.skills.some(s => agentSet.has(s.agent)) ||
|
|
768
|
-
p.components.mcpServers.some(m => agentSet.has(m.agent))
|
|
1153
|
+
p.components.mcpServers.some(m => agentSet.has(m.agent)) ||
|
|
1154
|
+
(p.components.nativePlugins || []).some(nativePlugin => agentSet.has(nativePlugin.agent)));
|
|
769
1155
|
}
|
|
770
1156
|
return plugins;
|
|
771
1157
|
}
|
|
@@ -778,7 +1164,8 @@ ${body.trim()}
|
|
|
778
1164
|
if (!plugin) {
|
|
779
1165
|
return { removed: false, details: [`Plugin "${name}" not found in registry`] };
|
|
780
1166
|
}
|
|
781
|
-
|
|
1167
|
+
const pluginNativeComponents = plugin.components.nativePlugins || [];
|
|
1168
|
+
if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0 && pluginNativeComponents.length === 0) {
|
|
782
1169
|
registry.plugins = registry.plugins.filter(p => p.name !== name);
|
|
783
1170
|
await this.saveRegistry(registry, projectPath, options.global);
|
|
784
1171
|
return {
|
|
@@ -877,7 +1264,48 @@ ${body.trim()}
|
|
|
877
1264
|
...retainedMcpServers,
|
|
878
1265
|
...targetedMcpServers.filter(mcp => !removedMcpKeys.has(`${mcp.agent}:${mcp.name}`)),
|
|
879
1266
|
];
|
|
880
|
-
|
|
1267
|
+
const targetedNativePlugins = agentFilter
|
|
1268
|
+
? pluginNativeComponents.filter(nativePlugin => agentFilter.has(nativePlugin.agent))
|
|
1269
|
+
: pluginNativeComponents;
|
|
1270
|
+
const retainedNativePlugins = agentFilter
|
|
1271
|
+
? pluginNativeComponents.filter(nativePlugin => !agentFilter.has(nativePlugin.agent))
|
|
1272
|
+
: [];
|
|
1273
|
+
const otherScopeRegistry = await this.getRegistry(projectPath, !options.global);
|
|
1274
|
+
const otherRegistryNativePlugins = otherScopeRegistry.plugins
|
|
1275
|
+
.flatMap(entry => entry.components.nativePlugins || []);
|
|
1276
|
+
const otherPluginNativePlugins = registry.plugins
|
|
1277
|
+
.filter(entry => entry.name !== plugin.name)
|
|
1278
|
+
.flatMap(entry => entry.components.nativePlugins || []);
|
|
1279
|
+
const remainingNativeRefs = [
|
|
1280
|
+
...retainedNativePlugins,
|
|
1281
|
+
...otherPluginNativePlugins,
|
|
1282
|
+
...otherRegistryNativePlugins,
|
|
1283
|
+
];
|
|
1284
|
+
const removedNativeKeys = new Set();
|
|
1285
|
+
for (const nativePlugin of targetedNativePlugins) {
|
|
1286
|
+
const nativeKey = `${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`;
|
|
1287
|
+
if (removedNativeKeys.has(nativeKey)) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const sharedNativeInstall = remainingNativeRefs.some(other => other.installPath === nativePlugin.installPath || other.pluginKey === nativePlugin.pluginKey);
|
|
1291
|
+
if (sharedNativeInstall) {
|
|
1292
|
+
details.push(`Skipped shared native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
await this.removeNativeClaudePlugin(nativePlugin);
|
|
1297
|
+
removedNativeKeys.add(nativeKey);
|
|
1298
|
+
details.push(`Removed native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
details.push(`Could not remove native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const remainingNativePlugins = [
|
|
1305
|
+
...retainedNativePlugins,
|
|
1306
|
+
...targetedNativePlugins.filter(nativePlugin => !removedNativeKeys.has(`${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`)),
|
|
1307
|
+
];
|
|
1308
|
+
if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0 && removedNativeKeys.size === 0) {
|
|
881
1309
|
if (agentFilter) {
|
|
882
1310
|
details.push(`No removable plugin components matched the requested agents for "${name}"`);
|
|
883
1311
|
}
|
|
@@ -888,10 +1316,13 @@ ${body.trim()}
|
|
|
888
1316
|
components: {
|
|
889
1317
|
skills: remainingSkills,
|
|
890
1318
|
mcpServers: remainingMcpServers,
|
|
1319
|
+
nativePlugins: remainingNativePlugins,
|
|
891
1320
|
},
|
|
892
1321
|
};
|
|
893
1322
|
registry.plugins = registry.plugins.filter(p => p.name !== name);
|
|
894
|
-
if (updatedPlugin.components.skills.length > 0 ||
|
|
1323
|
+
if (updatedPlugin.components.skills.length > 0 ||
|
|
1324
|
+
updatedPlugin.components.mcpServers.length > 0 ||
|
|
1325
|
+
(updatedPlugin.components.nativePlugins || []).length > 0) {
|
|
895
1326
|
registry.plugins.push(updatedPlugin);
|
|
896
1327
|
details.push('Updated plugin registry');
|
|
897
1328
|
}
|