@adversity/coding-tool-x 2.6.0 → 3.0.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/dist/web/assets/icons-BlzwYoRU.js +1 -0
- package/dist/web/assets/{index-Ej0MPDUI.js → index-AtwYwBZD.js} +2 -2
- package/dist/web/assets/index-BNHWEpD4.css +41 -0
- package/dist/web/assets/{naive-ui-sh0u_0bf.js → naive-ui-BcSq2wzw.js} +1 -1
- package/dist/web/assets/{vendors-CzcvkTIS.js → vendors-D2HHw_aW.js} +1 -1
- package/dist/web/assets/{vue-vendor-CEeI-Azr.js → vue-vendor-6JaYHOiI.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +2 -1
- package/src/plugins/constants.js +14 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/server/api/mcp.js +1 -1
- package/src/server/api/plugins.js +150 -10
- package/src/server/api/sessions.js +4 -4
- package/src/server/services/plugins-service.js +537 -37
- package/src/server/services/sessions.js +72 -16
- package/dist/web/assets/icons-CNM9_Fh0.js +0 -1
- package/dist/web/assets/index-BcmuQT-z.css +0 -41
|
@@ -6,43 +6,93 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
10
11
|
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
11
12
|
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
12
13
|
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
13
14
|
|
|
15
|
+
const CLAUDE_PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins');
|
|
16
|
+
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
17
|
+
const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
|
|
18
|
+
|
|
14
19
|
class PluginsService {
|
|
15
20
|
/**
|
|
16
21
|
* List all installed plugins with their status
|
|
22
|
+
* Reads from Claude Code's native installed_plugins.json
|
|
17
23
|
* @returns {Object} { plugins: Array }
|
|
18
24
|
*/
|
|
19
25
|
listPlugins() {
|
|
20
|
-
const plugins =
|
|
26
|
+
const plugins = [];
|
|
21
27
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
// Read Claude Code's installed_plugins.json
|
|
29
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
32
|
+
if (data.plugins) {
|
|
33
|
+
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
34
|
+
if (installations && installations.length > 0) {
|
|
35
|
+
const install = installations[0]; // Get first installation
|
|
36
|
+
const [name, marketplace] = key.split('@');
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
// Read plugin.json from installPath for description
|
|
39
|
+
let description = '';
|
|
40
|
+
let source = install.source || '';
|
|
41
|
+
let repoUrl = '';
|
|
42
|
+
|
|
43
|
+
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
44
|
+
const manifestPath = path.join(install.installPath, 'plugin.json');
|
|
45
|
+
if (fs.existsSync(manifestPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
48
|
+
description = manifest.description || '';
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Ignore parse errors
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse repoUrl from source if available
|
|
56
|
+
if (source) {
|
|
57
|
+
const match = source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
58
|
+
if (match) {
|
|
59
|
+
repoUrl = `https://github.com/${match[1]}/${match[2]}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
plugins.push({
|
|
64
|
+
name,
|
|
65
|
+
marketplace,
|
|
66
|
+
version: install.version || '1.0.0',
|
|
67
|
+
installPath: install.installPath,
|
|
68
|
+
installedAt: install.installedAt,
|
|
69
|
+
scope: install.scope,
|
|
70
|
+
enabled: true,
|
|
71
|
+
description,
|
|
72
|
+
source,
|
|
73
|
+
repoUrl
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
33
77
|
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('[PluginsService] Failed to read installed_plugins.json:', err.message);
|
|
34
80
|
}
|
|
81
|
+
}
|
|
35
82
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
// Also check legacy registry
|
|
84
|
+
try {
|
|
85
|
+
const legacyPlugins = listPlugins();
|
|
86
|
+
for (const plugin of legacyPlugins) {
|
|
87
|
+
if (!plugins.find(p => p.name === plugin.name)) {
|
|
88
|
+
plugins.push(plugin);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Ignore legacy registry errors
|
|
93
|
+
}
|
|
44
94
|
|
|
45
|
-
return { plugins
|
|
95
|
+
return { plugins };
|
|
46
96
|
}
|
|
47
97
|
|
|
48
98
|
/**
|
|
@@ -80,12 +130,118 @@ class PluginsService {
|
|
|
80
130
|
}
|
|
81
131
|
|
|
82
132
|
/**
|
|
83
|
-
* Install plugin from Git URL
|
|
84
|
-
* @param {string}
|
|
133
|
+
* Install plugin from Git URL or repo directory
|
|
134
|
+
* @param {string} source - Git repository URL or tree URL
|
|
135
|
+
* @param {Object} repoInfo - Optional repo info { owner, name, branch, directory }
|
|
85
136
|
* @returns {Promise<Object>} Installation result
|
|
86
137
|
*/
|
|
87
|
-
async installPlugin(
|
|
88
|
-
|
|
138
|
+
async installPlugin(source, repoInfo = null) {
|
|
139
|
+
// If repoInfo is provided, download from GitHub directly
|
|
140
|
+
if (repoInfo && repoInfo.owner && repoInfo.name && repoInfo.directory) {
|
|
141
|
+
return await this._installFromGitHubDirectory(repoInfo);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Parse tree URL format: https://github.com/owner/repo/tree/branch/path
|
|
145
|
+
const treeMatch = source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
146
|
+
if (treeMatch) {
|
|
147
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
148
|
+
return await this._installFromGitHubDirectory({ owner, name, branch, directory });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fallback to original git clone method
|
|
152
|
+
return await installPluginCore(source);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Install plugin from GitHub directory
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
async _installFromGitHubDirectory(repoInfo) {
|
|
160
|
+
const { owner, name, branch, directory } = repoInfo;
|
|
161
|
+
const https = require('https');
|
|
162
|
+
const pluginName = directory.split('/').pop();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Fetch plugin.json from the directory
|
|
166
|
+
const manifestUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/plugin.json`;
|
|
167
|
+
let manifest;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
manifest = await this._fetchJson(manifestUrl);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// No plugin.json, create a basic manifest
|
|
173
|
+
manifest = { name: pluginName, version: '1.0.0' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create plugin directory
|
|
177
|
+
const pluginDir = path.join(INSTALLED_DIR, manifest.name || pluginName);
|
|
178
|
+
if (!fs.existsSync(pluginDir)) {
|
|
179
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Download all files from the directory
|
|
183
|
+
const contentsUrl = `https://api.github.com/repos/${owner}/${name}/contents/${directory}?ref=${branch}`;
|
|
184
|
+
const contents = await this._fetchJson(contentsUrl);
|
|
185
|
+
|
|
186
|
+
for (const item of contents) {
|
|
187
|
+
if (item.type === 'file') {
|
|
188
|
+
const fileContent = await this._fetchRawFile(item.download_url);
|
|
189
|
+
fs.writeFileSync(path.join(pluginDir, item.name), fileContent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Write plugin.json if not exists
|
|
194
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
195
|
+
if (!fs.existsSync(manifestPath)) {
|
|
196
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Register plugin
|
|
200
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
201
|
+
addPlugin(manifest.name || pluginName, {
|
|
202
|
+
version: manifest.version || '1.0.0',
|
|
203
|
+
enabled: true,
|
|
204
|
+
installedAt: new Date().toISOString(),
|
|
205
|
+
source: `https://github.com/${owner}/${name}/tree/${branch}/${directory}`
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
success: true,
|
|
210
|
+
plugin: {
|
|
211
|
+
name: manifest.name || pluginName,
|
|
212
|
+
version: manifest.version || '1.0.0',
|
|
213
|
+
description: manifest.description || ''
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
error: `Failed to install plugin: ${err.message}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fetch raw file content
|
|
226
|
+
* @private
|
|
227
|
+
*/
|
|
228
|
+
async _fetchRawFile(url) {
|
|
229
|
+
const https = require('https');
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
https.get(url, {
|
|
232
|
+
headers: { 'User-Agent': 'coding-tool-x' }
|
|
233
|
+
}, (res) => {
|
|
234
|
+
let data = '';
|
|
235
|
+
res.on('data', chunk => data += chunk);
|
|
236
|
+
res.on('end', () => {
|
|
237
|
+
if (res.statusCode === 200) {
|
|
238
|
+
resolve(data);
|
|
239
|
+
} else {
|
|
240
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}).on('error', reject);
|
|
244
|
+
});
|
|
89
245
|
}
|
|
90
246
|
|
|
91
247
|
/**
|
|
@@ -145,32 +301,376 @@ class PluginsService {
|
|
|
145
301
|
}
|
|
146
302
|
|
|
147
303
|
/**
|
|
148
|
-
* Get plugin repositories
|
|
149
|
-
* @returns {
|
|
304
|
+
* Get plugin repositories config file path
|
|
305
|
+
* @returns {string} Config file path
|
|
306
|
+
*/
|
|
307
|
+
getReposConfigPath() {
|
|
308
|
+
const os = require('os');
|
|
309
|
+
const configDir = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
310
|
+
if (!fs.existsSync(configDir)) {
|
|
311
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
return path.join(configDir, 'plugin-repos.json');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Load repos from config file
|
|
318
|
+
* @returns {Object} Config object with repos array
|
|
319
|
+
*/
|
|
320
|
+
loadReposConfig() {
|
|
321
|
+
const configPath = this.getReposConfigPath();
|
|
322
|
+
if (!fs.existsSync(configPath)) {
|
|
323
|
+
return { repos: [] };
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.error('Failed to load repos config:', err);
|
|
329
|
+
return { repos: [] };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Save repos to config file
|
|
335
|
+
* @param {Object} config - Config object with repos array
|
|
336
|
+
*/
|
|
337
|
+
saveReposConfig(config) {
|
|
338
|
+
const configPath = this.getReposConfigPath();
|
|
339
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get plugin repositories
|
|
344
|
+
* Reads from both our config and Claude Code's native marketplace config
|
|
345
|
+
* @returns {Array} Repos list
|
|
150
346
|
*/
|
|
151
347
|
getRepos() {
|
|
152
|
-
|
|
153
|
-
|
|
348
|
+
const repos = [];
|
|
349
|
+
const seenRepos = new Set();
|
|
350
|
+
|
|
351
|
+
// 1. Load our own config
|
|
352
|
+
const config = this.loadReposConfig();
|
|
353
|
+
for (const repo of config.repos || []) {
|
|
354
|
+
const key = `${repo.owner}/${repo.name}`;
|
|
355
|
+
if (!seenRepos.has(key)) {
|
|
356
|
+
repos.push(repo);
|
|
357
|
+
seenRepos.add(key);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 2. Load Claude Code's native marketplace config
|
|
362
|
+
if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
363
|
+
try {
|
|
364
|
+
const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
365
|
+
|
|
366
|
+
for (const [marketplaceName, marketplaceData] of Object.entries(marketplaces)) {
|
|
367
|
+
if (marketplaceData.source && marketplaceData.source.url) {
|
|
368
|
+
const url = marketplaceData.source.url;
|
|
369
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
370
|
+
|
|
371
|
+
if (match) {
|
|
372
|
+
const [, owner, name] = match;
|
|
373
|
+
const key = `${owner}/${name}`;
|
|
374
|
+
|
|
375
|
+
if (!seenRepos.has(key)) {
|
|
376
|
+
repos.push({
|
|
377
|
+
owner,
|
|
378
|
+
name,
|
|
379
|
+
url,
|
|
380
|
+
branch: 'main', // Default branch
|
|
381
|
+
enabled: true,
|
|
382
|
+
source: 'claude-native',
|
|
383
|
+
lastUpdated: marketplaceData.lastUpdated
|
|
384
|
+
});
|
|
385
|
+
seenRepos.add(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error('[PluginsService] Failed to read known_marketplaces.json:', err.message);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return repos;
|
|
154
396
|
}
|
|
155
397
|
|
|
156
398
|
/**
|
|
157
|
-
* Add repository
|
|
158
|
-
* @param {Object} repo - Repository info
|
|
399
|
+
* Add repository
|
|
400
|
+
* @param {Object} repo - Repository info { url, owner, name, branch, enabled }
|
|
159
401
|
* @returns {Array} Updated repos list
|
|
160
402
|
*/
|
|
161
403
|
addRepo(repo) {
|
|
162
|
-
|
|
163
|
-
|
|
404
|
+
const config = this.loadReposConfig();
|
|
405
|
+
|
|
406
|
+
// Parse URL if provided
|
|
407
|
+
let owner = repo.owner;
|
|
408
|
+
let name = repo.name;
|
|
409
|
+
let url = repo.url;
|
|
410
|
+
|
|
411
|
+
if (url && !owner && !name) {
|
|
412
|
+
// Extract owner/name from URL
|
|
413
|
+
const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
414
|
+
if (match) {
|
|
415
|
+
owner = match[1];
|
|
416
|
+
name = match[2];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!owner || !name) {
|
|
421
|
+
throw new Error('Repository owner and name are required');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Construct URL if not provided
|
|
425
|
+
if (!url) {
|
|
426
|
+
url = `https://github.com/${owner}/${name}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check if repo already exists
|
|
430
|
+
const exists = config.repos.some(r => r.owner === owner && r.name === name);
|
|
431
|
+
if (exists) {
|
|
432
|
+
throw new Error(`Repository ${owner}/${name} already exists`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add new repo
|
|
436
|
+
const newRepo = {
|
|
437
|
+
owner,
|
|
438
|
+
name,
|
|
439
|
+
url,
|
|
440
|
+
branch: repo.branch || 'main',
|
|
441
|
+
enabled: repo.enabled !== false,
|
|
442
|
+
addedAt: new Date().toISOString()
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
config.repos.push(newRepo);
|
|
446
|
+
this.saveReposConfig(config);
|
|
447
|
+
|
|
448
|
+
return config.repos;
|
|
164
449
|
}
|
|
165
450
|
|
|
166
451
|
/**
|
|
167
|
-
* Remove repository
|
|
168
|
-
* @param {string}
|
|
452
|
+
* Remove repository
|
|
453
|
+
* @param {string} owner - Repository owner
|
|
454
|
+
* @param {string} name - Repository name
|
|
169
455
|
* @returns {Array} Updated repos list
|
|
170
456
|
*/
|
|
171
|
-
removeRepo(
|
|
172
|
-
|
|
173
|
-
|
|
457
|
+
removeRepo(owner, name) {
|
|
458
|
+
const config = this.loadReposConfig();
|
|
459
|
+
config.repos = config.repos.filter(r => !(r.owner === owner && r.name === name));
|
|
460
|
+
this.saveReposConfig(config);
|
|
461
|
+
return config.repos;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Toggle repository enabled status
|
|
466
|
+
* @param {string} owner - Repository owner
|
|
467
|
+
* @param {string} name - Repository name
|
|
468
|
+
* @param {boolean} enabled - Enable or disable
|
|
469
|
+
* @returns {Array} Updated repos list
|
|
470
|
+
*/
|
|
471
|
+
toggleRepo(owner, name, enabled) {
|
|
472
|
+
const config = this.loadReposConfig();
|
|
473
|
+
const repo = config.repos.find(r => r.owner === owner && r.name === name);
|
|
474
|
+
if (!repo) {
|
|
475
|
+
throw new Error(`Repository ${owner}/${name} not found`);
|
|
476
|
+
}
|
|
477
|
+
repo.enabled = enabled;
|
|
478
|
+
this.saveReposConfig(config);
|
|
479
|
+
return config.repos;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Sync repositories to Claude Code marketplace
|
|
484
|
+
* @returns {Promise<Object>} Sync results
|
|
485
|
+
*/
|
|
486
|
+
async syncRepos() {
|
|
487
|
+
const repos = this.getRepos();
|
|
488
|
+
const results = [];
|
|
489
|
+
const { execSync } = require('child_process');
|
|
490
|
+
|
|
491
|
+
for (const repo of repos.filter(r => r.enabled)) {
|
|
492
|
+
try {
|
|
493
|
+
execSync(`claude plugin marketplace add ${repo.url}`, {
|
|
494
|
+
encoding: 'utf8',
|
|
495
|
+
timeout: 30000,
|
|
496
|
+
stdio: 'pipe'
|
|
497
|
+
});
|
|
498
|
+
results.push({ repo: repo.url, success: true });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
results.push({ repo: repo.url, success: false, error: err.message });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return { success: true, results };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Sync plugins from Claude Code
|
|
509
|
+
* @returns {Promise<Object>} Updated plugins list
|
|
510
|
+
*/
|
|
511
|
+
async syncPlugins() {
|
|
512
|
+
return this.listPlugins();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Fetch JSON from URL
|
|
517
|
+
* @private
|
|
518
|
+
*/
|
|
519
|
+
async _fetchJson(url) {
|
|
520
|
+
const https = require('https');
|
|
521
|
+
return new Promise((resolve, reject) => {
|
|
522
|
+
https.get(url, {
|
|
523
|
+
headers: {
|
|
524
|
+
'User-Agent': 'coding-tool-x',
|
|
525
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
526
|
+
}
|
|
527
|
+
}, (res) => {
|
|
528
|
+
let data = '';
|
|
529
|
+
res.on('data', chunk => data += chunk);
|
|
530
|
+
res.on('end', () => {
|
|
531
|
+
if (res.statusCode === 200) {
|
|
532
|
+
resolve(JSON.parse(data));
|
|
533
|
+
} else {
|
|
534
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}).on('error', reject);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get plugin README content
|
|
543
|
+
* @param {Object} plugin - Plugin object with name, repoUrl, source, or repoInfo
|
|
544
|
+
* @returns {Promise<string>} README content or empty string
|
|
545
|
+
*/
|
|
546
|
+
async getPluginReadme(plugin) {
|
|
547
|
+
try {
|
|
548
|
+
let readmeUrl = null;
|
|
549
|
+
|
|
550
|
+
// Case 1: Market plugin with repoInfo
|
|
551
|
+
if (plugin.repoOwner && plugin.repoName && plugin.directory) {
|
|
552
|
+
const branch = plugin.repoBranch || 'main';
|
|
553
|
+
readmeUrl = `https://raw.githubusercontent.com/${plugin.repoOwner}/${plugin.repoName}/${branch}/${plugin.directory}/README.md`;
|
|
554
|
+
}
|
|
555
|
+
// Case 2: Installed plugin with source URL
|
|
556
|
+
else if (plugin.source) {
|
|
557
|
+
const treeMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
|
|
558
|
+
if (treeMatch) {
|
|
559
|
+
const [, owner, name, branch, directory] = treeMatch;
|
|
560
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${directory}/README.md`;
|
|
561
|
+
} else {
|
|
562
|
+
// Try to parse as regular repo URL
|
|
563
|
+
const repoMatch = plugin.source.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
564
|
+
if (repoMatch) {
|
|
565
|
+
const [, owner, name] = repoMatch;
|
|
566
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// Case 3: Plugin with repoUrl
|
|
571
|
+
else if (plugin.repoUrl) {
|
|
572
|
+
const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
|
|
573
|
+
if (match) {
|
|
574
|
+
const [, owner, name] = match;
|
|
575
|
+
readmeUrl = `https://raw.githubusercontent.com/${owner}/${name}/main/README.md`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!readmeUrl) {
|
|
580
|
+
return '';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Fetch README content
|
|
584
|
+
const content = await this._fetchRawFile(readmeUrl);
|
|
585
|
+
return content;
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error('[PluginsService] Failed to fetch README:', err.message);
|
|
588
|
+
return '';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get market plugins from configured repositories
|
|
594
|
+
* @returns {Promise<Array>} List of available market plugins
|
|
595
|
+
*/
|
|
596
|
+
async getMarketPlugins() {
|
|
597
|
+
const repos = this.getRepos().filter(r => r.enabled);
|
|
598
|
+
const marketPlugins = [];
|
|
599
|
+
|
|
600
|
+
for (const repo of repos) {
|
|
601
|
+
try {
|
|
602
|
+
const branch = repo.branch || 'main';
|
|
603
|
+
|
|
604
|
+
// Try to fetch marketplace.json first (official format)
|
|
605
|
+
try {
|
|
606
|
+
const marketplaceUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/.claude-plugin/marketplace.json`;
|
|
607
|
+
const marketplace = await this._fetchJson(marketplaceUrl);
|
|
608
|
+
|
|
609
|
+
if (marketplace && marketplace.plugins) {
|
|
610
|
+
for (const plugin of marketplace.plugins) {
|
|
611
|
+
marketPlugins.push({
|
|
612
|
+
name: plugin.name,
|
|
613
|
+
description: plugin.description || '',
|
|
614
|
+
author: plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
615
|
+
version: plugin.version || '1.0.0',
|
|
616
|
+
category: plugin.category || 'general',
|
|
617
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
618
|
+
repoOwner: repo.owner,
|
|
619
|
+
repoName: repo.name,
|
|
620
|
+
repoBranch: branch,
|
|
621
|
+
directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
|
|
622
|
+
lspServers: plugin.lspServers || null,
|
|
623
|
+
isInstalled: false
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
continue; // Skip legacy format check
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// marketplace.json not found, try legacy format
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Legacy format: each directory is a plugin with plugin.json
|
|
633
|
+
const apiUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/contents?ref=${branch}`;
|
|
634
|
+
const contents = await this._fetchJson(apiUrl);
|
|
635
|
+
const pluginDirs = contents.filter(item => item.type === 'dir' && !item.name.startsWith('.'));
|
|
636
|
+
|
|
637
|
+
for (const dir of pluginDirs) {
|
|
638
|
+
try {
|
|
639
|
+
const manifestUrl = `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/${branch}/${dir.name}/plugin.json`;
|
|
640
|
+
const manifest = await this._fetchJson(manifestUrl);
|
|
641
|
+
|
|
642
|
+
marketPlugins.push({
|
|
643
|
+
name: manifest.name || dir.name,
|
|
644
|
+
description: manifest.description || '',
|
|
645
|
+
author: manifest.author || repo.owner,
|
|
646
|
+
version: manifest.version || '1.0.0',
|
|
647
|
+
repoUrl: `https://github.com/${repo.owner}/${repo.name}`,
|
|
648
|
+
repoOwner: repo.owner,
|
|
649
|
+
repoName: repo.name,
|
|
650
|
+
repoBranch: branch,
|
|
651
|
+
directory: dir.name,
|
|
652
|
+
commands: manifest.commands || [],
|
|
653
|
+
hooks: manifest.hooks || [],
|
|
654
|
+
isInstalled: false
|
|
655
|
+
});
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// No plugin.json in this directory, skip
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
console.error(`[PluginsService] Failed to fetch plugins from ${repo.owner}/${repo.name}:`, err.message);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Mark installed plugins
|
|
666
|
+
const installedPlugins = this.listPlugins().plugins;
|
|
667
|
+
const installedNames = new Set(installedPlugins.map(p => p.name));
|
|
668
|
+
|
|
669
|
+
marketPlugins.forEach(plugin => {
|
|
670
|
+
plugin.isInstalled = installedNames.has(plugin.name);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return marketPlugins;
|
|
174
674
|
}
|
|
175
675
|
}
|
|
176
676
|
|