claude-code-marketplace 0.2.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksii Nikiforov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A web-based dashboard for browsing, installing, and managing [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugins across multiple marketplaces.
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/claude-code-marketplace)](https://www.npmjs.com/package/claude-code-marketplace)
6
+
5
7
  <p align="center">
6
8
  <img src="assets/main-dark.png" alt="Marketplace — dark theme" width="100%">
7
9
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-marketplace",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Web UI for browsing and managing Claude Code marketplace plugins",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -43,6 +43,8 @@ const ICONS = {
43
43
  '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="5" r="2.5"/><circle cx="12" cy="12" r="2.5"/><circle cx="12" cy="19" r="2.5"/></svg>',
44
44
  };
45
45
  ICONS.settings = ICONS.gear;
46
+ ICONS.openEditor =
47
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M17.583 2.207a1.1 1.1 0 0 1 1.541.033l2.636 2.636a1.1 1.1 0 0 1 .033 1.541L10.68 17.53a1.1 1.1 0 0 1-.345.247l-4.56 1.903a.55.55 0 0 1-.725-.725l1.903-4.56a1.1 1.1 0 0 1 .247-.345zm.902 1.87-8.794 8.793-.946 2.268 2.268-.946 8.794-8.793z"/></svg>';
46
48
  const COMP_HAS_DIR = new Set(['skills', 'commands', 'agents']);
47
49
  const COMP_LABELS = {
48
50
  skills: 'Skills',
@@ -65,6 +67,7 @@ function updateArrow(p) {
65
67
 
66
68
  // --- Init ---
67
69
  document.addEventListener('DOMContentLoaded', () => {
70
+ document.getElementById('contentOpenEditor').innerHTML = ICONS.openEditor;
68
71
  restoreAppState();
69
72
  loadProject();
70
73
  loadData();
@@ -101,10 +104,10 @@ document.addEventListener('DOMContentLoaded', () => {
101
104
  });
102
105
 
103
106
  const savedTheme = localStorage.getItem('theme');
104
- if (savedTheme === 'light') {
105
- document.body.classList.add('light');
106
- } else if (savedTheme === 'dark') {
107
+ if (savedTheme === 'dark') {
107
108
  document.body.classList.add('dark-forced');
109
+ } else {
110
+ document.body.classList.add('light');
108
111
  }
109
112
  syncHljsTheme();
110
113
 
@@ -225,7 +228,9 @@ function renderTree() {
225
228
  return;
226
229
  }
227
230
 
228
- let html = '';
231
+ const allIds = marketplaces.map((m) => `m_${safeId(m.name)}`);
232
+ const allExpanded = allIds.length > 0 && allIds.every((id) => expandedNodes.has(id));
233
+ let html = `<div class="tree-expand-toggle" id="toggleExpandBtn" onclick="toggleExpandAll()">${allExpanded ? 'Collapse All' : 'Expand All'}</div>`;
229
234
 
230
235
  for (const m of marketplaces) {
231
236
  const mid = safeId(m.name);
@@ -386,7 +391,10 @@ async function showDetail(pluginId) {
386
391
  panel.innerHTML = `
387
392
  <div class="detail-header">
388
393
  <h3>${headerIcon} ${esc(plugin.name)} ${plugin.version ? `<span class="version">v${esc(plugin.version)}</span>` : ''}</h3>
389
- <button class="detail-close" onclick="closeDetail()">\u2715</button>
394
+ <div class="detail-header-actions">
395
+ ${plugin._pluginDir ? `<button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({pluginId:'${esc(plugin.fullId)}'})">${ICONS.openEditor}</button>` : ''}
396
+ <button class="detail-close" onclick="closeDetail()">\u2715</button>
397
+ </div>
390
398
  </div>
391
399
  <div class="detail-body">
392
400
  ${updateBanner}
@@ -501,6 +509,7 @@ const EXT_TO_LANG = {
501
509
 
502
510
  const PREFERRED_FILE = 'SKILL.MD';
503
511
  let _contentCodeEl = null;
512
+ let _contentPluginId = null;
504
513
 
505
514
  function highlightSource(text, fileName) {
506
515
  const ext = (fileName || '').split('.').pop().toLowerCase();
@@ -528,7 +537,31 @@ function getContentCodeEl() {
528
537
  return _contentCodeEl;
529
538
  }
530
539
 
540
+ async function openInEditor() {
541
+ if (!_contentPluginId) return;
542
+ const relativePath = document.getElementById('contentViewerPath').textContent || '';
543
+ try {
544
+ await fetch('/api/open-in-editor', {
545
+ method: 'POST',
546
+ headers: { 'Content-Type': 'application/json' },
547
+ body: JSON.stringify({ pluginId: _contentPluginId, relativePath }),
548
+ });
549
+ } catch {}
550
+ }
551
+
552
+ async function openFolderInEditor({ pluginId, marketplaceName } = {}) {
553
+ if (!pluginId && !marketplaceName) return;
554
+ try {
555
+ await fetch('/api/open-folder-in-editor', {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify({ pluginId, marketplaceName }),
559
+ });
560
+ } catch {}
561
+ }
562
+
531
563
  async function openContentModal(pluginId, initialPath, componentType) {
564
+ _contentPluginId = pluginId;
532
565
  const plugin = findPlugin(pluginId);
533
566
  const label = COMP_LABELS[componentType] || componentType;
534
567
  document.getElementById('contentModalTitle').textContent = `${plugin?.name || pluginId} \u2014 ${label}`;
@@ -647,7 +680,10 @@ function showMarketplaceDetail(name) {
647
680
  panel.innerHTML = `
648
681
  <div class="detail-header">
649
682
  <h3>${ICONS.marketplace} ${esc(m.name)} ${m.version ? `<span class="version">v${esc(m.version)}</span>` : ''}</h3>
650
- <button class="detail-close" onclick="closeDetail()">\u2715</button>
683
+ <div class="detail-header-actions">
684
+ ${m.installLocation ? `<button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({marketplaceName:'${esc(m.name)}'})">${ICONS.openEditor}</button>` : ''}
685
+ <button class="detail-close" onclick="closeDetail()">\u2715</button>
686
+ </div>
651
687
  </div>
652
688
  <div class="detail-body">
653
689
  <div class="detail-section">
@@ -810,6 +846,17 @@ function toggleChildren(id) {
810
846
  }
811
847
  }
812
848
 
849
+ function toggleExpandAll() {
850
+ const allIds = marketplaces.map((m) => `m_${safeId(m.name)}`);
851
+ const collapse = allIds.length > 0 && allIds.every((id) => expandedNodes.has(id));
852
+ for (const id of allIds) {
853
+ if (collapse) expandedNodes.delete(id);
854
+ else expandedNodes.add(id);
855
+ }
856
+ saveExpandedNodes();
857
+ renderTree();
858
+ }
859
+
813
860
  function findPlugin(id) {
814
861
  for (const m of marketplaces) {
815
862
  const p = m.plugins.find((p) => p.fullId === id);
@@ -998,6 +1045,12 @@ function handleKeydown(e) {
998
1045
  return;
999
1046
  }
1000
1047
 
1048
+ if (matchKey(e, 'e')) {
1049
+ e.preventDefault();
1050
+ toggleExpandAll();
1051
+ return;
1052
+ }
1053
+
1001
1054
  if (matchKey(e, 'r')) {
1002
1055
  e.preventDefault();
1003
1056
  refresh();
package/public/index.html CHANGED
@@ -110,6 +110,7 @@
110
110
  <tr><td><kbd>?</kbd></td><td>Show keyboard shortcuts</td></tr>
111
111
  <tr><td><kbd>/</kbd></td><td>Focus search</td></tr>
112
112
  <tr><td><kbd>S</kbd></td><td>Focus scope filter</td></tr>
113
+ <tr><td><kbd>E</kbd></td><td>Expand / Collapse all</td></tr>
113
114
  <tr><td><kbd>R</kbd></td><td>Refresh data</td></tr>
114
115
  <tr><td><kbd>Esc</kbd></td><td>Close panel / blur input</td></tr>
115
116
  </table>
@@ -134,7 +135,10 @@
134
135
  <div class="modal content-modal" onclick="event.stopPropagation()">
135
136
  <div class="modal-header">
136
137
  <h3 id="contentModalTitle">File Preview</h3>
137
- <button class="modal-close" onclick="closeModal('contentModal')">&#10005;</button>
138
+ <div class="modal-header-actions">
139
+ <button class="modal-action-btn" id="contentOpenEditor" title="Open in VS Code" onclick="openInEditor()"></button>
140
+ <button class="modal-close" onclick="closeModal('contentModal')">&#10005;</button>
141
+ </div>
138
142
  </div>
139
143
  <div class="content-modal-body">
140
144
  <div class="content-tree" id="contentTree"></div>
package/public/style.css CHANGED
@@ -31,8 +31,8 @@
31
31
  --scope-user-dim: rgba(196, 149, 106, 0.18);
32
32
  --scope-project: #7ab5a0;
33
33
  --scope-project-dim: rgba(122, 181, 160, 0.18);
34
- --scope-local: #f0b429;
35
- --scope-local-dim: rgba(240, 180, 41, 0.18);
34
+ --scope-local: #8a9bb5;
35
+ --scope-local-dim: rgba(138, 155, 181, 0.18);
36
36
  }
37
37
 
38
38
  body.light {
@@ -57,43 +57,12 @@ body.light {
57
57
  --plan: #5a7a5a;
58
58
  --plan-dim: rgba(90, 122, 90, 0.15);
59
59
 
60
- --scope-user: #9a6d3a;
61
- --scope-user-dim: rgba(154, 109, 58, 0.15);
60
+ --scope-user: #8a7055;
61
+ --scope-user-dim: rgba(138, 112, 85, 0.12);
62
62
  --scope-project: #4d8a72;
63
63
  --scope-project-dim: rgba(77, 138, 114, 0.15);
64
- --scope-local: #b07d0a;
65
- --scope-local-dim: rgba(176, 125, 10, 0.15);
66
- }
67
-
68
- @media (prefers-color-scheme: light) {
69
- body:not(.dark-forced) {
70
- --bg-deep: #e8e6e3;
71
- --bg-surface: #f4f3f1;
72
- --bg-elevated: #dddbd8;
73
- --bg-hover: #d2d0cc;
74
- --border: #a09b94;
75
- --text-primary: #0a0a0a;
76
- --text-secondary: #444444;
77
- --text-tertiary: #666666;
78
- --text-muted: #888888;
79
- --accent-text: #b85a20;
80
- --accent-dim: rgba(232, 111, 51, 0.18);
81
- --accent-glow: rgba(232, 111, 51, 0.5);
82
- --success: #1a8a5a;
83
- --success-dim: rgba(26, 138, 90, 0.15);
84
- --warning: #b07d0a;
85
- --warning-dim: rgba(176, 125, 10, 0.15);
86
- --error: #c53030;
87
- --error-dim: rgba(197, 48, 48, 0.15);
88
- --plan: #5a7a5a;
89
- --plan-dim: rgba(90, 122, 90, 0.15);
90
- --scope-user: #9a6d3a;
91
- --scope-user-dim: rgba(154, 109, 58, 0.15);
92
- --scope-project: #4d8a72;
93
- --scope-project-dim: rgba(77, 138, 114, 0.15);
94
- --scope-local: #b07d0a;
95
- --scope-local-dim: rgba(176, 125, 10, 0.15);
96
- }
64
+ --scope-local: #6b7d96;
65
+ --scope-local-dim: rgba(107, 125, 150, 0.12);
97
66
  }
98
67
 
99
68
  /* === RESET === */
@@ -110,7 +79,7 @@ body {
110
79
  background: var(--bg-deep);
111
80
  color: var(--text-primary);
112
81
  line-height: 1.5;
113
- height: 100vh;
82
+ height: calc(100vh / 1.2);
114
83
  display: flex;
115
84
  zoom: 1.2;
116
85
  flex-direction: column;
@@ -303,6 +272,7 @@ body {
303
272
  .main-layout {
304
273
  display: flex;
305
274
  flex: 1;
275
+ min-height: 0;
306
276
  overflow: hidden;
307
277
  }
308
278
 
@@ -316,8 +286,22 @@ body {
316
286
  min-width: 400px;
317
287
  }
318
288
 
289
+ .tree-expand-toggle {
290
+ padding: 4px 12px;
291
+ font-size: 10px;
292
+ color: var(--text-muted);
293
+ cursor: pointer;
294
+ text-transform: uppercase;
295
+ letter-spacing: 0.5px;
296
+ font-family: var(--mono);
297
+ }
298
+ .tree-expand-toggle:hover {
299
+ color: var(--accent);
300
+ }
301
+
319
302
  .tree-container {
320
303
  flex: 1;
304
+ min-height: 0;
321
305
  overflow-y: auto;
322
306
  overflow-x: hidden;
323
307
  }
@@ -459,65 +443,51 @@ body {
459
443
  border-color: rgba(196, 149, 106, 0.4);
460
444
  color: rgba(196, 149, 106, 0.5);
461
445
  }
462
- .scope-toggle.user.active {
463
- background: var(--scope-user-dim);
464
- color: var(--scope-user);
465
- border-color: var(--scope-user);
466
- }
446
+ .scope-toggle.user.active,
467
447
  .scope-toggle.user.disabled {
468
448
  background: var(--scope-user-dim);
469
449
  color: var(--scope-user);
470
450
  border-color: var(--scope-user);
471
- opacity: 0.4;
472
- text-decoration: line-through;
473
451
  }
474
452
 
475
453
  .scope-toggle.project {
476
454
  border-color: rgba(122, 181, 160, 0.4);
477
455
  color: rgba(122, 181, 160, 0.5);
478
456
  }
479
- .scope-toggle.project.active {
480
- background: var(--scope-project-dim);
481
- color: var(--scope-project);
482
- border-color: var(--scope-project);
483
- }
457
+ .scope-toggle.project.active,
484
458
  .scope-toggle.project.disabled {
485
459
  background: var(--scope-project-dim);
486
460
  color: var(--scope-project);
487
461
  border-color: var(--scope-project);
488
- opacity: 0.4;
489
- text-decoration: line-through;
490
462
  }
491
463
 
492
464
  .scope-toggle.local {
493
- border-color: rgba(240, 180, 41, 0.4);
494
- color: rgba(240, 180, 41, 0.5);
495
- }
496
- .scope-toggle.local.active {
497
- background: var(--scope-local-dim);
498
- color: var(--scope-local);
499
- border-color: var(--scope-local);
465
+ border-color: rgba(138, 155, 181, 0.4);
466
+ color: rgba(138, 155, 181, 0.5);
500
467
  }
468
+ .scope-toggle.local.active,
501
469
  .scope-toggle.local.disabled {
502
470
  background: var(--scope-local-dim);
503
471
  color: var(--scope-local);
504
472
  border-color: var(--scope-local);
473
+ }
474
+ .scope-toggle.disabled {
505
475
  opacity: 0.4;
506
476
  text-decoration: line-through;
507
477
  }
508
478
 
509
479
  /* Light theme: boost inactive scope contrast further */
510
480
  body.light .scope-toggle.user {
511
- border-color: rgba(154, 109, 58, 0.5);
512
- color: rgba(154, 109, 58, 0.6);
481
+ border-color: rgba(138, 112, 85, 0.4);
482
+ color: rgba(138, 112, 85, 0.5);
513
483
  }
514
484
  body.light .scope-toggle.project {
515
485
  border-color: rgba(77, 138, 114, 0.5);
516
486
  color: rgba(77, 138, 114, 0.6);
517
487
  }
518
488
  body.light .scope-toggle.local {
519
- border-color: rgba(176, 125, 10, 0.5);
520
- color: rgba(176, 125, 10, 0.6);
489
+ border-color: rgba(107, 125, 150, 0.4);
490
+ color: rgba(107, 125, 150, 0.5);
521
491
  }
522
492
 
523
493
  /* === BADGES === */
@@ -652,7 +622,7 @@ body.light .scope-toggle.local {
652
622
  .tree-desc-inline {
653
623
  font-size: 11px;
654
624
  color: var(--text-muted);
655
- opacity: 0.8;
625
+ opacity: 0.95;
656
626
  white-space: nowrap;
657
627
  overflow: hidden;
658
628
  text-overflow: ellipsis;
@@ -750,6 +720,12 @@ body.light .scope-toggle.local {
750
720
  font-size: 11px;
751
721
  font-weight: 400;
752
722
  }
723
+ .detail-header-actions,
724
+ .modal-header-actions {
725
+ display: flex;
726
+ align-items: center;
727
+ gap: 4px;
728
+ }
753
729
  .detail-close {
754
730
  background: transparent;
755
731
  border: 1px solid transparent;
@@ -1027,6 +1003,20 @@ body.light .scope-toggle.local {
1027
1003
  font-size: 13px;
1028
1004
  font-weight: 600;
1029
1005
  }
1006
+ .modal-action-btn {
1007
+ background: none;
1008
+ border: none;
1009
+ color: var(--text-muted);
1010
+ cursor: pointer;
1011
+ padding: 4px;
1012
+ display: flex;
1013
+ align-items: center;
1014
+ border-radius: 4px;
1015
+ }
1016
+ .modal-action-btn:hover {
1017
+ color: var(--accent);
1018
+ background: var(--hover);
1019
+ }
1030
1020
  .modal-close {
1031
1021
  background: none;
1032
1022
  border: none;
@@ -1034,6 +1024,10 @@ body.light .scope-toggle.local {
1034
1024
  cursor: pointer;
1035
1025
  font-size: 16px;
1036
1026
  padding: 4px;
1027
+ display: flex;
1028
+ align-items: center;
1029
+ justify-content: center;
1030
+ line-height: 1;
1037
1031
  }
1038
1032
  .modal-body {
1039
1033
  padding: 16px;
package/server.js CHANGED
@@ -202,6 +202,21 @@ function loadMarketplaces() {
202
202
 
203
203
  const compKeys = ['skills', 'commands', 'agents', 'mcpServers', 'hooks', 'lspServers'];
204
204
 
205
+ // Resolve origin dir from marketplace source
206
+ let originDir = null;
207
+ if (installLocation) {
208
+ const rawSource = pd.source;
209
+ if (typeof rawSource === 'string' && rawSource) {
210
+ const srcDir = path.resolve(installLocation, rawSource);
211
+ if (fs.existsSync(srcDir)) originDir = srcDir;
212
+ }
213
+ if (!originDir) {
214
+ const pluginSubdir = path.join(installLocation, 'plugins', pd.name);
215
+ if (fs.existsSync(pluginSubdir)) originDir = pluginSubdir;
216
+ else if ((mData.plugins || []).length === 1) originDir = installLocation;
217
+ }
218
+ }
219
+
205
220
  // Resolve plugin dir for filesystem-based component counts
206
221
  let pluginDir = null;
207
222
  for (const s of ['user', 'project', 'local']) {
@@ -209,11 +224,7 @@ function loadMarketplaces() {
209
224
  const resolved = resolveInstallPath(ip);
210
225
  if (resolved) { pluginDir = resolved; break; }
211
226
  }
212
- if (!pluginDir && installLocation) {
213
- const pluginSubdir = path.join(installLocation, 'plugins', pd.name);
214
- if (fs.existsSync(pluginSubdir)) pluginDir = pluginSubdir;
215
- else if ((mData.plugins || []).length === 1) pluginDir = installLocation;
216
- }
227
+ if (!pluginDir) pluginDir = originDir;
217
228
 
218
229
  const fsComps = pluginDir ? countComponents(pluginDir) : null;
219
230
  const components = {};
@@ -253,6 +264,7 @@ function loadMarketplaces() {
253
264
  installedScopes,
254
265
  components,
255
266
  _pluginDir: pluginDir,
267
+ _originDir: originDir,
256
268
  _fsComps: fsComps,
257
269
  metadata: Object.fromEntries(
258
270
  Object.entries(pd).filter(([k]) => !['name', 'description', 'source', 'version', ...compKeys].includes(k))
@@ -562,6 +574,54 @@ app.get('/api/plugins/:pluginId/preview/*', (req, res) => {
562
574
  }
563
575
  });
564
576
 
577
+ function openVSCode(args, res) {
578
+ execFile('code', args, { shell: true }, (err) => {
579
+ if (err) return res.status(500).json({ error: 'Failed to open editor' });
580
+ res.json({ ok: true });
581
+ });
582
+ }
583
+
584
+ app.post('/api/open-in-editor', (req, res) => {
585
+ const { pluginId, relativePath } = req.body;
586
+ if (!pluginId) return res.status(400).json({ error: 'pluginId required' });
587
+
588
+ const marketplaces = getCachedMarketplaces();
589
+ const pluginDir = resolvePluginDir(pluginId, marketplaces);
590
+ if (!pluginDir) return res.status(404).json({ error: 'Plugin not found' });
591
+
592
+ const args = ['-n', pluginDir];
593
+
594
+ const pluginJson = path.join(pluginDir, '.claude-plugin', 'plugin.json');
595
+ if (fs.existsSync(pluginJson)) args.push(pluginJson);
596
+
597
+ if (relativePath) {
598
+ const fullPath = path.resolve(pluginDir, relativePath);
599
+ if (fullPath.startsWith(path.resolve(pluginDir))) {
600
+ args.push(fullPath);
601
+ }
602
+ }
603
+
604
+ openVSCode(args, res);
605
+ });
606
+
607
+ app.post('/api/open-folder-in-editor', (req, res) => {
608
+ const { pluginId, marketplaceName } = req.body;
609
+ const marketplaces = getCachedMarketplaces();
610
+ let folder;
611
+
612
+ if (pluginId) {
613
+ const plugin = findPlugin(pluginId, marketplaces);
614
+ folder = plugin?._originDir || plugin?._pluginDir || null;
615
+ } else if (marketplaceName) {
616
+ const m = marketplaces.find(m => m.name === marketplaceName);
617
+ folder = m?.installLocation || null;
618
+ }
619
+
620
+ if (!folder) return res.status(404).json({ error: 'Directory not found' });
621
+
622
+ openVSCode(['-n', folder], res);
623
+ });
624
+
565
625
  app.get('/api/project', (req, res) => {
566
626
  res.json({ path: projectPath });
567
627
  });