claude-code-marketplace 0.3.2 → 0.4.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.
- package/package.json +1 -1
- package/public/app.js +125 -37
- package/public/index.html +2 -1
- package/public/style.css +40 -0
- package/server.js +23 -0
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -42,9 +42,14 @@ 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>';
|
|
51
|
+
ICONS.copyPath =
|
|
52
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
48
53
|
const COMP_HAS_DIR = new Set(['skills', 'commands', 'agents']);
|
|
49
54
|
const COMP_LABELS = {
|
|
50
55
|
skills: 'Skills',
|
|
@@ -54,6 +59,7 @@ const COMP_LABELS = {
|
|
|
54
59
|
hooks: 'Hooks',
|
|
55
60
|
lspServers: 'LSP Servers',
|
|
56
61
|
settings: 'Settings',
|
|
62
|
+
readme: 'README',
|
|
57
63
|
};
|
|
58
64
|
|
|
59
65
|
function updateArrow(p) {
|
|
@@ -68,6 +74,7 @@ function updateArrow(p) {
|
|
|
68
74
|
// --- Init ---
|
|
69
75
|
document.addEventListener('DOMContentLoaded', () => {
|
|
70
76
|
document.getElementById('contentOpenEditor').innerHTML = ICONS.openEditor;
|
|
77
|
+
document.getElementById('contentCopyPath').innerHTML = ICONS.copyPath;
|
|
71
78
|
restoreAppState();
|
|
72
79
|
loadProject();
|
|
73
80
|
loadData();
|
|
@@ -443,7 +450,7 @@ async function showDetail(pluginId) {
|
|
|
443
450
|
<div class="detail-header">
|
|
444
451
|
<h3>${headerIcon} ${esc(plugin.name)} ${plugin.version ? `<span class="version">v${esc(plugin.version)}</span>` : ''}</h3>
|
|
445
452
|
<div class="detail-header-actions">
|
|
446
|
-
${plugin._pluginDir ? `<button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({pluginId:'${esc(plugin.fullId)}'})">${ICONS.openEditor}</button>` : ''}
|
|
453
|
+
${plugin._pluginDir ? `<button class="modal-action-btn" title="${esc(plugin._pluginDir)}" onclick="copyPluginPath('${escJs(plugin._pluginDir)}', event)">${ICONS.copyPath}</button><button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({pluginId:'${esc(plugin.fullId)}',event})">${ICONS.openEditor}</button>` : ''}
|
|
447
454
|
<button class="detail-close" onclick="closeDetail()">\u2715</button>
|
|
448
455
|
</div>
|
|
449
456
|
</div>
|
|
@@ -505,41 +512,52 @@ function renderDetailComponents(pluginId, comps, hasDirAccess) {
|
|
|
505
512
|
const entries = Object.entries(comps).filter(
|
|
506
513
|
([k, v]) => !k.startsWith('_') && (Array.isArray(v) ? v.length > 0 : v > 0),
|
|
507
514
|
);
|
|
508
|
-
if (!entries.length
|
|
515
|
+
if (!entries.length && !comps._readmePath)
|
|
516
|
+
return '<div style="color:var(--text-dim);font-size:12px">No components found</div>';
|
|
509
517
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
518
|
+
let readmeHtml = '';
|
|
519
|
+
if (comps._readmePath && hasDirAccess) {
|
|
520
|
+
readmeHtml = `<div class="readme-comp-item" onclick="openContentModal('${esc(pluginId)}', '${esc(comps._readmePath)}', 'readme')">
|
|
521
|
+
<span class="icon">${ICONS.readme}</span> ${esc(comps._readmePath)}
|
|
522
|
+
</div>`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
readmeHtml +
|
|
527
|
+
entries
|
|
528
|
+
.map(([type, items]) => {
|
|
529
|
+
const names = Array.isArray(items) ? items : [];
|
|
530
|
+
const count = names.length || items;
|
|
531
|
+
let html = `<div class="detail-comp-group">
|
|
515
532
|
<div class="detail-comp-header">
|
|
516
533
|
<span class="comp-icon">${ICONS[type] || ''}</span>
|
|
517
534
|
${COMP_LABELS[type] || type}
|
|
518
535
|
<span class="count">${count}</span>
|
|
519
536
|
</div>`;
|
|
520
537
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
538
|
+
if (names.length) {
|
|
539
|
+
const configFile = configFiles[type];
|
|
540
|
+
const dir = COMP_HAS_DIR.has(type) ? type : null;
|
|
541
|
+
html += '<div class="detail-comp-items">';
|
|
542
|
+
for (const name of names) {
|
|
543
|
+
const clickPath = configFile || (dir ? `${dir}/${name}` : name);
|
|
544
|
+
const cls = hasDirAccess ? '' : ' disabled';
|
|
545
|
+
const click = hasDirAccess
|
|
546
|
+
? ` onclick="openContentModal('${esc(pluginId)}', '${esc(clickPath)}', '${esc(type)}')"`
|
|
547
|
+
: '';
|
|
548
|
+
html += `<div class="detail-comp-item${cls}"${click}>
|
|
532
549
|
<span class="icon">${type === 'skills' ? ICONS.folder : ICONS.file}</span>
|
|
533
550
|
${esc(name)}
|
|
534
551
|
</div>`;
|
|
552
|
+
}
|
|
553
|
+
html += '</div>';
|
|
535
554
|
}
|
|
536
|
-
html += '</div>';
|
|
537
|
-
}
|
|
538
555
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
556
|
+
html += '</div>';
|
|
557
|
+
return html;
|
|
558
|
+
})
|
|
559
|
+
.join('')
|
|
560
|
+
);
|
|
543
561
|
}
|
|
544
562
|
|
|
545
563
|
const EXT_TO_LANG = {
|
|
@@ -561,6 +579,7 @@ const EXT_TO_LANG = {
|
|
|
561
579
|
const PREFERRED_FILE = 'SKILL.MD';
|
|
562
580
|
let _contentCodeEl = null;
|
|
563
581
|
let _contentPluginId = null;
|
|
582
|
+
let _contentPluginDir = null;
|
|
564
583
|
|
|
565
584
|
function highlightSource(text, fileName) {
|
|
566
585
|
const ext = (fileName || '').split('.').pop().toLowerCase();
|
|
@@ -588,31 +607,94 @@ function getContentCodeEl() {
|
|
|
588
607
|
return _contentCodeEl;
|
|
589
608
|
}
|
|
590
609
|
|
|
591
|
-
async function
|
|
592
|
-
if (!_contentPluginId) return;
|
|
593
|
-
const relativePath = document.getElementById('contentViewerPath').textContent || '';
|
|
610
|
+
async function postAndFlash(endpoint, data, btn) {
|
|
594
611
|
try {
|
|
595
|
-
await fetch(
|
|
612
|
+
await fetch(endpoint, {
|
|
596
613
|
method: 'POST',
|
|
597
614
|
headers: { 'Content-Type': 'application/json' },
|
|
598
|
-
body: JSON.stringify(
|
|
615
|
+
body: JSON.stringify(data),
|
|
599
616
|
});
|
|
617
|
+
if (btn) flashButton(btn);
|
|
600
618
|
} catch {}
|
|
601
619
|
}
|
|
602
620
|
|
|
603
|
-
async function
|
|
621
|
+
async function openInEditor(event) {
|
|
622
|
+
if (!_contentPluginId) return;
|
|
623
|
+
const relativePath = document.getElementById('contentViewerPath').textContent || '';
|
|
624
|
+
await postAndFlash('/api/open-in-editor', { pluginId: _contentPluginId, relativePath }, event?.currentTarget);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const CHECKMARK_SVG =
|
|
628
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
|
|
629
|
+
|
|
630
|
+
function flashButton(btn) {
|
|
631
|
+
if (btn._flashTimeout) clearTimeout(btn._flashTimeout);
|
|
632
|
+
if (!btn._flashOrig) btn._flashOrig = btn.innerHTML;
|
|
633
|
+
btn.innerHTML = CHECKMARK_SVG;
|
|
634
|
+
btn.classList.add('copy-success');
|
|
635
|
+
btn._flashTimeout = setTimeout(() => {
|
|
636
|
+
btn.innerHTML = btn._flashOrig;
|
|
637
|
+
btn.classList.remove('copy-success');
|
|
638
|
+
btn._flashOrig = null;
|
|
639
|
+
btn._flashTimeout = null;
|
|
640
|
+
}, 1000);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function copyToClipboard(text, btn) {
|
|
644
|
+
try {
|
|
645
|
+
await navigator.clipboard.writeText(text);
|
|
646
|
+
} catch {
|
|
647
|
+
const ta = document.createElement('textarea');
|
|
648
|
+
ta.value = text;
|
|
649
|
+
document.body.appendChild(ta);
|
|
650
|
+
ta.select();
|
|
651
|
+
document.execCommand('copy');
|
|
652
|
+
document.body.removeChild(ta);
|
|
653
|
+
}
|
|
654
|
+
if (btn) flashButton(btn);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function copyContentPath(event) {
|
|
658
|
+
if (!_contentPluginDir) return;
|
|
659
|
+
const relativePath = document.getElementById('contentViewerPath').textContent || '';
|
|
660
|
+
const full = relativePath ? `${_contentPluginDir}/${relativePath}` : _contentPluginDir;
|
|
661
|
+
await copyToClipboard(full, event?.currentTarget);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function copyPluginPath(pluginDir, event) {
|
|
665
|
+
if (pluginDir) await copyToClipboard(pluginDir, event?.currentTarget);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function openFolderInEditor({ pluginId, marketplaceName, event } = {}) {
|
|
604
669
|
if (!pluginId && !marketplaceName) return;
|
|
670
|
+
await postAndFlash('/api/open-folder-in-editor', { pluginId, marketplaceName }, event?.currentTarget);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function openReadmeModal(title, fetchUrl) {
|
|
674
|
+
_contentPluginId = null;
|
|
675
|
+
_contentPluginDir = null;
|
|
676
|
+
document.getElementById('contentModalTitle').textContent = `${title} \u2014 README`;
|
|
677
|
+
const tree = document.getElementById('contentTree');
|
|
678
|
+
const codeEl = getContentCodeEl();
|
|
679
|
+
const pathEl = document.getElementById('contentViewerPath');
|
|
680
|
+
tree.innerHTML = '';
|
|
681
|
+
pathEl.textContent = 'README.md';
|
|
682
|
+
codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
|
|
683
|
+
document.getElementById('contentModal').classList.add('open');
|
|
605
684
|
try {
|
|
606
|
-
await fetch(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
685
|
+
const res = await fetch(fetchUrl);
|
|
686
|
+
if (!res.ok) throw new Error('Not found');
|
|
687
|
+
const data = await res.json();
|
|
688
|
+
codeEl.innerHTML = highlightSource(data.content || '', data.name || 'README.md');
|
|
689
|
+
} catch {
|
|
690
|
+
codeEl.innerHTML = '<span style="color:var(--error)">Failed to load README</span>';
|
|
691
|
+
}
|
|
612
692
|
}
|
|
613
693
|
|
|
614
694
|
async function openContentModal(pluginId, initialPath, componentType) {
|
|
615
695
|
_contentPluginId = pluginId;
|
|
696
|
+
const comps = await fetchComponents(pluginId);
|
|
697
|
+
_contentPluginDir = comps?._pluginDir || null;
|
|
616
698
|
const plugin = findPlugin(pluginId);
|
|
617
699
|
const label = COMP_LABELS[componentType] || componentType;
|
|
618
700
|
document.getElementById('contentModalTitle').textContent = `${plugin?.name || pluginId} \u2014 ${label}`;
|
|
@@ -732,7 +814,7 @@ function showMarketplaceDetail(name) {
|
|
|
732
814
|
<div class="detail-header">
|
|
733
815
|
<h3>${ICONS.marketplace} ${esc(m.name)} ${m.version ? `<span class="version">v${esc(m.version)}</span>` : ''}</h3>
|
|
734
816
|
<div class="detail-header-actions">
|
|
735
|
-
${m.installLocation ? `<button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({marketplaceName:'${esc(m.name)}'})">${ICONS.openEditor}</button>` : ''}
|
|
817
|
+
${m.installLocation ? `<button class="modal-action-btn" title="${esc(m.installLocation)}" onclick="copyPluginPath('${escJs(m.installLocation)}', event)">${ICONS.copyPath}</button><button class="modal-action-btn" title="Open in VS Code" onclick="openFolderInEditor({marketplaceName:'${esc(m.name)}',event})">${ICONS.openEditor}</button>` : ''}
|
|
736
818
|
<button class="detail-close" onclick="closeDetail()">\u2715</button>
|
|
737
819
|
</div>
|
|
738
820
|
</div>
|
|
@@ -752,6 +834,7 @@ function showMarketplaceDetail(name) {
|
|
|
752
834
|
<span class="meta-value">${installed} installed / ${total} total</span>
|
|
753
835
|
</div>
|
|
754
836
|
</div>
|
|
837
|
+
${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
838
|
<div class="detail-section">
|
|
756
839
|
<h4>Actions</h4>
|
|
757
840
|
<div class="mkt-actions">
|
|
@@ -968,6 +1051,11 @@ function esc(str) {
|
|
|
968
1051
|
.replace(/'/g, ''');
|
|
969
1052
|
}
|
|
970
1053
|
|
|
1054
|
+
function escJs(str) {
|
|
1055
|
+
if (!str) return '';
|
|
1056
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
971
1059
|
function shortenPath(p) {
|
|
972
1060
|
if (!p) return '';
|
|
973
1061
|
const home = '~';
|
package/public/index.html
CHANGED
|
@@ -158,7 +158,8 @@
|
|
|
158
158
|
<div class="modal-header">
|
|
159
159
|
<h3 id="contentModalTitle">File Preview</h3>
|
|
160
160
|
<div class="modal-header-actions">
|
|
161
|
-
<button class="modal-action-btn" id="
|
|
161
|
+
<button class="modal-action-btn" id="contentCopyPath" title="Copy path" onclick="copyContentPath(event)"></button>
|
|
162
|
+
<button class="modal-action-btn" id="contentOpenEditor" title="Open in VS Code" onclick="openInEditor(event)"></button>
|
|
162
163
|
<button class="modal-close" onclick="closeModal('contentModal')">✕</button>
|
|
163
164
|
</div>
|
|
164
165
|
</div>
|
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
|
|
|
@@ -1017,6 +1042,21 @@ body.light .scope-toggle.local {
|
|
|
1017
1042
|
color: var(--accent);
|
|
1018
1043
|
background: var(--hover);
|
|
1019
1044
|
}
|
|
1045
|
+
.modal-action-btn.copy-success {
|
|
1046
|
+
color: var(--success, #22c55e);
|
|
1047
|
+
animation: copy-flash 0.3s ease;
|
|
1048
|
+
}
|
|
1049
|
+
@keyframes copy-flash {
|
|
1050
|
+
0% {
|
|
1051
|
+
transform: scale(1);
|
|
1052
|
+
}
|
|
1053
|
+
50% {
|
|
1054
|
+
transform: scale(1.3);
|
|
1055
|
+
}
|
|
1056
|
+
100% {
|
|
1057
|
+
transform: scale(1);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1020
1060
|
.modal-close {
|
|
1021
1061
|
background: none;
|
|
1022
1062
|
border: none;
|
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);
|