claude-code-marketplace 0.3.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-marketplace",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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
@@ -91,17 +91,20 @@ document.addEventListener('DOMContentLoaded', () => {
91
91
  document.getElementById('addMarketplaceBtn').addEventListener('click', openAddMarketplace);
92
92
  document.getElementById('helpBtn').addEventListener('click', showHelpModal);
93
93
 
94
- // Enter key in modal
95
- document.getElementById('marketplaceSource').addEventListener('keydown', (e) => {
96
- if (e.key === 'Enter') {
97
- e.preventDefault();
98
- submitAddMarketplace();
99
- }
100
- if (e.key === 'Escape') {
101
- e.preventDefault();
102
- closeModal('addMarketplaceModal');
103
- }
104
- });
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);
105
108
 
106
109
  const savedTheme = localStorage.getItem('theme');
107
110
  if (savedTheme === 'dark') {
@@ -141,6 +144,7 @@ async function loadProject() {
141
144
  const data = await res.json();
142
145
  document.getElementById('projectPath').textContent = shortenPath(data.path);
143
146
  document.getElementById('projectBtn').title = data.path;
147
+ saveRecentProject(data.path);
144
148
  } catch {}
145
149
  }
146
150
 
@@ -181,25 +185,68 @@ async function refresh() {
181
185
  toast('Data refreshed', 'success');
182
186
  }
183
187
 
184
- async function changeProject() {
185
- // Try native directory picker API first, fall back to prompt
186
- let dirPath = null;
187
- if (window.showDirectoryPicker) {
188
- try {
189
- const handle = await window.showDirectoryPicker({ mode: 'read' });
190
- dirPath = handle.name;
191
- // showDirectoryPicker doesn't give full path — fall back to prompt with the name as hint
192
- dirPath = prompt('Confirm project directory (browser cannot read full path):', dirPath);
193
- } catch (e) {
194
- if (e.name === 'AbortError') return;
195
- }
188
+ function getRecentProjects() {
189
+ try {
190
+ return JSON.parse(localStorage.getItem('recentProjects') || '[]');
191
+ } catch {
192
+ return [];
196
193
  }
197
- if (!dirPath) {
198
- const current = document.getElementById('projectBtn').title;
199
- dirPath = prompt('Project directory:', current);
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;
200
216
  }
201
- if (!dirPath) return;
217
+ const escAttr = (s) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
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">&#10005;</button>` +
227
+ `</div>`,
228
+ )
229
+ .join('');
230
+ }
202
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...';
203
250
  try {
204
251
  const res = await fetch('/api/project', {
205
252
  method: 'PUT',
@@ -211,11 +258,15 @@ async function changeProject() {
211
258
  toast(err.error, 'error');
212
259
  return;
213
260
  }
261
+ closeModal('projectPickerModal');
214
262
  await loadProject();
215
263
  await loadData();
216
264
  toast('Project switched', 'success');
217
265
  } catch (err) {
218
266
  toast(err.message, 'error');
267
+ } finally {
268
+ btn.disabled = false;
269
+ btn.textContent = 'Switch';
219
270
  }
220
271
  }
221
272
 
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')">&#10005;</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()">
package/public/style.css CHANGED
@@ -1096,6 +1096,52 @@ body.light .scope-toggle.local {
1096
1096
  margin-top: 16px;
1097
1097
  }
1098
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
+
1099
1145
  /* === HELP MODAL === */
1100
1146
 
1101
1147
  .help-modal {
package/server.js CHANGED
@@ -552,10 +552,11 @@ app.get('/api/plugins/:pluginId/preview/*', (req, res) => {
552
552
  const pluginDir = resolvePluginDir(pluginId, marketplaces);
553
553
  if (!pluginDir) return res.status(404).json({ error: 'Plugin not found' });
554
554
 
555
- const fullPath = path.resolve(pluginDir, relPath);
555
+ let fullPath = path.resolve(pluginDir, relPath);
556
556
  if (!fullPath.startsWith(path.resolve(pluginDir))) {
557
557
  return res.status(403).json({ error: 'Access denied' });
558
558
  }
559
+ if (!fs.existsSync(fullPath) && fs.existsSync(fullPath + '.md')) fullPath += '.md';
559
560
 
560
561
  try {
561
562
  const stat = fs.statSync(fullPath);
@@ -629,9 +630,11 @@ app.get('/api/project', (req, res) => {
629
630
  app.put('/api/project', (req, res) => {
630
631
  const newPath = req.body.path;
631
632
  if (!newPath) return res.status(400).json({ error: 'path required' });
632
- const resolved = newPath.startsWith('~') ? newPath.replace('~', os.homedir()) : newPath;
633
+ const expanded = newPath.startsWith('~') ? newPath.replace('~', os.homedir()) : newPath;
634
+ const resolved = path.resolve(expanded);
633
635
  if (!fs.existsSync(resolved)) return res.status(400).json({ error: 'Directory does not exist' });
634
636
  projectPath = resolved;
637
+ invalidateCache();
635
638
  res.json({ path: projectPath });
636
639
  });
637
640