claude-code-marketplace 0.2.1 → 0.3.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/LICENSE +21 -0
- package/package.json +1 -1
- package/public/app.js +114 -29
- package/public/index.html +26 -1
- package/public/style.css +70 -0
- package/server.js +70 -7
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
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();
|
|
@@ -88,17 +91,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
88
91
|
document.getElementById('addMarketplaceBtn').addEventListener('click', openAddMarketplace);
|
|
89
92
|
document.getElementById('helpBtn').addEventListener('click', showHelpModal);
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
function bindModalKeys(inputId, modalId, onSubmit) {
|
|
95
|
+
document.getElementById(inputId).addEventListener('keydown', (e) => {
|
|
96
|
+
if (e.key === 'Enter') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
onSubmit();
|
|
99
|
+
}
|
|
100
|
+
if (e.key === 'Escape') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
closeModal(modalId);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
bindModalKeys('marketplaceSource', 'addMarketplaceModal', submitAddMarketplace);
|
|
107
|
+
bindModalKeys('projectPathInput', 'projectPickerModal', submitProjectPicker);
|
|
102
108
|
|
|
103
109
|
const savedTheme = localStorage.getItem('theme');
|
|
104
110
|
if (savedTheme === 'dark') {
|
|
@@ -138,6 +144,7 @@ async function loadProject() {
|
|
|
138
144
|
const data = await res.json();
|
|
139
145
|
document.getElementById('projectPath').textContent = shortenPath(data.path);
|
|
140
146
|
document.getElementById('projectBtn').title = data.path;
|
|
147
|
+
saveRecentProject(data.path);
|
|
141
148
|
} catch {}
|
|
142
149
|
}
|
|
143
150
|
|
|
@@ -178,25 +185,68 @@ async function refresh() {
|
|
|
178
185
|
toast('Data refreshed', 'success');
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const handle = await window.showDirectoryPicker({ mode: 'read' });
|
|
187
|
-
dirPath = handle.name;
|
|
188
|
-
// showDirectoryPicker doesn't give full path — fall back to prompt with the name as hint
|
|
189
|
-
dirPath = prompt('Confirm project directory (browser cannot read full path):', dirPath);
|
|
190
|
-
} catch (e) {
|
|
191
|
-
if (e.name === 'AbortError') return;
|
|
192
|
-
}
|
|
188
|
+
function getRecentProjects() {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(localStorage.getItem('recentProjects') || '[]');
|
|
191
|
+
} catch {
|
|
192
|
+
return [];
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function saveRecentProject(projectPath) {
|
|
197
|
+
const recent = getRecentProjects().filter((p) => p !== projectPath);
|
|
198
|
+
recent.unshift(projectPath);
|
|
199
|
+
localStorage.setItem('recentProjects', JSON.stringify(recent.slice(0, 10)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function removeRecentProject(projectPath, e) {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
const recent = getRecentProjects().filter((p) => p !== projectPath);
|
|
205
|
+
localStorage.setItem('recentProjects', JSON.stringify(recent));
|
|
206
|
+
renderRecentProjects();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderRecentProjects() {
|
|
210
|
+
const container = document.getElementById('recentProjectsList');
|
|
211
|
+
const current = document.getElementById('projectBtn').title;
|
|
212
|
+
const recent = getRecentProjects().filter((p) => p !== current);
|
|
213
|
+
if (!recent.length) {
|
|
214
|
+
container.innerHTML = '';
|
|
215
|
+
return;
|
|
197
216
|
}
|
|
198
|
-
|
|
217
|
+
const escAttr = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
218
|
+
const escJs = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
219
|
+
container.innerHTML =
|
|
220
|
+
'<div class="recent-projects-label">Recent</div>' +
|
|
221
|
+
recent
|
|
222
|
+
.map(
|
|
223
|
+
(p) =>
|
|
224
|
+
`<div class="recent-project-item" onclick="selectRecentProject('${escJs(p)}')">` +
|
|
225
|
+
`<span>${escAttr(p)}</span>` +
|
|
226
|
+
`<button class="recent-project-remove" onclick="removeRecentProject('${escJs(p)}', event)" title="Remove">✕</button>` +
|
|
227
|
+
`</div>`,
|
|
228
|
+
)
|
|
229
|
+
.join('');
|
|
230
|
+
}
|
|
199
231
|
|
|
232
|
+
function selectRecentProject(projectPath) {
|
|
233
|
+
document.getElementById('projectPathInput').value = projectPath;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function changeProject() {
|
|
237
|
+
const current = document.getElementById('projectBtn').title;
|
|
238
|
+
document.getElementById('projectPathInput').value = current;
|
|
239
|
+
renderRecentProjects();
|
|
240
|
+
document.getElementById('projectPickerModal').classList.add('open');
|
|
241
|
+
setTimeout(() => document.getElementById('projectPathInput').focus(), 100);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function submitProjectPicker() {
|
|
245
|
+
const dirPath = document.getElementById('projectPathInput').value.trim();
|
|
246
|
+
if (!dirPath) return;
|
|
247
|
+
const btn = document.getElementById('projectPickerSubmit');
|
|
248
|
+
btn.disabled = true;
|
|
249
|
+
btn.textContent = 'Switching...';
|
|
200
250
|
try {
|
|
201
251
|
const res = await fetch('/api/project', {
|
|
202
252
|
method: 'PUT',
|
|
@@ -208,11 +258,15 @@ async function changeProject() {
|
|
|
208
258
|
toast(err.error, 'error');
|
|
209
259
|
return;
|
|
210
260
|
}
|
|
261
|
+
closeModal('projectPickerModal');
|
|
211
262
|
await loadProject();
|
|
212
263
|
await loadData();
|
|
213
264
|
toast('Project switched', 'success');
|
|
214
265
|
} catch (err) {
|
|
215
266
|
toast(err.message, 'error');
|
|
267
|
+
} finally {
|
|
268
|
+
btn.disabled = false;
|
|
269
|
+
btn.textContent = 'Switch';
|
|
216
270
|
}
|
|
217
271
|
}
|
|
218
272
|
|
|
@@ -388,7 +442,10 @@ async function showDetail(pluginId) {
|
|
|
388
442
|
panel.innerHTML = `
|
|
389
443
|
<div class="detail-header">
|
|
390
444
|
<h3>${headerIcon} ${esc(plugin.name)} ${plugin.version ? `<span class="version">v${esc(plugin.version)}</span>` : ''}</h3>
|
|
391
|
-
<
|
|
445
|
+
<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>` : ''}
|
|
447
|
+
<button class="detail-close" onclick="closeDetail()">\u2715</button>
|
|
448
|
+
</div>
|
|
392
449
|
</div>
|
|
393
450
|
<div class="detail-body">
|
|
394
451
|
${updateBanner}
|
|
@@ -503,6 +560,7 @@ const EXT_TO_LANG = {
|
|
|
503
560
|
|
|
504
561
|
const PREFERRED_FILE = 'SKILL.MD';
|
|
505
562
|
let _contentCodeEl = null;
|
|
563
|
+
let _contentPluginId = null;
|
|
506
564
|
|
|
507
565
|
function highlightSource(text, fileName) {
|
|
508
566
|
const ext = (fileName || '').split('.').pop().toLowerCase();
|
|
@@ -530,7 +588,31 @@ function getContentCodeEl() {
|
|
|
530
588
|
return _contentCodeEl;
|
|
531
589
|
}
|
|
532
590
|
|
|
591
|
+
async function openInEditor() {
|
|
592
|
+
if (!_contentPluginId) return;
|
|
593
|
+
const relativePath = document.getElementById('contentViewerPath').textContent || '';
|
|
594
|
+
try {
|
|
595
|
+
await fetch('/api/open-in-editor', {
|
|
596
|
+
method: 'POST',
|
|
597
|
+
headers: { 'Content-Type': 'application/json' },
|
|
598
|
+
body: JSON.stringify({ pluginId: _contentPluginId, relativePath }),
|
|
599
|
+
});
|
|
600
|
+
} catch {}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function openFolderInEditor({ pluginId, marketplaceName } = {}) {
|
|
604
|
+
if (!pluginId && !marketplaceName) return;
|
|
605
|
+
try {
|
|
606
|
+
await fetch('/api/open-folder-in-editor', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: { 'Content-Type': 'application/json' },
|
|
609
|
+
body: JSON.stringify({ pluginId, marketplaceName }),
|
|
610
|
+
});
|
|
611
|
+
} catch {}
|
|
612
|
+
}
|
|
613
|
+
|
|
533
614
|
async function openContentModal(pluginId, initialPath, componentType) {
|
|
615
|
+
_contentPluginId = pluginId;
|
|
534
616
|
const plugin = findPlugin(pluginId);
|
|
535
617
|
const label = COMP_LABELS[componentType] || componentType;
|
|
536
618
|
document.getElementById('contentModalTitle').textContent = `${plugin?.name || pluginId} \u2014 ${label}`;
|
|
@@ -649,7 +731,10 @@ function showMarketplaceDetail(name) {
|
|
|
649
731
|
panel.innerHTML = `
|
|
650
732
|
<div class="detail-header">
|
|
651
733
|
<h3>${ICONS.marketplace} ${esc(m.name)} ${m.version ? `<span class="version">v${esc(m.version)}</span>` : ''}</h3>
|
|
652
|
-
<
|
|
734
|
+
<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>` : ''}
|
|
736
|
+
<button class="detail-close" onclick="closeDetail()">\u2715</button>
|
|
737
|
+
</div>
|
|
653
738
|
</div>
|
|
654
739
|
<div class="detail-body">
|
|
655
740
|
<div class="detail-section">
|
package/public/index.html
CHANGED
|
@@ -95,6 +95,28 @@
|
|
|
95
95
|
</div>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
|
+
<!-- Project picker modal -->
|
|
99
|
+
<div class="modal-overlay" id="projectPickerModal">
|
|
100
|
+
<div class="modal">
|
|
101
|
+
<div class="modal-header">
|
|
102
|
+
<h3>Switch Project</h3>
|
|
103
|
+
<button class="modal-close" onclick="closeModal('projectPickerModal')">✕</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="modal-body">
|
|
106
|
+
<div class="modal-field">
|
|
107
|
+
<label>Project directory</label>
|
|
108
|
+
<input type="text" id="projectPathInput" placeholder="/path/to/project" autocomplete="off">
|
|
109
|
+
<span class="modal-hint">Full path to the project directory</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div id="recentProjectsList"></div>
|
|
112
|
+
<div class="modal-actions">
|
|
113
|
+
<button class="action-btn" onclick="closeModal('projectPickerModal')">Cancel</button>
|
|
114
|
+
<button class="action-btn primary" id="projectPickerSubmit" onclick="submitProjectPicker()">Switch</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
98
120
|
<!-- Help modal -->
|
|
99
121
|
<div class="modal-overlay" id="helpModal">
|
|
100
122
|
<div class="modal help-modal" onclick="event.stopPropagation()">
|
|
@@ -135,7 +157,10 @@
|
|
|
135
157
|
<div class="modal content-modal" onclick="event.stopPropagation()">
|
|
136
158
|
<div class="modal-header">
|
|
137
159
|
<h3 id="contentModalTitle">File Preview</h3>
|
|
138
|
-
<
|
|
160
|
+
<div class="modal-header-actions">
|
|
161
|
+
<button class="modal-action-btn" id="contentOpenEditor" title="Open in VS Code" onclick="openInEditor()"></button>
|
|
162
|
+
<button class="modal-close" onclick="closeModal('contentModal')">✕</button>
|
|
163
|
+
</div>
|
|
139
164
|
</div>
|
|
140
165
|
<div class="content-modal-body">
|
|
141
166
|
<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;
|
|
@@ -1072,6 +1096,52 @@ body.light .scope-toggle.local {
|
|
|
1072
1096
|
margin-top: 16px;
|
|
1073
1097
|
}
|
|
1074
1098
|
|
|
1099
|
+
/* === RECENT PROJECTS === */
|
|
1100
|
+
|
|
1101
|
+
.recent-projects-label {
|
|
1102
|
+
font-size: 11px;
|
|
1103
|
+
color: var(--text-muted);
|
|
1104
|
+
margin-bottom: 6px;
|
|
1105
|
+
text-transform: uppercase;
|
|
1106
|
+
letter-spacing: 0.5px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.recent-project-item {
|
|
1110
|
+
display: flex;
|
|
1111
|
+
align-items: center;
|
|
1112
|
+
gap: 8px;
|
|
1113
|
+
padding: 6px 8px;
|
|
1114
|
+
border-radius: 4px;
|
|
1115
|
+
cursor: pointer;
|
|
1116
|
+
font-size: 13px;
|
|
1117
|
+
color: var(--text-secondary);
|
|
1118
|
+
font-family: var(--font-mono);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.recent-project-item:hover {
|
|
1122
|
+
background: var(--hover-bg);
|
|
1123
|
+
color: var(--text-primary);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.recent-project-remove {
|
|
1127
|
+
margin-left: auto;
|
|
1128
|
+
opacity: 0;
|
|
1129
|
+
background: none;
|
|
1130
|
+
border: none;
|
|
1131
|
+
color: var(--text-muted);
|
|
1132
|
+
cursor: pointer;
|
|
1133
|
+
font-size: 14px;
|
|
1134
|
+
padding: 0 4px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.recent-project-item:hover .recent-project-remove {
|
|
1138
|
+
opacity: 1;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.recent-project-remove:hover {
|
|
1142
|
+
color: var(--danger);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1075
1145
|
/* === HELP MODAL === */
|
|
1076
1146
|
|
|
1077
1147
|
.help-modal {
|
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
|
|
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))
|
|
@@ -540,10 +552,11 @@ app.get('/api/plugins/:pluginId/preview/*', (req, res) => {
|
|
|
540
552
|
const pluginDir = resolvePluginDir(pluginId, marketplaces);
|
|
541
553
|
if (!pluginDir) return res.status(404).json({ error: 'Plugin not found' });
|
|
542
554
|
|
|
543
|
-
|
|
555
|
+
let fullPath = path.resolve(pluginDir, relPath);
|
|
544
556
|
if (!fullPath.startsWith(path.resolve(pluginDir))) {
|
|
545
557
|
return res.status(403).json({ error: 'Access denied' });
|
|
546
558
|
}
|
|
559
|
+
if (!fs.existsSync(fullPath) && fs.existsSync(fullPath + '.md')) fullPath += '.md';
|
|
547
560
|
|
|
548
561
|
try {
|
|
549
562
|
const stat = fs.statSync(fullPath);
|
|
@@ -562,6 +575,54 @@ app.get('/api/plugins/:pluginId/preview/*', (req, res) => {
|
|
|
562
575
|
}
|
|
563
576
|
});
|
|
564
577
|
|
|
578
|
+
function openVSCode(args, res) {
|
|
579
|
+
execFile('code', args, { shell: true }, (err) => {
|
|
580
|
+
if (err) return res.status(500).json({ error: 'Failed to open editor' });
|
|
581
|
+
res.json({ ok: true });
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
app.post('/api/open-in-editor', (req, res) => {
|
|
586
|
+
const { pluginId, relativePath } = req.body;
|
|
587
|
+
if (!pluginId) return res.status(400).json({ error: 'pluginId required' });
|
|
588
|
+
|
|
589
|
+
const marketplaces = getCachedMarketplaces();
|
|
590
|
+
const pluginDir = resolvePluginDir(pluginId, marketplaces);
|
|
591
|
+
if (!pluginDir) return res.status(404).json({ error: 'Plugin not found' });
|
|
592
|
+
|
|
593
|
+
const args = ['-n', pluginDir];
|
|
594
|
+
|
|
595
|
+
const pluginJson = path.join(pluginDir, '.claude-plugin', 'plugin.json');
|
|
596
|
+
if (fs.existsSync(pluginJson)) args.push(pluginJson);
|
|
597
|
+
|
|
598
|
+
if (relativePath) {
|
|
599
|
+
const fullPath = path.resolve(pluginDir, relativePath);
|
|
600
|
+
if (fullPath.startsWith(path.resolve(pluginDir))) {
|
|
601
|
+
args.push(fullPath);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
openVSCode(args, res);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
app.post('/api/open-folder-in-editor', (req, res) => {
|
|
609
|
+
const { pluginId, marketplaceName } = req.body;
|
|
610
|
+
const marketplaces = getCachedMarketplaces();
|
|
611
|
+
let folder;
|
|
612
|
+
|
|
613
|
+
if (pluginId) {
|
|
614
|
+
const plugin = findPlugin(pluginId, marketplaces);
|
|
615
|
+
folder = plugin?._originDir || plugin?._pluginDir || null;
|
|
616
|
+
} else if (marketplaceName) {
|
|
617
|
+
const m = marketplaces.find(m => m.name === marketplaceName);
|
|
618
|
+
folder = m?.installLocation || null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!folder) return res.status(404).json({ error: 'Directory not found' });
|
|
622
|
+
|
|
623
|
+
openVSCode(['-n', folder], res);
|
|
624
|
+
});
|
|
625
|
+
|
|
565
626
|
app.get('/api/project', (req, res) => {
|
|
566
627
|
res.json({ path: projectPath });
|
|
567
628
|
});
|
|
@@ -569,9 +630,11 @@ app.get('/api/project', (req, res) => {
|
|
|
569
630
|
app.put('/api/project', (req, res) => {
|
|
570
631
|
const newPath = req.body.path;
|
|
571
632
|
if (!newPath) return res.status(400).json({ error: 'path required' });
|
|
572
|
-
const
|
|
633
|
+
const expanded = newPath.startsWith('~') ? newPath.replace('~', os.homedir()) : newPath;
|
|
634
|
+
const resolved = path.resolve(expanded);
|
|
573
635
|
if (!fs.existsSync(resolved)) return res.status(400).json({ error: 'Directory does not exist' });
|
|
574
636
|
projectPath = resolved;
|
|
637
|
+
invalidateCache();
|
|
575
638
|
res.json({ path: projectPath });
|
|
576
639
|
});
|
|
577
640
|
|