@adversity/coding-tool-x 2.5.1 → 2.6.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.
@@ -0,0 +1,677 @@
1
+ /**
2
+ * Plugins Service
3
+ *
4
+ * Wraps the plugin system for API access
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
11
+ const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
12
+ const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
13
+ const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
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
+
19
+ class PluginsService {
20
+ /**
21
+ * List all installed plugins with their status
22
+ * Reads from Claude Code's native installed_plugins.json
23
+ * @returns {Object} { plugins: Array }
24
+ */
25
+ listPlugins() {
26
+ const plugins = [];
27
+
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('@');
37
+
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
+ }
77
+ }
78
+ } catch (err) {
79
+ console.error('[PluginsService] Failed to read installed_plugins.json:', err.message);
80
+ }
81
+ }
82
+
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
+ }
94
+
95
+ return { plugins };
96
+ }
97
+
98
+ /**
99
+ * Get single plugin details
100
+ * @param {string} name - Plugin name
101
+ * @returns {Object|null} Plugin details or null
102
+ */
103
+ getPlugin(name) {
104
+ const plugin = getPlugin(name);
105
+ if (!plugin) {
106
+ return null;
107
+ }
108
+
109
+ const pluginDir = path.join(INSTALLED_DIR, name);
110
+ const manifestPath = path.join(pluginDir, 'plugin.json');
111
+
112
+ let manifest = null;
113
+ if (fs.existsSync(manifestPath)) {
114
+ try {
115
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
116
+ } catch (err) {
117
+ // Ignore parse errors
118
+ }
119
+ }
120
+
121
+ return {
122
+ name,
123
+ ...plugin,
124
+ description: manifest?.description || '',
125
+ author: manifest?.author || '',
126
+ commands: manifest?.commands || [],
127
+ hooks: manifest?.hooks || [],
128
+ manifest
129
+ };
130
+ }
131
+
132
+ /**
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 }
136
+ * @returns {Promise<Object>} Installation result
137
+ */
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
+ });
245
+ }
246
+
247
+ /**
248
+ * Uninstall plugin
249
+ * @param {string} name - Plugin name
250
+ * @returns {Object} Uninstallation result
251
+ */
252
+ uninstallPlugin(name) {
253
+ return uninstallPluginCore(name);
254
+ }
255
+
256
+ /**
257
+ * Toggle plugin enabled/disabled
258
+ * @param {string} name - Plugin name
259
+ * @param {boolean} enabled - Enable or disable
260
+ * @returns {Object} Updated plugin info
261
+ */
262
+ togglePlugin(name, enabled) {
263
+ const plugin = getPlugin(name);
264
+ if (!plugin) {
265
+ throw new Error(`Plugin "${name}" not found`);
266
+ }
267
+
268
+ updatePluginRegistry(name, { enabled });
269
+
270
+ return {
271
+ name,
272
+ ...getPlugin(name)
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Update plugin config
278
+ * @param {string} name - Plugin name
279
+ * @param {Object} config - Configuration object
280
+ * @returns {Object} Result
281
+ */
282
+ updatePluginConfig(name, config) {
283
+ const plugin = getPlugin(name);
284
+ if (!plugin) {
285
+ throw new Error(`Plugin "${name}" not found`);
286
+ }
287
+
288
+ const configFile = path.join(CONFIG_DIR, `${name}.json`);
289
+
290
+ // Ensure config directory exists
291
+ if (!fs.existsSync(CONFIG_DIR)) {
292
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
293
+ }
294
+
295
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
296
+
297
+ return {
298
+ success: true,
299
+ message: `Configuration updated for plugin "${name}"`
300
+ };
301
+ }
302
+
303
+ /**
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
346
+ */
347
+ getRepos() {
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;
396
+ }
397
+
398
+ /**
399
+ * Add repository
400
+ * @param {Object} repo - Repository info { url, owner, name, branch, enabled }
401
+ * @returns {Array} Updated repos list
402
+ */
403
+ addRepo(repo) {
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;
449
+ }
450
+
451
+ /**
452
+ * Remove repository
453
+ * @param {string} owner - Repository owner
454
+ * @param {string} name - Repository name
455
+ * @returns {Array} Updated repos list
456
+ */
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;
674
+ }
675
+ }
676
+
677
+ module.exports = { PluginsService };