agentinit 1.12.0 → 1.13.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/CHANGELOG.md +21 -0
- package/README.md +11 -1
- package/dist/cli.js +552 -172
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +15 -2
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +99 -96
- 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 +27 -0
- package/dist/core/pluginManager.d.ts.map +1 -1
- package/dist/core/pluginManager.js +376 -20
- 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/plugins.d.ts +13 -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,6 +34,9 @@ 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;
|
|
@@ -23,6 +44,259 @@ export class PluginManager {
|
|
|
23
44
|
this.agentManager = agentManager || new AgentManager();
|
|
24
45
|
this.skillsManager = new SkillsManager(this.agentManager);
|
|
25
46
|
}
|
|
47
|
+
parseGitHubRepo(url) {
|
|
48
|
+
const normalized = url.replace(/\.git$/, '');
|
|
49
|
+
const match = normalized.match(/github\.com[:/]([^/]+)\/([^/]+)/i);
|
|
50
|
+
if (!match) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const [, owner, repo] = match;
|
|
54
|
+
if (!owner || !repo) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
owner,
|
|
59
|
+
repo,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
deriveGitHubFallbackSource(source, marketplaceId) {
|
|
63
|
+
const repoShorthandMatch = source.match(/^([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)$/);
|
|
64
|
+
if (repoShorthandMatch) {
|
|
65
|
+
const [, owner, repo] = repoShorthandMatch;
|
|
66
|
+
return {
|
|
67
|
+
type: 'github',
|
|
68
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
69
|
+
owner,
|
|
70
|
+
repo,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!marketplaceId || !/^[a-zA-Z0-9._-]+$/.test(source)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const registry = this.getMarketplace(marketplaceId);
|
|
77
|
+
const registryRepo = registry ? this.parseGitHubRepo(registry.repoUrl) : null;
|
|
78
|
+
if (!registryRepo) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: 'github',
|
|
83
|
+
url: `https://github.com/${registryRepo.owner}/${source}.git`,
|
|
84
|
+
owner: registryRepo.owner,
|
|
85
|
+
repo: source,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
formatGitHubRepoUrl(source) {
|
|
89
|
+
if (!source.owner || !source.repo) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return `https://github.com/${source.owner}/${source.repo}`;
|
|
93
|
+
}
|
|
94
|
+
async resolvePreparedPluginDir(pluginDir, source) {
|
|
95
|
+
const claudeMarketplaceManifestPath = join(pluginDir, '.claude-plugin', 'marketplace.json');
|
|
96
|
+
if (!(await fileExists(claudeMarketplaceManifestPath))) {
|
|
97
|
+
return { pluginDir, warnings: [] };
|
|
98
|
+
}
|
|
99
|
+
const manifestContent = await readFileIfExists(claudeMarketplaceManifestPath);
|
|
100
|
+
if (!manifestContent) {
|
|
101
|
+
return { pluginDir, warnings: [] };
|
|
102
|
+
}
|
|
103
|
+
let manifest;
|
|
104
|
+
try {
|
|
105
|
+
manifest = JSON.parse(manifestContent);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error(`Invalid .claude-plugin/marketplace.json in ${pluginDir}`);
|
|
109
|
+
}
|
|
110
|
+
const entries = Array.isArray(manifest.plugins)
|
|
111
|
+
? manifest.plugins.filter((entry) => typeof entry?.name === 'string' && typeof entry?.source === 'string')
|
|
112
|
+
: [];
|
|
113
|
+
if (entries.length === 0) {
|
|
114
|
+
throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
|
|
115
|
+
}
|
|
116
|
+
const [firstEntry] = entries;
|
|
117
|
+
if (!firstEntry) {
|
|
118
|
+
throw new Error(`No plugins declared in .claude-plugin/marketplace.json for ${pluginDir}`);
|
|
119
|
+
}
|
|
120
|
+
let selectedEntry = firstEntry;
|
|
121
|
+
if (entries.length > 1) {
|
|
122
|
+
const candidates = new Set([source.pluginName, source.repo]
|
|
123
|
+
.filter((value) => !!value)
|
|
124
|
+
.flatMap(value => [value, basename(value)]));
|
|
125
|
+
const matched = entries.find(entry => candidates.has(entry.name) || candidates.has(basename(entry.source)));
|
|
126
|
+
if (!matched) {
|
|
127
|
+
throw new Error(`Repository "${pluginDir}" is a Claude marketplace bundle with multiple plugins. Select one of: ${entries.map(entry => entry.name).join(', ')}.`);
|
|
128
|
+
}
|
|
129
|
+
selectedEntry = matched;
|
|
130
|
+
}
|
|
131
|
+
const selectedPluginDir = resolve(pluginDir, selectedEntry.source);
|
|
132
|
+
const relativePath = relative(resolve(pluginDir), selectedPluginDir);
|
|
133
|
+
if (relativePath.startsWith('..') ||
|
|
134
|
+
relativePath.includes('/../') ||
|
|
135
|
+
relativePath.includes('\\..\\')) {
|
|
136
|
+
throw new Error(`Invalid bundled plugin source path "${selectedEntry.source}" in ${claudeMarketplaceManifestPath}`);
|
|
137
|
+
}
|
|
138
|
+
if (!(await isDirectory(selectedPluginDir))) {
|
|
139
|
+
throw new Error(`Bundled plugin path not found: ${selectedPluginDir}`);
|
|
140
|
+
}
|
|
141
|
+
const sourceLabel = this.formatGitHubRepoUrl(source) || pluginDir;
|
|
142
|
+
return {
|
|
143
|
+
pluginDir: selectedPluginDir,
|
|
144
|
+
warnings: [
|
|
145
|
+
`Source "${sourceLabel}" is a Claude Code marketplace bundle; using bundled plugin "${selectedEntry.name}".`,
|
|
146
|
+
],
|
|
147
|
+
claudeBundle: {
|
|
148
|
+
bundleName: manifest.name || selectedEntry.name,
|
|
149
|
+
pluginName: selectedEntry.name,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async loadPluginFromDirectory(pluginDir, source, warnings = []) {
|
|
154
|
+
const preparedPlugin = await this.resolvePreparedPluginDir(pluginDir, source);
|
|
155
|
+
const parsedPlugin = await this.parsePlugin(preparedPlugin.pluginDir, source);
|
|
156
|
+
return {
|
|
157
|
+
...parsedPlugin,
|
|
158
|
+
warnings: [...warnings, ...preparedPlugin.warnings, ...parsedPlugin.warnings],
|
|
159
|
+
resolvedPluginDir: preparedPlugin.pluginDir,
|
|
160
|
+
...(preparedPlugin.claudeBundle ? { nativeClaudeBundle: preparedPlugin.claudeBundle } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
slugifyPluginNamespace(value) {
|
|
164
|
+
return value
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase()
|
|
167
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
168
|
+
.replace(/^-+|-+$/g, '') || 'plugin';
|
|
169
|
+
}
|
|
170
|
+
async getClaudeNativeFeatureKinds(pluginDir, manifest) {
|
|
171
|
+
const featureChecks = await Promise.all([
|
|
172
|
+
(async () => !!manifest.commands || await isDirectory(join(pluginDir, 'commands')))(),
|
|
173
|
+
(async () => !!manifest.hooks || await isDirectory(join(pluginDir, 'hooks')))(),
|
|
174
|
+
(async () => !!manifest.agents || await isDirectory(join(pluginDir, 'agents')))(),
|
|
175
|
+
isDirectory(join(pluginDir, 'prompts')),
|
|
176
|
+
isDirectory(join(pluginDir, 'schemas')),
|
|
177
|
+
isDirectory(join(pluginDir, 'scripts')),
|
|
178
|
+
isDirectory(join(pluginDir, 'templates')),
|
|
179
|
+
]);
|
|
180
|
+
return [
|
|
181
|
+
...(featureChecks[0] ? ['commands'] : []),
|
|
182
|
+
...(featureChecks[1] ? ['hooks'] : []),
|
|
183
|
+
...(featureChecks[2] ? ['agents'] : []),
|
|
184
|
+
...(featureChecks[3] ? ['prompts'] : []),
|
|
185
|
+
...(featureChecks[4] ? ['schemas'] : []),
|
|
186
|
+
...(featureChecks[5] ? ['scripts'] : []),
|
|
187
|
+
...(featureChecks[6] ? ['templates'] : []),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
async getClaudeNativeInstallTarget(plugin, pluginDir) {
|
|
191
|
+
if (plugin.format !== 'claude') {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const manifestContent = await readFileIfExists(join(pluginDir, '.claude-plugin', 'plugin.json'));
|
|
195
|
+
if (!manifestContent) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
let manifest;
|
|
199
|
+
try {
|
|
200
|
+
manifest = JSON.parse(manifestContent);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const features = await this.getClaudeNativeFeatureKinds(pluginDir, manifest);
|
|
206
|
+
if (features.length === 0) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const baseNamespace = plugin.nativeClaudeBundle?.bundleName
|
|
210
|
+
|| plugin.source.marketplace
|
|
211
|
+
|| (plugin.source.owner && plugin.source.repo ? `${plugin.source.owner}-${plugin.source.repo}` : plugin.name);
|
|
212
|
+
const namespace = `agentinit-${this.slugifyPluginNamespace(baseNamespace)}`;
|
|
213
|
+
const versionDir = this.slugifyPluginNamespace(plugin.version || '0.0.0');
|
|
214
|
+
return {
|
|
215
|
+
namespace,
|
|
216
|
+
pluginKey: `${plugin.name}@${namespace}`,
|
|
217
|
+
installPath: join(homedir(), '.claude', 'plugins', 'cache', namespace, plugin.name, versionDir),
|
|
218
|
+
features,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async readClaudeInstalledPlugins() {
|
|
222
|
+
const path = getClaudeInstalledPluginsPath();
|
|
223
|
+
const content = await readFileIfExists(path);
|
|
224
|
+
if (!content) {
|
|
225
|
+
return { version: 2, plugins: {} };
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(content);
|
|
229
|
+
if (!parsed || parsed.version !== 2 || typeof parsed.plugins !== 'object' || parsed.plugins === null) {
|
|
230
|
+
return { version: 2, plugins: {} };
|
|
231
|
+
}
|
|
232
|
+
return parsed;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return { version: 2, plugins: {} };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async saveClaudeInstalledPlugins(state) {
|
|
239
|
+
await writeFile(getClaudeInstalledPluginsPath(), JSON.stringify(state, null, 2));
|
|
240
|
+
}
|
|
241
|
+
async installNativeClaudePlugin(plugin, pluginDir, agents) {
|
|
242
|
+
const installed = [];
|
|
243
|
+
const skipped = [];
|
|
244
|
+
const warnings = [];
|
|
245
|
+
const nativeTarget = await this.getClaudeNativeInstallTarget(plugin, pluginDir);
|
|
246
|
+
if (!nativeTarget) {
|
|
247
|
+
return { installed, skipped, warnings };
|
|
248
|
+
}
|
|
249
|
+
const hasClaudeTarget = agents.some(agent => agent.id === 'claude');
|
|
250
|
+
const featureLabel = nativeTarget.features.join(', ');
|
|
251
|
+
if (!hasClaudeTarget) {
|
|
252
|
+
warnings.push(`Claude Code-native plugin components detected (${featureLabel}), but no Claude Code target was selected; skipped native install.`);
|
|
253
|
+
return { installed, skipped, warnings };
|
|
254
|
+
}
|
|
255
|
+
warnings.push(`Claude Code-native plugin components detected (${featureLabel}); they will only work in Claude Code and install into ~/.claude/plugins.`);
|
|
256
|
+
const claudeInstalled = await this.readClaudeInstalledPlugins();
|
|
257
|
+
const conflictingKey = Object.keys(claudeInstalled.plugins).find(key => key !== nativeTarget.pluginKey && key.startsWith(`${plugin.name}@`));
|
|
258
|
+
if (conflictingKey) {
|
|
259
|
+
skipped.push({
|
|
260
|
+
agent: 'claude',
|
|
261
|
+
reason: `Claude plugin "${plugin.name}" is already installed as ${conflictingKey}; skipped native install to avoid duplicates.`,
|
|
262
|
+
});
|
|
263
|
+
warnings.push(`Skipped native Claude plugin install because Claude already has "${plugin.name}" installed as ${conflictingKey}.`);
|
|
264
|
+
return { installed, skipped, warnings };
|
|
265
|
+
}
|
|
266
|
+
await fs.rm(nativeTarget.installPath, { recursive: true, force: true }).catch(() => { });
|
|
267
|
+
await fs.mkdir(dirname(nativeTarget.installPath), { recursive: true });
|
|
268
|
+
await fs.cp(pluginDir, nativeTarget.installPath, { recursive: true, dereference: true });
|
|
269
|
+
const now = new Date().toISOString();
|
|
270
|
+
claudeInstalled.plugins[nativeTarget.pluginKey] = [{
|
|
271
|
+
scope: 'user',
|
|
272
|
+
installPath: nativeTarget.installPath,
|
|
273
|
+
version: plugin.version,
|
|
274
|
+
installedAt: now,
|
|
275
|
+
lastUpdated: now,
|
|
276
|
+
}];
|
|
277
|
+
await this.saveClaudeInstalledPlugins(claudeInstalled);
|
|
278
|
+
installed.push({
|
|
279
|
+
agent: 'claude',
|
|
280
|
+
pluginKey: nativeTarget.pluginKey,
|
|
281
|
+
installPath: nativeTarget.installPath,
|
|
282
|
+
});
|
|
283
|
+
warnings.push('Reload plugins in Claude Code with /reload-plugins to activate native plugin components.');
|
|
284
|
+
return { installed, skipped, warnings };
|
|
285
|
+
}
|
|
286
|
+
async removeNativeClaudePlugin(component) {
|
|
287
|
+
const claudeInstalled = await this.readClaudeInstalledPlugins();
|
|
288
|
+
const entries = claudeInstalled.plugins[component.pluginKey] || [];
|
|
289
|
+
const remainingEntries = entries.filter(entry => entry.installPath !== component.installPath);
|
|
290
|
+
if (remainingEntries.length > 0) {
|
|
291
|
+
claudeInstalled.plugins[component.pluginKey] = remainingEntries;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
delete claudeInstalled.plugins[component.pluginKey];
|
|
295
|
+
}
|
|
296
|
+
await this.saveClaudeInstalledPlugins(claudeInstalled);
|
|
297
|
+
await fs.rm(component.installPath, { recursive: true, force: true }).catch(() => { });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
26
300
|
// ── Source Resolution ──────────────────────────────────────────────
|
|
27
301
|
/**
|
|
28
302
|
* Resolve a source string into a PluginSource.
|
|
@@ -182,11 +456,7 @@ export class PluginManager {
|
|
|
182
456
|
.filter(p => p.name.includes(name) || name.includes(p.name))
|
|
183
457
|
.map(p => p.name)
|
|
184
458
|
.slice(0, 5);
|
|
185
|
-
|
|
186
|
-
if (suggestions.length > 0) {
|
|
187
|
-
msg += ` Did you mean: ${suggestions.join(', ')}?`;
|
|
188
|
-
}
|
|
189
|
-
throw new Error(msg);
|
|
459
|
+
throw new MarketplacePluginNotFoundError(name, registryId, registry.name, suggestions);
|
|
190
460
|
}
|
|
191
461
|
/**
|
|
192
462
|
* List all plugins in a marketplace, optionally filtered
|
|
@@ -201,7 +471,13 @@ export class PluginManager {
|
|
|
201
471
|
const fullDir = join(cacheDir, dir);
|
|
202
472
|
if (!(await isDirectory(fullDir)))
|
|
203
473
|
continue;
|
|
204
|
-
const cat = dir === 'plugins'
|
|
474
|
+
const cat = dir === 'plugins'
|
|
475
|
+
? 'official'
|
|
476
|
+
: dir === 'external_plugins'
|
|
477
|
+
? 'community'
|
|
478
|
+
: dir.startsWith('skills/.')
|
|
479
|
+
? dir.slice('skills/.'.length)
|
|
480
|
+
: dir;
|
|
205
481
|
if (category && cat !== category)
|
|
206
482
|
continue;
|
|
207
483
|
const entries = await listFiles(fullDir);
|
|
@@ -312,10 +588,10 @@ export class PluginManager {
|
|
|
312
588
|
const mcpServers = await this.parseMcpJson(pluginDir);
|
|
313
589
|
// Warn about agent-specific features
|
|
314
590
|
if (await isDirectory(join(pluginDir, 'hooks')) || manifest.hooks) {
|
|
315
|
-
warnings.push('Hooks (hooks/) are Claude Code-specific
|
|
591
|
+
warnings.push('Hooks (hooks/) are Claude Code-specific');
|
|
316
592
|
}
|
|
317
593
|
if (await isDirectory(join(pluginDir, 'agents')) || manifest.agents) {
|
|
318
|
-
warnings.push('Agent definitions (agents/) are Claude Code-specific
|
|
594
|
+
warnings.push('Agent definitions (agents/) are Claude Code-specific');
|
|
319
595
|
}
|
|
320
596
|
return {
|
|
321
597
|
name: manifest.name,
|
|
@@ -500,11 +776,38 @@ ${body.trim()}
|
|
|
500
776
|
*/
|
|
501
777
|
async installPlugin(source, projectPath, options = {}) {
|
|
502
778
|
const resolved = this.resolveSource(source, { from: options.from });
|
|
779
|
+
let effectiveSource = resolved;
|
|
503
780
|
let pluginDir;
|
|
504
781
|
let tempDir = null;
|
|
782
|
+
const resolutionWarnings = [];
|
|
505
783
|
// 1. Resolve source to a local directory
|
|
506
784
|
if (resolved.type === 'marketplace') {
|
|
507
|
-
|
|
785
|
+
try {
|
|
786
|
+
pluginDir = await this.resolveMarketplacePlugin(resolved.pluginName, resolved.marketplace || 'claude');
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (!(error instanceof MarketplacePluginNotFoundError)) {
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
const fallbackSource = this.deriveGitHubFallbackSource(source, resolved.marketplace);
|
|
793
|
+
if (!fallbackSource || !fallbackSource.url) {
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
const fallbackUrl = this.formatGitHubRepoUrl(fallbackSource) || fallbackSource.url.replace(/\.git$/, '');
|
|
797
|
+
resolutionWarnings.push(error.message);
|
|
798
|
+
resolutionWarnings.push(`Marketplace lookup failed; trying unverified GitHub repository ${fallbackUrl} instead.`);
|
|
799
|
+
try {
|
|
800
|
+
tempDir = await this.skillsManager.cloneRepo(fallbackSource.url);
|
|
801
|
+
}
|
|
802
|
+
catch (fallbackError) {
|
|
803
|
+
throw new Error(`${error.message} Tried unverified GitHub repository ${fallbackUrl} but failed: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown error'}`);
|
|
804
|
+
}
|
|
805
|
+
effectiveSource = {
|
|
806
|
+
...fallbackSource,
|
|
807
|
+
...(resolved.pluginName ? { pluginName: resolved.pluginName } : {}),
|
|
808
|
+
};
|
|
809
|
+
pluginDir = tempDir;
|
|
810
|
+
}
|
|
508
811
|
}
|
|
509
812
|
else if (resolved.type === 'github') {
|
|
510
813
|
if (!resolved.url)
|
|
@@ -520,13 +823,14 @@ ${body.trim()}
|
|
|
520
823
|
}
|
|
521
824
|
try {
|
|
522
825
|
// 2. Parse plugin
|
|
523
|
-
const plugin = await this.
|
|
826
|
+
const plugin = await this.loadPluginFromDirectory(pluginDir, effectiveSource, resolutionWarnings);
|
|
524
827
|
// 3. If --list, return early with contents
|
|
525
828
|
if (options.list) {
|
|
526
829
|
return {
|
|
527
830
|
plugin,
|
|
528
831
|
skills: { installed: [], skipped: [] },
|
|
529
832
|
mcpServers: { applied: [], skipped: [] },
|
|
833
|
+
nativePlugins: { installed: [], skipped: [] },
|
|
530
834
|
warnings: plugin.warnings,
|
|
531
835
|
};
|
|
532
836
|
}
|
|
@@ -537,6 +841,7 @@ ${body.trim()}
|
|
|
537
841
|
plugin,
|
|
538
842
|
skills: { installed: [], skipped: plugin.skills.map(s => ({ name: s.name, reason: 'No target agents found' })) },
|
|
539
843
|
mcpServers: { applied: [], skipped: plugin.mcpServers.map(s => ({ name: s.name, reason: 'No target agents found' })) },
|
|
844
|
+
nativePlugins: { installed: [], skipped: [] },
|
|
540
845
|
warnings: plugin.warnings,
|
|
541
846
|
};
|
|
542
847
|
}
|
|
@@ -544,21 +849,25 @@ ${body.trim()}
|
|
|
544
849
|
const skillResult = await this.installPluginSkills(plugin, projectPath, agents, options);
|
|
545
850
|
// 6. Apply MCP servers per agent
|
|
546
851
|
const mcpResult = await this.applyPluginMcpServers(plugin, projectPath, agents, options.global);
|
|
547
|
-
// 7.
|
|
548
|
-
|
|
852
|
+
// 7. Install agent-native plugin payloads when supported.
|
|
853
|
+
const nativePluginResult = await this.installNativeClaudePlugin(plugin, plugin.resolvedPluginDir, agents);
|
|
854
|
+
const installWarnings = [...plugin.warnings, ...nativePluginResult.warnings];
|
|
855
|
+
// 8. Save to registry only when the install actually applied components.
|
|
856
|
+
if (skillResult.installed.length > 0 || mcpResult.applied.length > 0 || nativePluginResult.installed.length > 0) {
|
|
549
857
|
const installed = {
|
|
550
858
|
name: plugin.name,
|
|
551
859
|
version: plugin.version,
|
|
552
860
|
description: plugin.description,
|
|
553
|
-
source:
|
|
861
|
+
source: effectiveSource,
|
|
554
862
|
format: plugin.format,
|
|
555
863
|
installedAt: new Date().toISOString(),
|
|
556
864
|
scope: options.global ? 'global' : 'project',
|
|
557
865
|
components: {
|
|
558
866
|
skills: skillResult.installed,
|
|
559
867
|
mcpServers: mcpResult.applied,
|
|
868
|
+
nativePlugins: nativePluginResult.installed,
|
|
560
869
|
},
|
|
561
|
-
warnings:
|
|
870
|
+
warnings: installWarnings,
|
|
562
871
|
};
|
|
563
872
|
await this.addToRegistry(installed, projectPath, options.global);
|
|
564
873
|
}
|
|
@@ -566,7 +875,8 @@ ${body.trim()}
|
|
|
566
875
|
plugin,
|
|
567
876
|
skills: skillResult,
|
|
568
877
|
mcpServers: mcpResult,
|
|
569
|
-
|
|
878
|
+
nativePlugins: nativePluginResult,
|
|
879
|
+
warnings: installWarnings,
|
|
570
880
|
};
|
|
571
881
|
}
|
|
572
882
|
finally {
|
|
@@ -765,7 +1075,8 @@ ${body.trim()}
|
|
|
765
1075
|
if (options.agents && options.agents.length > 0) {
|
|
766
1076
|
const agentSet = new Set(options.agents);
|
|
767
1077
|
plugins = plugins.filter(p => p.components.skills.some(s => agentSet.has(s.agent)) ||
|
|
768
|
-
p.components.mcpServers.some(m => agentSet.has(m.agent))
|
|
1078
|
+
p.components.mcpServers.some(m => agentSet.has(m.agent)) ||
|
|
1079
|
+
(p.components.nativePlugins || []).some(nativePlugin => agentSet.has(nativePlugin.agent)));
|
|
769
1080
|
}
|
|
770
1081
|
return plugins;
|
|
771
1082
|
}
|
|
@@ -778,7 +1089,8 @@ ${body.trim()}
|
|
|
778
1089
|
if (!plugin) {
|
|
779
1090
|
return { removed: false, details: [`Plugin "${name}" not found in registry`] };
|
|
780
1091
|
}
|
|
781
|
-
|
|
1092
|
+
const pluginNativeComponents = plugin.components.nativePlugins || [];
|
|
1093
|
+
if (plugin.components.skills.length === 0 && plugin.components.mcpServers.length === 0 && pluginNativeComponents.length === 0) {
|
|
782
1094
|
registry.plugins = registry.plugins.filter(p => p.name !== name);
|
|
783
1095
|
await this.saveRegistry(registry, projectPath, options.global);
|
|
784
1096
|
return {
|
|
@@ -877,7 +1189,48 @@ ${body.trim()}
|
|
|
877
1189
|
...retainedMcpServers,
|
|
878
1190
|
...targetedMcpServers.filter(mcp => !removedMcpKeys.has(`${mcp.agent}:${mcp.name}`)),
|
|
879
1191
|
];
|
|
880
|
-
|
|
1192
|
+
const targetedNativePlugins = agentFilter
|
|
1193
|
+
? pluginNativeComponents.filter(nativePlugin => agentFilter.has(nativePlugin.agent))
|
|
1194
|
+
: pluginNativeComponents;
|
|
1195
|
+
const retainedNativePlugins = agentFilter
|
|
1196
|
+
? pluginNativeComponents.filter(nativePlugin => !agentFilter.has(nativePlugin.agent))
|
|
1197
|
+
: [];
|
|
1198
|
+
const otherScopeRegistry = await this.getRegistry(projectPath, !options.global);
|
|
1199
|
+
const otherRegistryNativePlugins = otherScopeRegistry.plugins
|
|
1200
|
+
.flatMap(entry => entry.components.nativePlugins || []);
|
|
1201
|
+
const otherPluginNativePlugins = registry.plugins
|
|
1202
|
+
.filter(entry => entry.name !== plugin.name)
|
|
1203
|
+
.flatMap(entry => entry.components.nativePlugins || []);
|
|
1204
|
+
const remainingNativeRefs = [
|
|
1205
|
+
...retainedNativePlugins,
|
|
1206
|
+
...otherPluginNativePlugins,
|
|
1207
|
+
...otherRegistryNativePlugins,
|
|
1208
|
+
];
|
|
1209
|
+
const removedNativeKeys = new Set();
|
|
1210
|
+
for (const nativePlugin of targetedNativePlugins) {
|
|
1211
|
+
const nativeKey = `${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`;
|
|
1212
|
+
if (removedNativeKeys.has(nativeKey)) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const sharedNativeInstall = remainingNativeRefs.some(other => other.installPath === nativePlugin.installPath || other.pluginKey === nativePlugin.pluginKey);
|
|
1216
|
+
if (sharedNativeInstall) {
|
|
1217
|
+
details.push(`Skipped shared native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
await this.removeNativeClaudePlugin(nativePlugin);
|
|
1222
|
+
removedNativeKeys.add(nativeKey);
|
|
1223
|
+
details.push(`Removed native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
details.push(`Could not remove native plugin payload: ${nativePlugin.pluginKey} (${nativePlugin.agent})`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const remainingNativePlugins = [
|
|
1230
|
+
...retainedNativePlugins,
|
|
1231
|
+
...targetedNativePlugins.filter(nativePlugin => !removedNativeKeys.has(`${nativePlugin.agent}:${nativePlugin.pluginKey}:${nativePlugin.installPath}`)),
|
|
1232
|
+
];
|
|
1233
|
+
if (removedSkillPaths.size === 0 && removedMcpKeys.size === 0 && removedNativeKeys.size === 0) {
|
|
881
1234
|
if (agentFilter) {
|
|
882
1235
|
details.push(`No removable plugin components matched the requested agents for "${name}"`);
|
|
883
1236
|
}
|
|
@@ -888,10 +1241,13 @@ ${body.trim()}
|
|
|
888
1241
|
components: {
|
|
889
1242
|
skills: remainingSkills,
|
|
890
1243
|
mcpServers: remainingMcpServers,
|
|
1244
|
+
nativePlugins: remainingNativePlugins,
|
|
891
1245
|
},
|
|
892
1246
|
};
|
|
893
1247
|
registry.plugins = registry.plugins.filter(p => p.name !== name);
|
|
894
|
-
if (updatedPlugin.components.skills.length > 0 ||
|
|
1248
|
+
if (updatedPlugin.components.skills.length > 0 ||
|
|
1249
|
+
updatedPlugin.components.mcpServers.length > 0 ||
|
|
1250
|
+
(updatedPlugin.components.nativePlugins || []).length > 0) {
|
|
895
1251
|
registry.plugins.push(updatedPlugin);
|
|
896
1252
|
details.push('Updated plugin registry');
|
|
897
1253
|
}
|