groove-dev 0.26.12 → 0.26.14

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.
@@ -19,7 +19,7 @@ import { fmtNum, timeAgo } from '../lib/format';
19
19
  import { useGrooveStore } from '../stores/groove';
20
20
  import {
21
21
  ChevronLeft, ChevronDown, Sparkles, Plug, LogIn, LogOut,
22
- User, Upload, Package, Download, ShoppingBag,
22
+ User, Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
23
23
  } from 'lucide-react';
24
24
 
25
25
  // ── Skill Detail ─────────────────────────────────────────
@@ -28,6 +28,8 @@ function SkillDetail({ skill, onBack }) {
28
28
  const [content, setContent] = useState('');
29
29
  const [requiresPurchase, setRequiresPurchase] = useState(false);
30
30
  const [installing, setInstalling] = useState(false);
31
+ const [updating, setUpdating] = useState(false);
32
+ const [uninstalling, setUninstalling] = useState(false);
31
33
  const [installed, setInstalled] = useState(skill.installed);
32
34
  const [loadingContent, setLoadingContent] = useState(true);
33
35
 
@@ -52,6 +54,28 @@ function SkillDetail({ skill, onBack }) {
52
54
  setInstalling(false);
53
55
  }
54
56
 
57
+ async function handleUpdate() {
58
+ setUpdating(true);
59
+ try {
60
+ await api.post(`/skills/${skill.id}/update`);
61
+ toast.success(`${skill.name} updated to latest`);
62
+ // Refresh content preview
63
+ const d = await api.get(`/skills/${skill.id}/content`);
64
+ if (d.content) setContent(d.content);
65
+ } catch (err) { toast.error('Update failed', err.message); }
66
+ setUpdating(false);
67
+ }
68
+
69
+ async function handleUninstall() {
70
+ setUninstalling(true);
71
+ try {
72
+ await api.delete(`/skills/${skill.id}`);
73
+ setInstalled(false);
74
+ toast.success(`${skill.name} uninstalled`);
75
+ } catch (err) { toast.error('Uninstall failed', err.message); }
76
+ setUninstalling(false);
77
+ }
78
+
55
79
  async function handleBuy() {
56
80
  const { marketplaceAuthenticated, marketplaceLogin, marketplaceCheckout } = useGrooveStore.getState();
57
81
  if (!marketplaceAuthenticated) {
@@ -133,13 +157,32 @@ function SkillDetail({ skill, onBack }) {
133
157
  >
134
158
  Buy ${(skill.price || 0).toFixed(2)}
135
159
  </button>
160
+ ) : installed ? (
161
+ <div className="mt-3 flex flex-col gap-1.5">
162
+ <button
163
+ onClick={handleUpdate}
164
+ disabled={updating}
165
+ className="w-full py-2 px-3 text-xs font-sans font-semibold rounded cursor-pointer transition-all hover:opacity-85 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2 border bg-accent/15 text-accent border-accent/20 hover:bg-accent/25"
166
+ >
167
+ <RefreshCw size={12} className={updating ? 'animate-spin' : ''} />
168
+ {updating ? 'Updating...' : 'Pull Latest'}
169
+ </button>
170
+ <button
171
+ onClick={handleUninstall}
172
+ disabled={uninstalling}
173
+ className="w-full py-2 px-3 text-xs font-sans font-semibold rounded cursor-pointer transition-all hover:opacity-85 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2 border bg-error/10 text-error border-error/20 hover:bg-error/15"
174
+ >
175
+ <Trash2 size={12} />
176
+ {uninstalling ? 'Removing...' : 'Uninstall'}
177
+ </button>
178
+ </div>
136
179
  ) : (
137
180
  <button
138
181
  onClick={handleInstall}
139
- disabled={installing || installed}
140
- className={`w-full mt-3 py-2 px-3 text-xs font-sans font-semibold rounded cursor-pointer transition-all hover:opacity-85 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2 border ${installed ? 'bg-success/15 text-success border-success/20' : 'bg-accent/15 text-accent border-accent/20 hover:bg-accent/25'}`}
182
+ disabled={installing}
183
+ className="w-full mt-3 py-2 px-3 text-xs font-sans font-semibold rounded cursor-pointer transition-all hover:opacity-85 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2 border bg-accent/15 text-accent border-accent/20 hover:bg-accent/25"
141
184
  >
142
- {installing ? 'Installing...' : installed ? '\u2713 Installed' : 'Install'}
185
+ {installing ? 'Installing...' : 'Install'}
143
186
  </button>
144
187
  )}
145
188
 
@@ -278,9 +321,15 @@ function MyLibrary() {
278
321
  const [purchases, setPurchases] = useState([]);
279
322
  const [installed, setInstalled] = useState([]);
280
323
  const [loading, setLoading] = useState(true);
324
+ const [busyId, setBusyId] = useState(null);
281
325
  const toast = useToast();
282
326
  const fileRef = useRef(null);
283
327
 
328
+ const refreshInstalled = async () => {
329
+ const data = await api.get('/skills/installed');
330
+ setInstalled(Array.isArray(data) ? data : data.skills || []);
331
+ };
332
+
284
333
  useEffect(() => {
285
334
  setLoading(true);
286
335
  Promise.all([
@@ -292,6 +341,26 @@ function MyLibrary() {
292
341
  }).finally(() => setLoading(false));
293
342
  }, [authenticated]);
294
343
 
344
+ async function handleUpdate(s) {
345
+ setBusyId(s.id);
346
+ try {
347
+ await api.post(`/skills/${s.id}/update`);
348
+ toast.success(`${s.name || s.id} updated`);
349
+ await refreshInstalled();
350
+ } catch (err) { toast.error('Update failed', err.message); }
351
+ setBusyId(null);
352
+ }
353
+
354
+ async function handleUninstall(s) {
355
+ setBusyId(s.id);
356
+ try {
357
+ await api.delete(`/skills/${s.id}`);
358
+ toast.success(`${s.name || s.id} uninstalled`);
359
+ await refreshInstalled();
360
+ } catch (err) { toast.error('Uninstall failed', err.message); }
361
+ setBusyId(null);
362
+ }
363
+
295
364
  async function handleImport(e) {
296
365
  const file = e.target.files?.[0];
297
366
  if (!file) return;
@@ -300,9 +369,7 @@ function MyLibrary() {
300
369
  const name = file.name.replace(/\.md$/i, '');
301
370
  await api.post('/skills/import', { name, content });
302
371
  toast.success(`Imported "${name}"`);
303
- // Refresh installed list
304
- const data = await api.get('/skills/installed');
305
- setInstalled(Array.isArray(data) ? data : data.skills || []);
372
+ await refreshInstalled();
306
373
  } catch (err) {
307
374
  toast.error('Import failed', err.message);
308
375
  }
@@ -398,6 +465,24 @@ function MyLibrary() {
398
465
  <div className="text-xs font-semibold text-text-0 font-sans truncate">{s.name || s.id}</div>
399
466
  <div className="text-2xs text-text-3 font-sans truncate">{s.description || s.category || 'local skill'}</div>
400
467
  </div>
468
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
469
+ <button
470
+ onClick={() => handleUpdate(s)}
471
+ disabled={busyId === s.id}
472
+ title="Pull latest version"
473
+ className="p-1.5 rounded text-text-3 hover:text-accent hover:bg-accent/10 cursor-pointer transition-colors disabled:opacity-50"
474
+ >
475
+ <RefreshCw size={12} className={busyId === s.id ? 'animate-spin' : ''} />
476
+ </button>
477
+ <button
478
+ onClick={() => handleUninstall(s)}
479
+ disabled={busyId === s.id}
480
+ title="Uninstall"
481
+ className="p-1.5 rounded text-text-3 hover:text-error hover:bg-error/10 cursor-pointer transition-colors disabled:opacity-50"
482
+ >
483
+ <Trash2 size={12} />
484
+ </button>
485
+ </div>
401
486
  <Badge variant="accent" className="text-2xs flex-shrink-0">Installed</Badge>
402
487
  </div>
403
488
  ))}