claude-code-marketplace 0.3.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-marketplace",
3
- "version": "0.3.2",
3
+ "version": "0.4.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
@@ -42,6 +42,9 @@ const ICONS = {
42
42
  kebab:
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
+ ICONS.readme = SVG(
46
+ '<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>',
47
+ );
45
48
  ICONS.settings = ICONS.gear;
46
49
  ICONS.openEditor =
47
50
  '<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>';
@@ -54,6 +57,7 @@ const COMP_LABELS = {
54
57
  hooks: 'Hooks',
55
58
  lspServers: 'LSP Servers',
56
59
  settings: 'Settings',
60
+ readme: 'README',
57
61
  };
58
62
 
59
63
  function updateArrow(p) {
@@ -505,41 +509,52 @@ function renderDetailComponents(pluginId, comps, hasDirAccess) {
505
509
  const entries = Object.entries(comps).filter(
506
510
  ([k, v]) => !k.startsWith('_') && (Array.isArray(v) ? v.length > 0 : v > 0),
507
511
  );
508
- if (!entries.length) return '<div style="color:var(--text-dim);font-size:12px">No components found</div>';
512
+ if (!entries.length && !comps._readmePath)
513
+ return '<div style="color:var(--text-dim);font-size:12px">No components found</div>';
509
514
 
510
- return entries
511
- .map(([type, items]) => {
512
- const names = Array.isArray(items) ? items : [];
513
- const count = names.length || items;
514
- let html = `<div class="detail-comp-group">
515
+ let readmeHtml = '';
516
+ if (comps._readmePath && hasDirAccess) {
517
+ readmeHtml = `<div class="readme-comp-item" onclick="openContentModal('${esc(pluginId)}', '${esc(comps._readmePath)}', 'readme')">
518
+ <span class="icon">${ICONS.readme}</span> ${esc(comps._readmePath)}
519
+ </div>`;
520
+ }
521
+
522
+ return (
523
+ readmeHtml +
524
+ entries
525
+ .map(([type, items]) => {
526
+ const names = Array.isArray(items) ? items : [];
527
+ const count = names.length || items;
528
+ let html = `<div class="detail-comp-group">
515
529
  <div class="detail-comp-header">
516
530
  <span class="comp-icon">${ICONS[type] || ''}</span>
517
531
  ${COMP_LABELS[type] || type}
518
532
  <span class="count">${count}</span>
519
533
  </div>`;
520
534
 
521
- if (names.length) {
522
- const configFile = configFiles[type];
523
- const dir = COMP_HAS_DIR.has(type) ? type : null;
524
- html += '<div class="detail-comp-items">';
525
- for (const name of names) {
526
- const clickPath = configFile || (dir ? `${dir}/${name}` : name);
527
- const cls = hasDirAccess ? '' : ' disabled';
528
- const click = hasDirAccess
529
- ? ` onclick="openContentModal('${esc(pluginId)}', '${esc(clickPath)}', '${esc(type)}')"`
530
- : '';
531
- html += `<div class="detail-comp-item${cls}"${click}>
535
+ if (names.length) {
536
+ const configFile = configFiles[type];
537
+ const dir = COMP_HAS_DIR.has(type) ? type : null;
538
+ html += '<div class="detail-comp-items">';
539
+ for (const name of names) {
540
+ const clickPath = configFile || (dir ? `${dir}/${name}` : name);
541
+ const cls = hasDirAccess ? '' : ' disabled';
542
+ const click = hasDirAccess
543
+ ? ` onclick="openContentModal('${esc(pluginId)}', '${esc(clickPath)}', '${esc(type)}')"`
544
+ : '';
545
+ html += `<div class="detail-comp-item${cls}"${click}>
532
546
  <span class="icon">${type === 'skills' ? ICONS.folder : ICONS.file}</span>
533
547
  ${esc(name)}
534
548
  </div>`;
549
+ }
550
+ html += '</div>';
535
551
  }
536
- html += '</div>';
537
- }
538
552
 
539
- html += '</div>';
540
- return html;
541
- })
542
- .join('');
553
+ html += '</div>';
554
+ return html;
555
+ })
556
+ .join('')
557
+ );
543
558
  }
544
559
 
545
560
  const EXT_TO_LANG = {
@@ -611,6 +626,26 @@ async function openFolderInEditor({ pluginId, marketplaceName } = {}) {
611
626
  } catch {}
612
627
  }
613
628
 
