claude-code-marketplace 0.5.3 → 0.5.5

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +36 -7
  3. package/server.js +27 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-marketplace",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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
@@ -46,6 +46,7 @@ ICONS.readme = SVG(
46
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
47
  );
48
48
  ICONS.settings = ICONS.gear;
49
+ ICONS.claudeMd = ICONS.readme;
49
50
  ICONS.openEditor =
50
51
  '<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>';
51
52
  ICONS.copyPath =
@@ -59,9 +60,26 @@ const COMP_LABELS = {
59
60
  hooks: 'Hooks',
60
61
  lspServers: 'LSP Servers',
61
62
  settings: 'Settings',
63
+ claudeMd: 'CLAUDE.md',
62
64
  readme: 'README',
63
65
  };
64
66
 
67
+ const VIRTUAL_ROOT_PREFIX = '~root/';
68
+
69
+ function encodePathSegments(p) {
70
+ return p.split('/').map(encodeURIComponent).join('/');
71
+ }
72
+
73
+ function getContentRelativePath() {
74
+ const el = document.getElementById('contentViewerPath');
75
+ return el.dataset.rawPath || el.textContent || '';
76
+ }
77
+
78
+ function claudeMdLabel(name, count) {
79
+ if (name.startsWith(VIRTUAL_ROOT_PREFIX)) return count > 1 ? 'CLAUDE.md (project root)' : 'CLAUDE.md';
80
+ return count > 1 ? 'CLAUDE.md (.claude/)' : name;
81
+ }
82
+
65
83
  function updateArrow(p) {
66
84
  if (!p.hasUpdate) return '';
67
85
  const title =
@@ -601,7 +619,7 @@ function renderDetailComponents(pluginId, comps, hasDirAccess) {
601
619
  : '';
602
620
  html += `<div class="detail-comp-item${cls}"${click}>
603
621
  <span class="icon">${type === 'skills' ? ICONS.folder : ICONS.file}</span>
604
- ${esc(name)}
622
+ ${esc(type === 'claudeMd' ? claudeMdLabel(name, names.length) : name)}
605
623
  </div>`;
606
624
  }
607
625
  html += '</div>';
@@ -674,7 +692,7 @@ async function postAndFlash(endpoint, data, btn) {
674
692
 
675
693
  async function openInEditor(event) {
676
694
  if (!_contentPluginId) return;
677
- const relativePath = document.getElementById('contentViewerPath').textContent || '';
695
+ const relativePath = getContentRelativePath();
678
696
  await postAndFlash('/api/open-in-editor', { pluginId: _contentPluginId, relativePath }, event?.currentTarget);
679
697
  }
680
698
 
@@ -710,8 +728,14 @@ async function copyToClipboard(text, btn) {
710
728
 
711
729
  async function copyContentPath(event) {
712
730
  if (!_contentPluginDir) return;
713
- const relativePath = document.getElementById('contentViewerPath').textContent || '';
714
- const full = relativePath ? `${_contentPluginDir}/${relativePath}` : _contentPluginDir;
731
+ const relativePath = getContentRelativePath();
732
+ let full;
733
+ if (relativePath.startsWith(VIRTUAL_ROOT_PREFIX)) {
734
+ const parentDir = _contentPluginDir.replace(/\/[^/]+\/?$/, '');
735
+ full = `${parentDir}/${relativePath.slice(VIRTUAL_ROOT_PREFIX.length)}`;
736
+ } else {
737
+ full = relativePath ? `${_contentPluginDir}/${relativePath}` : _contentPluginDir;
738
+ }
715
739
  await copyToClipboard(full, event?.currentTarget);
716
740
  }
717
741
 
@@ -766,7 +790,7 @@ async function openContentModal(pluginId, initialPath, componentType) {
766
790
 
767
791
  async function loadContentTree(pluginId, treePath, container, depth, autoSelect) {
768
792
  try {
769
- const res = await fetch(`/api/plugins/${encodeURIComponent(pluginId)}/preview/${treePath}`);
793
+ const res = await fetch(`/api/plugins/${encodeURIComponent(pluginId)}/preview/${encodePathSegments(treePath)}`);
770
794
  if (!res.ok) throw new Error('Not found');
771
795
  const data = await res.json();
772
796
 
@@ -816,7 +840,8 @@ async function loadContentTree(pluginId, treePath, container, depth, autoSelect)
816
840
  async function loadContentFile(pluginId, filePath) {
817
841
  const codeEl = getContentCodeEl();
818
842
  const pathEl = document.getElementById('contentViewerPath');
819
- pathEl.textContent = filePath;
843
+ pathEl.textContent = filePath.startsWith(VIRTUAL_ROOT_PREFIX) ? filePath.slice(VIRTUAL_ROOT_PREFIX.length) : filePath;
844
+ pathEl.dataset.rawPath = filePath;
820
845
  codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
821
846
 
822
847
  document.querySelectorAll('#contentTree .content-tree-item.active').forEach((el) => el.classList.remove('active'));
@@ -824,7 +849,7 @@ async function loadContentFile(pluginId, filePath) {
824
849
  if (activeItem) activeItem.classList.add('active');
825
850
 
826
851
  try {
827
- const res = await fetch(`/api/plugins/${encodeURIComponent(pluginId)}/preview/${filePath}`);
852
+ const res = await fetch(`/api/plugins/${encodeURIComponent(pluginId)}/preview/${encodePathSegments(filePath)}`);
828
853
  if (!res.ok) throw new Error('Not found');
829
854
  const data = await res.json();
830
855
 
@@ -1369,6 +1394,10 @@ if ('serviceWorker' in navigator) {
1369
1394
  e.preventDefault();
1370
1395
  window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
1371
1396
  }
1397
+ if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey && /^[1-9]$/.test(e.key)) {
1398
+ e.preventDefault();
1399
+ window.parent?.postMessage({ type: 'hub:keydown', key: e.key }, '*');
1400
+ }
1372
1401
  });
1373
1402
  })();
1374
1403
 
package/server.js CHANGED
@@ -452,6 +452,15 @@ function scanCustomizations(basePath, scope) {
452
452
  if (fs.existsSync(path.join(basePath, 'settings.local.json'))) settingsFiles.push('settings.local.json');
453
453
  if (settingsFiles.length) components.settings = settingsFiles;
454
454
 
455
+ // Add CLAUDE.md files as browsable entries
456
+ const claudeMdFiles = [];
457
+ if (fs.existsSync(path.join(basePath, 'CLAUDE.md'))) claudeMdFiles.push('CLAUDE.md');
458
+ if (scope === 'project') {
459
+ const parentClaude = path.join(basePath, '..', 'CLAUDE.md');
460
+ if (fs.existsSync(parentClaude)) claudeMdFiles.push('~root/CLAUDE.md');
461
+ }
462
+ if (claudeMdFiles.length) components.claudeMd = claudeMdFiles;
463
+
455
464
  const hasAny = Object.values(components).some(v => Array.isArray(v) && v.length > 0);
456
465
  if (!hasAny) return null;
457
466
 
@@ -552,6 +561,20 @@ function resolvePluginDir(fullId, marketplaces) {
552
561
  return findPlugin(fullId, marketplaces)?._pluginDir || null;
553
562
  }
554
563
 
564
+ function resolveVirtualRelPath(pluginId, relPath) {
565
+ return pluginId === '_custom/project' && relPath.startsWith('~root/')
566
+ ? path.join('..', relPath.slice(6))
567
+ : relPath;
568
+ }
569
+
570
+ function isPathAllowed(fullPath, pluginDir, pluginId) {
571
+ if (fullPath.startsWith(path.resolve(pluginDir))) return true;
572
+ if (pluginId === '_custom/project') {
573
+ return fullPath === path.join(path.resolve(pluginDir, '..'), 'CLAUDE.md');
574
+ }
575
+ return false;
576
+ }
577
+
555
578
  // --- API Routes ---
556
579
 
557
580
  app.get('/api/marketplaces', (req, res) => {
@@ -598,8 +621,8 @@ app.get('/api/plugins/:pluginId/preview/*', (req, res) => {
598
621
  const pluginDir = resolvePluginDir(pluginId, marketplaces);
599
622
  if (!pluginDir) return res.status(404).json({ error: 'Plugin not found' });
600
623
 
601
- let fullPath = path.resolve(pluginDir, relPath);
602
- if (!fullPath.startsWith(path.resolve(pluginDir))) {
624
+ let fullPath = path.resolve(pluginDir, resolveVirtualRelPath(pluginId, relPath));
625
+ if (!isPathAllowed(fullPath, pluginDir, pluginId)) {
603
626
  return res.status(403).json({ error: 'Access denied' });
604
627
  }
605
628
  if (!fs.existsSync(fullPath) && fs.existsSync(fullPath + '.md')) fullPath += '.md';
@@ -642,10 +665,8 @@ app.post('/api/open-in-editor', (req, res) => {
642
665
  if (fs.existsSync(pluginJson)) args.push(pluginJson);
643
666
 
644
667
  if (relativePath) {
645
- const fullPath = path.resolve(pluginDir, relativePath);
646
- if (fullPath.startsWith(path.resolve(pluginDir))) {
647
- args.push(fullPath);
648
- }
668
+ const fullPath = path.resolve(pluginDir, resolveVirtualRelPath(pluginId, relativePath));
669
+ if (isPathAllowed(fullPath, pluginDir, pluginId)) args.push(fullPath);
649
670
  }
650
671
 
651
672
  openVSCode(args, res);