@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.
@@ -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 = listPlugins();
26
+ const plugins = [];
21
27
 
22
- // Enhance with additional info
23
- const enhancedPlugins = plugins.map(plugin => {
24
- const pluginDir = path.join(INSTALLED_DIR, plugin.name);
25
- const manifestPath = path.join(pluginDir, 'plugin.json');
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
- let manifest = null;
28
- if (fs.existsSync(manifestPath)) {
29
- try {
30
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
31
- } catch (err) {
32
- // Ignore parse errors
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
- return {
37
- ...plugin,
38
- description: manifest?.description || '',
39
- author: manifest?.author || '',
40
- commands: manifest?.commands || [],
41
- hooks: manifest?.hooks || []
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: enhancedPlugins };
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} gitUrl - Git repository URL
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(gitUrl) {
88
- return await installPluginCore(gitUrl);
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 (for future use)
149
- * @returns {Array} Empty array for now
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
- // TODO: Implement plugin repository system
153
- return [];
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 (for future use)
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
- // TODO: Implement plugin repository system
163
- throw new Error('Plugin repositories not yet implemented');
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 (for future use)
168
- * @param {string} id - Repository ID
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(id) {
172
- // TODO: Implement plugin repository system
173
- throw new Error('Plugin repositories not yet implemented');
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