629
+ async function openReadmeModal(title, fetchUrl) {
630
+ _contentPluginId = null;
631
+ document.getElementById('contentModalTitle').textContent = `${title} \u2014 README`;
632
+ const tree = document.getElementById('contentTree');
633
+ const codeEl = getContentCodeEl();
634
+ const pathEl = document.getElementById('contentViewerPath');
635
+ tree.innerHTML = '';
636
+ pathEl.textContent = 'README.md';
637
+ codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
638
+ document.getElementById('contentModal').classList.add('open');
639
+ try {
640
+ const res = await fetch(fetchUrl);
641
+ if (!res.ok) throw new Error('Not found');
642
+ const data = await res.json();
643
+ codeEl.innerHTML = highlightSource(data.content || '', data.name || 'README.md');
644
+ } catch {
645
+ codeEl.innerHTML = '<span style="color:var(--error)">Failed to load README</span>';
646
+ }
647
+ }
648
+
614
649
  async function openContentModal(pluginId, initialPath, componentType) {
615
650
  _contentPluginId = pluginId;
616
651
  const plugin = findPlugin(pluginId);
@@ -752,6 +787,7 @@ function showMarketplaceDetail(name) {
752
787
  <span class="meta-value">${installed} installed / ${total} total</span>
753
788
  </div>
754
789
  </div>
790
+ ${m.readmeFile ? `<div class="detail-section"><h4>Documentation</h4><div class="readme-comp-item" onclick="openReadmeModal('${esc(m.name)}', '/api/marketplaces/${encodeURIComponent(m.name)}/readme')">${ICONS.readme} ${esc(m.readmeFile)}</div></div>` : ''}
755
791
  <div class="detail-section">
756
792
  <h4>Actions</h4>
757
793
  <div class="mkt-actions">
package/public/style.css CHANGED
@@ -934,6 +934,31 @@ body.light .scope-toggle.local {
934
934
  background: none;
935
935
  color: var(--text-tertiary);
936
936
  }
937
+ .readme-comp-item {
938
+ display: flex;
939
+ align-items: center;
940
+ gap: 8px;
941
+ padding: 6px 10px;
942
+ font-size: 12px;
943
+ color: var(--accent);
944
+ cursor: pointer;
945
+ border: 1px dashed var(--border);
946
+ border-radius: 6px;
947
+ background: var(--bg-deep);
948
+ transition:
949
+ background 0.12s,
950
+ border-color 0.12s;
951
+ margin-bottom: 8px;
952
+ }
953
+ .readme-comp-item:hover {
954
+ background: var(--bg-hover);
955
+ border-color: var(--accent);
956
+ }
957
+ .readme-comp-item svg {
958
+ width: 14px;
959
+ height: 14px;
960
+ opacity: 0.7;
961
+ }
937
962
 
938
963
  /* === TOAST === */
939
964
 
package/server.js CHANGED
@@ -37,6 +37,12 @@ function readJsonSafe(filePath) {
37
37
  } catch { return null; }
38
38
  }
39
39
 
40
+ function findReadmeFile(dirPath) {
41
+ try {
42
+ return fs.readdirSync(dirPath).find(f => f.toLowerCase() === 'readme.md') || null;
43
+ } catch { return null; }
44
+ }
45
+
40
46
  function readJsonKey(filePath, key) {
41
47
  const data = readJsonSafe(filePath);
42
48
  return data ? (data[key] || {}) : {};
@@ -168,6 +174,8 @@ function loadMarketplaces() {
168
174
  marketplace.version = mData.version || null;
169
175
  marketplace.owner = mData.owner || null;
170
176
  marketplace.description = mData.description || null;
177
+ const mktReadme = findReadmeFile(installLocation);
178
+ if (mktReadme) marketplace.readmeFile = mktReadme;
171
179
 
172
180
  for (const pd of (mData.plugins || [])) {
173
181
  if (!pd.name) continue;
@@ -370,6 +378,9 @@ function countComponents(pluginDir, meta = {}) {
370
378
  }
371
379
  }
372
380
 
381
+ const readmeFile = findReadmeFile(pluginDir);
382
+ if (readmeFile) result._readmePath = readmeFile;
383
+
373
384
  result._configFiles = configFiles;
374
385
  return result;
375
386
  }
@@ -529,6 +540,18 @@ app.get('/api/marketplaces', (req, res) => {
529
540
  }
530
541
  });
531
542
 
543
+ app.get('/api/marketplaces/:name/readme', (req, res) => {
544
+ const mktData = getCachedMarketplaces();
545
+ const m = mktData.find(m => m.name === req.params.name);
546
+ if (!m?.readmeFile) return res.status(404).json({ error: 'No README found' });
547
+ try {
548
+ const content = fs.readFileSync(path.join(m.installLocation, m.readmeFile), 'utf-8');
549
+ res.json({ type: 'file', content, name: m.readmeFile });
550
+ } catch {
551
+ res.status(404).json({ error: 'Failed to read README' });
552
+ }
553
+ });
554
+
532
555
  app.get('/api/plugins/:pluginId/components', (req, res) => {
533
556
  res.setHeader('Cache-Control', 'no-store');
534
557
  const pluginId = decodeURIComponent(req.params.pluginId);