claude-code-marketplace 0.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-marketplace",
3
- "version": "0.2.1",
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();
@@ -388,7 +391,10 @@ async function showDetail(pluginId) {
388
391
  panel.innerHTML = `
389
392
  <div class="detail-header">
390
393
  <h3>${headerIcon} ${esc(plugin.name)} ${plugin.version ? `<span class="version">v${esc(plugin.version)}</span>` : ''}</h3>
391
- <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>
392
398
  </div>
393
399
  <div class="detail-body">
394
400
  ${updateBanner}
@@ -503,6 +509,7 @@ const EXT_TO_LANG = {
503
509
 
504
510
  const PREFERRED_FILE = 'SKILL.MD';
505
511
  let _contentCodeEl = null;
512
+ let _contentPluginId = null;
506
513
 
507
514
  function highlightSource(text, fileName) {
508
515
  const ext = (fileName || '').split('.').pop().toLowerCase();
@@ -530,7 +537,31 @@ function getContentCodeEl() {
530
537
  return _contentCodeEl;
531
538
  }
532
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
+
533
563
  async function openContentModal(pluginId, initialPath, componentType) {
564
+ _contentPluginId = pluginId;
534
565
  const plugin = findPlugin(pluginId);
535
566
  const label = COMP_LABELS[componentType] || componentType;
536
567
  document.getElementById('contentModalTitle').textContent = `${plugin?.name || pluginId} \u2014 ${label}`;
@@ -649,7 +680,10 @@ function showMarketplaceDetail(name) {
649
680
  panel.innerHTML = `
650
681
  <div class="detail-header">
651
682
  <h3>${ICONS.marketplace} ${esc(m.name)} ${m.version ? `<span class="version">v${esc(m.version)}</span>` : ''}</h3>
652
- <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>
653
687
  </div>
654
688
  <div class="detail-body">
655
689
  <div class="detail-section">
package/public/index.html CHANGED
@@ -135,7 +135,10 @@
135
135
  <div class="modal content-modal" onclick="event.stopPropagation()">
136
136
  <div class="modal-header">
137
137
  <h3 id="contentModalTitle">File Preview</h3>
138
- <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>
139
142
  </div>
140
143
  <div class="content-modal-body">
141
144
  <div class="content-tree" id="contentTree"></div>
package/public/style.css CHANGED
@@ -720,6 +720,12 @@ body.light .scope-toggle.local {
720
720
  font-size: 11px;
721
721
  font-weight: 400;
722
722
  }
723
+ .detail-header-actions,
724
+ .modal-header-actions {
725
+ display: flex;
726
+ align-items: center;
727
+ gap: 4px;
728
+ }
723
729
  .detail-close {
724
730
  background: transparent;
725
731
  border: 1px solid transparent;
@@ -997,6 +1003,20 @@ body.light .scope-toggle.local {
997
1003
  font-size: 13px;
998
1004
  font-weight: 600;
999
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
+ }
1000
1020
  .modal-close {
1001
1021
  background: none;
1002
1022
  border: none;
@@ -1004,6 +1024,10 @@ body.light .scope-toggle.local {
1004
1024
  cursor: pointer;
1005
1025
  font-size: 16px;
1006
1026
  padding: 4px;
1027
+ display: flex;
1028
+ align-items: center;
1029
+ justify-content: center;
1030
+ line-height: 1;
1007
1031
  }
1008
1032
  .modal-body {
1009
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
  });