skillverse 0.1.0

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 (72) hide show
  1. package/.prettierrc +10 -0
  2. package/README.md +369 -0
  3. package/client/README.md +73 -0
  4. package/client/eslint.config.js +23 -0
  5. package/client/index.html +13 -0
  6. package/client/package.json +41 -0
  7. package/client/postcss.config.js +6 -0
  8. package/client/public/vite.svg +1 -0
  9. package/client/src/App.css +42 -0
  10. package/client/src/App.tsx +26 -0
  11. package/client/src/assets/react.svg +1 -0
  12. package/client/src/components/AddSkillDialog.tsx +249 -0
  13. package/client/src/components/Layout.tsx +134 -0
  14. package/client/src/components/LinkWorkspaceDialog.tsx +196 -0
  15. package/client/src/components/LoadingSpinner.tsx +57 -0
  16. package/client/src/components/SkillCard.tsx +269 -0
  17. package/client/src/components/Toast.tsx +44 -0
  18. package/client/src/components/Tooltip.tsx +132 -0
  19. package/client/src/index.css +168 -0
  20. package/client/src/lib/api.ts +196 -0
  21. package/client/src/main.tsx +10 -0
  22. package/client/src/pages/Dashboard.tsx +209 -0
  23. package/client/src/pages/Marketplace.tsx +282 -0
  24. package/client/src/pages/Settings.tsx +136 -0
  25. package/client/src/pages/SkillLibrary.tsx +163 -0
  26. package/client/src/pages/Workspaces.tsx +662 -0
  27. package/client/src/stores/appStore.ts +222 -0
  28. package/client/tailwind.config.js +82 -0
  29. package/client/tsconfig.app.json +28 -0
  30. package/client/tsconfig.json +7 -0
  31. package/client/tsconfig.node.json +26 -0
  32. package/client/vite.config.ts +26 -0
  33. package/package.json +34 -0
  34. package/registry/.env.example +5 -0
  35. package/registry/Dockerfile +42 -0
  36. package/registry/docker-compose.yml +33 -0
  37. package/registry/package.json +37 -0
  38. package/registry/prisma/schema.prisma +59 -0
  39. package/registry/src/index.ts +34 -0
  40. package/registry/src/lib/db.ts +3 -0
  41. package/registry/src/middleware/errorHandler.ts +35 -0
  42. package/registry/src/routes/auth.ts +152 -0
  43. package/registry/src/routes/skills.ts +295 -0
  44. package/registry/tsconfig.json +23 -0
  45. package/server/.env.example +11 -0
  46. package/server/package.json +60 -0
  47. package/server/prisma/schema.prisma +73 -0
  48. package/server/public/assets/index-BsYtpZSa.css +1 -0
  49. package/server/public/assets/index-Dfr_6UV8.js +20 -0
  50. package/server/public/index.html +14 -0
  51. package/server/public/vite.svg +1 -0
  52. package/server/src/bin.ts +428 -0
  53. package/server/src/config.ts +39 -0
  54. package/server/src/index.ts +112 -0
  55. package/server/src/lib/db.ts +14 -0
  56. package/server/src/middleware/errorHandler.ts +40 -0
  57. package/server/src/middleware/logger.ts +12 -0
  58. package/server/src/routes/dashboard.ts +102 -0
  59. package/server/src/routes/marketplace.ts +273 -0
  60. package/server/src/routes/skills.ts +294 -0
  61. package/server/src/routes/workspaces.ts +168 -0
  62. package/server/src/services/bundleService.ts +123 -0
  63. package/server/src/services/skillService.ts +722 -0
  64. package/server/src/services/workspaceService.ts +521 -0
  65. package/server/src/verify-sync.ts +91 -0
  66. package/server/tsconfig.json +19 -0
  67. package/server/tsup.config.ts +18 -0
  68. package/shared/package.json +21 -0
  69. package/shared/pnpm-lock.yaml +24 -0
  70. package/shared/src/index.ts +169 -0
  71. package/shared/tsconfig.json +10 -0
  72. package/tsconfig.json +25 -0
@@ -0,0 +1,662 @@
1
+ import { useEffect, useState, useMemo } from 'react';
2
+ import { useAppStore } from '../stores/appStore';
3
+ import {
4
+ FolderOpen,
5
+ Plus,
6
+ X,
7
+ Loader2,
8
+ Monitor,
9
+ Code,
10
+ MessageSquare,
11
+ Box,
12
+ Trash2,
13
+ Link2,
14
+ AlertTriangle,
15
+ Sparkles,
16
+ Terminal,
17
+ Globe,
18
+ FolderCog,
19
+ ArrowRight,
20
+ CheckCircle,
21
+ XCircle,
22
+ Pencil,
23
+ Check,
24
+ } from 'lucide-react';
25
+ import { LoadingPage, EmptyState } from '../components/LoadingSpinner';
26
+ import { workspacesApi } from '../lib/api';
27
+ import { clsx } from 'clsx';
28
+ import Tooltip from '../components/Tooltip';
29
+ import type { Workspace, WorkspaceType, WorkspaceScope } from '@skillverse/shared';
30
+ import { WORKSPACE_SKILLS_PATHS } from '@skillverse/shared';
31
+
32
+ const workspaceTypeInfo: Record<
33
+ WorkspaceType,
34
+ { icon: React.ComponentType<{ className?: string }>; label: string; color: string }
35
+ > = {
36
+ vscode: { icon: Code, label: 'VS Code', color: 'text-blue-400 bg-blue-500/10' },
37
+ cursor: { icon: Monitor, label: 'Cursor', color: 'text-purple-400 bg-purple-500/10' },
38
+ 'claude-desktop': {
39
+ icon: MessageSquare,
40
+ label: 'Claude Code',
41
+ color: 'text-orange-400 bg-orange-500/10',
42
+ },
43
+ codex: { icon: Terminal, label: 'Codex', color: 'text-green-400 bg-green-500/10' },
44
+ antigravity: { icon: Sparkles, label: 'Antigravity', color: 'text-cyan-400 bg-cyan-500/10' },
45
+ custom: { icon: Box, label: 'Custom', color: 'text-dark-400 bg-dark-700' },
46
+ };
47
+
48
+ const scopeInfo: Record<WorkspaceScope, { icon: React.ComponentType<{ className?: string }>; label: string; description: string }> = {
49
+ project: { icon: FolderCog, label: 'Project', description: 'Skills for a specific project' },
50
+ global: { icon: Globe, label: 'Global', description: 'Skills available across all projects' },
51
+ };
52
+
53
+ export default function Workspaces() {
54
+ const {
55
+ workspaces,
56
+ workspacesLoading,
57
+ fetchWorkspaces,
58
+ addWorkspace,
59
+ removeWorkspace,
60
+ showToast,
61
+ updateWorkspace,
62
+ } = useAppStore();
63
+ const [showAddDialog, setShowAddDialog] = useState(false);
64
+ const [editingWorkspaceId, setEditingWorkspaceId] = useState<string | null>(null);
65
+ const [editName, setEditName] = useState('');
66
+ const [saving, setSaving] = useState(false);
67
+ const [migrationData, setMigrationData] = useState<{
68
+ workspace: Workspace;
69
+ skills: { name: string; hasSkillMd: boolean; path: string }[];
70
+ } | null>(null);
71
+
72
+ useEffect(() => {
73
+ fetchWorkspaces();
74
+ }, [fetchWorkspaces]);
75
+
76
+ const handleDelete = async (workspace: Workspace) => {
77
+ if (
78
+ !confirm(
79
+ `Are you sure you want to delete "${workspace.name}"? This will remove all skill links.`
80
+ )
81
+ )
82
+ return;
83
+
84
+ try {
85
+ await workspacesApi.delete(workspace.id);
86
+ removeWorkspace(workspace.id);
87
+ showToast(`Workspace "${workspace.name}" deleted`, 'success');
88
+ } catch (err: any) {
89
+ showToast(err.message || 'Failed to delete workspace', 'error');
90
+ }
91
+ };
92
+
93
+ const handleWorkspaceAdded = async (workspace: Workspace) => {
94
+ addWorkspace(workspace);
95
+ setShowAddDialog(false);
96
+
97
+ // Check for existing skills in workspace
98
+ try {
99
+ const existingSkills = await workspacesApi.detectSkills(workspace.id);
100
+ if (existingSkills.length > 0) {
101
+ setMigrationData({ workspace, skills: existingSkills });
102
+ } else {
103
+ showToast(`Workspace "${workspace.name}" added successfully!`, 'success');
104
+ }
105
+ } catch (err) {
106
+ // Silent fail - just show success without migration prompt
107
+ showToast(`Workspace "${workspace.name}" added successfully!`, 'success');
108
+ }
109
+ };
110
+
111
+ const handleMigrationComplete = () => {
112
+ setMigrationData(null);
113
+ fetchWorkspaces(); // Refresh to get updated workspace data
114
+ };
115
+
116
+ const handleEditStart = (workspace: Workspace) => {
117
+ setEditingWorkspaceId(workspace.id);
118
+ setEditName(workspace.name);
119
+ };
120
+
121
+ const handleEditCancel = () => {
122
+ setEditingWorkspaceId(null);
123
+ setEditName('');
124
+ };
125
+
126
+ const handleEditSave = async (workspace: Workspace) => {
127
+ if (!editName.trim() || editName === workspace.name) {
128
+ handleEditCancel();
129
+ return;
130
+ }
131
+
132
+ setSaving(true);
133
+ try {
134
+ const updated = await workspacesApi.update(workspace.id, { name: editName });
135
+ updateWorkspace(workspace.id, updated);
136
+ showToast('Workspace name updated', 'success');
137
+ handleEditCancel();
138
+ } catch (err: any) {
139
+ showToast(err.message || 'Failed to update workspace name', 'error');
140
+ } finally {
141
+ setSaving(false);
142
+ }
143
+ };
144
+
145
+ if (workspacesLoading && workspaces.length === 0) {
146
+ return <LoadingPage />;
147
+ }
148
+
149
+ return (
150
+ <div className="space-y-6 animate-fade-in">
151
+ {/* Header - Responsive */}
152
+ <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
153
+ <div className="min-w-0 flex-1">
154
+ <h1 className="text-2xl sm:text-3xl font-bold text-dark-100">Workspaces</h1>
155
+ <p className="text-dark-400 mt-1 text-sm sm:text-base">
156
+ Manage your development workspaces
157
+ </p>
158
+ </div>
159
+ <button onClick={() => setShowAddDialog(true)} className="btn-primary shrink-0 w-full sm:w-auto">
160
+ <Plus className="w-4 h-4" />
161
+ Add Workspace
162
+ </button>
163
+ </div>
164
+
165
+ {/* Workspaces Grid - Enhanced responsive */}
166
+ {workspaces.length > 0 ? (
167
+ <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-6">
168
+ {workspaces.map((workspace: any) => {
169
+ const typeInfo = workspaceTypeInfo[workspace.type as WorkspaceType];
170
+ const TypeIcon = typeInfo?.icon || Box;
171
+ const linkedSkills = workspace.linkedSkills?.length || 0;
172
+ const isPathValid = workspace.isPathValid !== false;
173
+
174
+ return (
175
+ <div key={workspace.id} className="card group">
176
+ <div className="flex items-start justify-between gap-2">
177
+ <div className="flex items-center gap-3 min-w-0 flex-1">
178
+ <div className={clsx('w-10 h-10 rounded-xl flex items-center justify-center shrink-0', typeInfo?.color || 'bg-dark-700')}>
179
+ <TypeIcon className="w-5 h-5" />
180
+ </div>
181
+ <div className="min-w-0 flex-1">
182
+ <div className="flex items-center gap-2">
183
+ {editingWorkspaceId === workspace.id ? (
184
+ <div className="flex items-center gap-1">
185
+ <input
186
+ type="text"
187
+ value={editName}
188
+ onChange={(e) => setEditName(e.target.value)}
189
+ className="px-2 py-0.5 text-sm bg-dark-800 border border-primary-500 rounded text-dark-100 placeholder-dark-500 focus:outline-none w-[120px] sm:w-[160px]"
190
+ autoFocus
191
+ onKeyDown={(e) => {
192
+ if (e.key === 'Enter') handleEditSave(workspace);
193
+ if (e.key === 'Escape') handleEditCancel();
194
+ }}
195
+ onClick={(e) => e.stopPropagation()}
196
+ />
197
+ <button
198
+ onClick={(e) => {
199
+ e.stopPropagation();
200
+ handleEditSave(workspace);
201
+ }}
202
+ className="p-1 rounded-md text-green-400 hover:bg-green-500/10"
203
+ disabled={saving}
204
+ >
205
+ {saving ? (
206
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
207
+ ) : (
208
+ <Check className="w-3.5 h-3.5" />
209
+ )}
210
+ </button>
211
+ <button
212
+ onClick={(e) => {
213
+ e.stopPropagation();
214
+ handleEditCancel();
215
+ }}
216
+ className="p-1 rounded-md text-red-400 hover:bg-red-500/10"
217
+ disabled={saving}
218
+ >
219
+ <X className="w-3.5 h-3.5" />
220
+ </button>
221
+ </div>
222
+ ) : (
223
+ <div className="flex items-center gap-2 group/title">
224
+ <Tooltip content={workspace.name} position="top">
225
+ <h3 className="font-semibold text-dark-100 truncate max-w-[150px] sm:max-w-[200px]">
226
+ {workspace.name}
227
+ </h3>
228
+ </Tooltip>
229
+ <button
230
+ onClick={(e) => {
231
+ e.stopPropagation();
232
+ handleEditStart(workspace);
233
+ }}
234
+ className="opacity-0 group-hover/title:opacity-100 text-dark-400 hover:text-primary-400 transition-all p-0.5 rounded"
235
+ >
236
+ <Pencil className="w-3.5 h-3.5" />
237
+ </button>
238
+ </div>
239
+ )}
240
+ {!isPathValid && (
241
+ <Tooltip content="Skills folder not found - directory may have been deleted" position="top">
242
+ <span className="text-amber-400 shrink-0">
243
+ <AlertTriangle className="w-4 h-4" />
244
+ </span>
245
+ </Tooltip>
246
+ )}
247
+ </div>
248
+ <div className="flex items-center gap-2 text-xs text-dark-500">
249
+ <span>{typeInfo?.label || workspace.type}</span>
250
+ <span>•</span>
251
+ <span className="capitalize">{workspace.scope || 'project'}</span>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ <button
256
+ onClick={() => handleDelete(workspace)}
257
+ className="btn-icon opacity-0 group-hover:opacity-100 transition-opacity text-red-400 hover:text-red-300 shrink-0"
258
+ >
259
+ <Trash2 className="w-4 h-4" />
260
+ </button>
261
+ </div>
262
+
263
+ <div className="mt-4">
264
+ <Tooltip content={workspace.path} position="bottom" className="max-w-full">
265
+ <p className={clsx("text-sm truncate", isPathValid ? "text-dark-400" : "text-amber-400")}>
266
+ {workspace.path}
267
+ </p>
268
+ </Tooltip>
269
+ </div>
270
+
271
+ <div className="mt-4 pt-4 border-t border-dark-700 flex items-center justify-between">
272
+ <div className="flex items-center gap-2 text-xs text-dark-500">
273
+ <Link2 className="w-3.5 h-3.5" />
274
+ <span>{linkedSkills} skill{linkedSkills !== 1 ? 's' : ''} linked</span>
275
+ </div>
276
+ <div className="text-xs text-dark-500">
277
+ {new Date(workspace.createdAt).toLocaleDateString()}
278
+ </div>
279
+ </div>
280
+ </div>
281
+ );
282
+ })}
283
+ </div>
284
+ ) : (
285
+ <EmptyState
286
+ icon={FolderOpen}
287
+ title="No workspaces yet"
288
+ description="Add a workspace to start linking skills to your development environment."
289
+ action={
290
+ <button onClick={() => setShowAddDialog(true)} className="btn-primary">
291
+ <Plus className="w-4 h-4" />
292
+ Add Your First Workspace
293
+ </button>
294
+ }
295
+ />
296
+ )}
297
+
298
+ {/* Add Dialog */}
299
+ {showAddDialog && (
300
+ <AddWorkspaceDialog
301
+ onClose={() => setShowAddDialog(false)}
302
+ onAdd={handleWorkspaceAdded}
303
+ />
304
+ )}
305
+
306
+ {/* Migration Dialog */}
307
+ {migrationData && (
308
+ <MigrationDialog
309
+ workspace={migrationData.workspace}
310
+ skills={migrationData.skills}
311
+ onClose={handleMigrationComplete}
312
+ />
313
+ )}
314
+ </div>
315
+ );
316
+ }
317
+
318
+ function AddWorkspaceDialog({
319
+ onClose,
320
+ onAdd,
321
+ }: {
322
+ onClose: () => void;
323
+ onAdd: (workspace: Workspace) => void;
324
+ }) {
325
+ const [name, setName] = useState('');
326
+ const [projectPath, setProjectPath] = useState('');
327
+ const [type, setType] = useState<WorkspaceType>('antigravity');
328
+ const [scope, setScope] = useState<WorkspaceScope>('project');
329
+ const [loading, setLoading] = useState(false);
330
+ const [error, setError] = useState('');
331
+
332
+ // Compute preview path
333
+ const previewPath = useMemo(() => {
334
+ const config = WORKSPACE_SKILLS_PATHS[type];
335
+ if (scope === 'global') {
336
+ return config.global;
337
+ }
338
+ return projectPath ? `${projectPath}/${config.project}` : config.project;
339
+ }, [type, scope, projectPath]);
340
+
341
+ const handleSubmit = async (e: React.FormEvent) => {
342
+ e.preventDefault();
343
+ if (!name) {
344
+ setError('Name is required');
345
+ return;
346
+ }
347
+ if (scope === 'project' && !projectPath) {
348
+ setError('Project path is required for project scope');
349
+ return;
350
+ }
351
+
352
+ setLoading(true);
353
+ setError('');
354
+
355
+ try {
356
+ const workspace = await workspacesApi.create({ name, projectPath, type, scope });
357
+ onAdd(workspace);
358
+ } catch (err: any) {
359
+ setError(err.message || 'Failed to create workspace');
360
+ } finally {
361
+ setLoading(false);
362
+ }
363
+ };
364
+
365
+ return (
366
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-2 sm:p-4">
367
+ <div className="absolute inset-0 bg-dark-950/80 backdrop-blur-sm" onClick={onClose} />
368
+
369
+ <div className="relative w-full max-w-lg bg-dark-800 border border-dark-700 rounded-2xl shadow-2xl animate-scale-in max-h-[90vh] overflow-y-auto">
370
+ <div className="flex items-center justify-between px-4 sm:px-6 py-4 border-b border-dark-700 sticky top-0 bg-dark-800 z-10">
371
+ <h2 className="text-lg font-semibold text-dark-100">Add Workspace</h2>
372
+ <button onClick={onClose} className="btn-icon">
373
+ <X className="w-5 h-5" />
374
+ </button>
375
+ </div>
376
+
377
+ <form onSubmit={handleSubmit} className="p-4 sm:p-6 space-y-4 sm:space-y-5">
378
+ {/* Name */}
379
+ <div>
380
+ <label className="block text-sm font-medium text-dark-300 mb-2">Name</label>
381
+ <input
382
+ type="text"
383
+ value={name}
384
+ onChange={(e) => setName(e.target.value)}
385
+ placeholder="My Workspace"
386
+ className="input"
387
+ disabled={loading}
388
+ />
389
+ </div>
390
+
391
+ {/* Type */}
392
+ <div>
393
+ <label className="block text-sm font-medium text-dark-300 mb-2">Editor Type</label>
394
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
395
+ {(Object.keys(workspaceTypeInfo) as WorkspaceType[]).map((t) => {
396
+ const info = workspaceTypeInfo[t];
397
+ const Icon = info.icon;
398
+ return (
399
+ <button
400
+ key={t}
401
+ type="button"
402
+ onClick={() => setType(t)}
403
+ className={clsx(
404
+ 'flex items-center gap-2 p-2 sm:p-3 rounded-lg border transition-all',
405
+ type === t
406
+ ? 'bg-primary-500/10 border-primary-500/50 text-dark-100'
407
+ : 'bg-dark-750 border-dark-600 text-dark-400 hover:border-dark-500'
408
+ )}
409
+ disabled={loading}
410
+ >
411
+ <Icon className="w-4 h-4 shrink-0" />
412
+ <span className="text-xs sm:text-sm truncate">{info.label}</span>
413
+ </button>
414
+ );
415
+ })}
416
+ </div>
417
+ </div>
418
+
419
+ {/* Scope */}
420
+ <div>
421
+ <label className="block text-sm font-medium text-dark-300 mb-2">Scope</label>
422
+ <div className="grid grid-cols-2 gap-2">
423
+ {(Object.keys(scopeInfo) as WorkspaceScope[]).map((s) => {
424
+ const info = scopeInfo[s];
425
+ const Icon = info.icon;
426
+ return (
427
+ <button
428
+ key={s}
429
+ type="button"
430
+ onClick={() => setScope(s)}
431
+ className={clsx(
432
+ 'flex flex-col items-start gap-1 p-2 sm:p-3 rounded-lg border transition-all',
433
+ scope === s
434
+ ? 'bg-primary-500/10 border-primary-500/50 text-dark-100'
435
+ : 'bg-dark-750 border-dark-600 text-dark-400 hover:border-dark-500'
436
+ )}
437
+ disabled={loading}
438
+ >
439
+ <div className="flex items-center gap-2">
440
+ <Icon className="w-4 h-4" />
441
+ <span className="text-sm font-medium">{info.label}</span>
442
+ </div>
443
+ <span className="text-xs text-dark-500 text-left">{info.description}</span>
444
+ </button>
445
+ );
446
+ })}
447
+ </div>
448
+ </div>
449
+
450
+ {/* Project Path - only show for project scope */}
451
+ {scope === 'project' && (
452
+ <div>
453
+ <label className="block text-sm font-medium text-dark-300 mb-2">Project Path</label>
454
+ <input
455
+ type="text"
456
+ value={projectPath}
457
+ onChange={(e) => setProjectPath(e.target.value)}
458
+ placeholder="/path/to/your/project"
459
+ className="input"
460
+ disabled={loading}
461
+ />
462
+ </div>
463
+ )}
464
+
465
+ {/* Path Preview */}
466
+ <div className="p-3 rounded-lg bg-dark-750 border border-dark-600">
467
+ <p className="text-xs text-dark-400 mb-1">Skills will be stored in:</p>
468
+ <code className="text-xs sm:text-sm text-primary-400 break-all">{previewPath}</code>
469
+ <p className="text-xs text-dark-500 mt-2">
470
+ The folder will be created automatically if it doesn&apos;t exist.
471
+ </p>
472
+ </div>
473
+
474
+ {error && (
475
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
476
+ {error}
477
+ </div>
478
+ )}
479
+
480
+ <div className="flex gap-3 pt-2">
481
+ <button type="button" onClick={onClose} className="btn-secondary flex-1" disabled={loading}>
482
+ Cancel
483
+ </button>
484
+ <button type="submit" className="btn-primary flex-1" disabled={loading}>
485
+ {loading ? (
486
+ <>
487
+ <Loader2 className="w-4 h-4 animate-spin" />
488
+ Creating...
489
+ </>
490
+ ) : (
491
+ 'Add Workspace'
492
+ )}
493
+ </button>
494
+ </div>
495
+ </form>
496
+ </div>
497
+ </div>
498
+ );
499
+ }
500
+
501
+ function MigrationDialog({
502
+ workspace,
503
+ skills,
504
+ onClose,
505
+ }: {
506
+ workspace: Workspace;
507
+ skills: { name: string; hasSkillMd: boolean; path: string }[];
508
+ onClose: () => void;
509
+ }) {
510
+ const [loading, setLoading] = useState(false);
511
+ const [result, setResult] = useState<{ success: boolean; migrated: string[]; errors: string[] } | null>(null);
512
+ const { showToast, fetchSkills } = useAppStore();
513
+
514
+ const handleMigrate = async () => {
515
+ setLoading(true);
516
+ try {
517
+ const skillNames = skills.map(s => s.name);
518
+ const migrationResult = await workspacesApi.migrateSkills(workspace.id, skillNames);
519
+ setResult(migrationResult);
520
+
521
+ if (migrationResult.migrated.length > 0) {
522
+ showToast(`Migrated ${migrationResult.migrated.length} skill(s) successfully!`, 'success');
523
+ fetchSkills(); // Refresh skills list
524
+ }
525
+ } catch (err: any) {
526
+ showToast(err.message || 'Migration failed', 'error');
527
+ } finally {
528
+ setLoading(false);
529
+ }
530
+ };
531
+
532
+ const handleSkip = () => {
533
+ showToast(`Workspace "${workspace.name}" added successfully!`, 'success');
534
+ onClose();
535
+ };
536
+
537
+ return (
538
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-2 sm:p-4">
539
+ <div className="absolute inset-0 bg-dark-950/80 backdrop-blur-sm" onClick={result ? onClose : undefined} />
540
+
541
+ <div className="relative w-full max-w-lg bg-dark-800 border border-dark-700 rounded-2xl shadow-2xl animate-scale-in max-h-[90vh] overflow-y-auto">
542
+ <div className="flex items-center justify-between px-4 sm:px-6 py-4 border-b border-dark-700 sticky top-0 bg-dark-800 z-10">
543
+ <h2 className="text-lg font-semibold text-dark-100">
544
+ {result ? 'Migration Complete' : 'Existing Skills Detected'}
545
+ </h2>
546
+ {result && (
547
+ <button onClick={onClose} className="btn-icon">
548
+ <X className="w-5 h-5" />
549
+ </button>
550
+ )}
551
+ </div>
552
+
553
+ <div className="p-4 sm:p-6 space-y-4">
554
+ {!result ? (
555
+ <>
556
+ <div className="p-3 sm:p-4 rounded-lg bg-amber-500/10 border border-amber-500/30">
557
+ <p className="text-amber-400 text-sm text-justified">
558
+ Found <strong>{skills.length}</strong> existing skill(s) in this workspace.
559
+ Would you like to migrate them to unified management?
560
+ </p>
561
+ </div>
562
+
563
+ <div className="space-y-2 max-h-48 overflow-y-auto">
564
+ {skills.map((skill) => (
565
+ <div key={skill.name} className="flex items-center gap-3 p-3 rounded-lg bg-dark-750 border border-dark-600">
566
+ <Box className="w-4 h-4 text-primary-400 shrink-0" />
567
+ <div className="flex-1 min-w-0">
568
+ <p className="text-sm font-medium text-dark-100 truncate">{skill.name}</p>
569
+ <Tooltip content={skill.path} position="bottom">
570
+ <p className="text-xs text-dark-500 truncate lg:max-w-[320px]">{skill.path}</p>
571
+ </Tooltip>
572
+ </div>
573
+ {skill.hasSkillMd && (
574
+ <span className="px-2 py-0.5 text-xs rounded bg-green-500/10 text-green-400 shrink-0">
575
+ SKILL.md
576
+ </span>
577
+ )}
578
+ </div>
579
+ ))}
580
+ </div>
581
+
582
+ <div className="p-3 rounded-lg bg-dark-750 border border-dark-600">
583
+ <p className="text-xs text-dark-400 mb-2">Migration will:</p>
584
+ <ul className="text-xs text-dark-500 space-y-1 ml-3">
585
+ <li className="flex items-center gap-2">
586
+ <ArrowRight className="w-3 h-3 text-primary-400 shrink-0" />
587
+ <span>Move skills to <code className="text-primary-400">~/.skillverse/skills/</code></span>
588
+ </li>
589
+ <li className="flex items-center gap-2">
590
+ <ArrowRight className="w-3 h-3 text-primary-400 shrink-0" />
591
+ <span>Register them in SkillVerse for unified management</span>
592
+ </li>
593
+ <li className="flex items-center gap-2">
594
+ <ArrowRight className="w-3 h-3 text-primary-400 shrink-0" />
595
+ <span>Create symlinks back to this workspace</span>
596
+ </li>
597
+ </ul>
598
+ </div>
599
+
600
+ <div className="flex gap-3 pt-2">
601
+ <button onClick={handleSkip} className="btn-secondary flex-1" disabled={loading}>
602
+ No, Keep as is
603
+ </button>
604
+ <button onClick={handleMigrate} className="btn-primary flex-1" disabled={loading}>
605
+ {loading ? (
606
+ <>
607
+ <Loader2 className="w-4 h-4 animate-spin" />
608
+ Migrating...
609
+ </>
610
+ ) : (
611
+ 'Yes, Migrate'
612
+ )}
613
+ </button>
614
+ </div>
615
+ </>
616
+ ) : (
617
+ <>
618
+ {result.migrated.length > 0 && (
619
+ <div className="space-y-2">
620
+ <p className="text-sm font-medium text-green-400 flex items-center gap-2">
621
+ <CheckCircle className="w-4 h-4" />
622
+ Successfully migrated ({result.migrated.length})
623
+ </p>
624
+ <div className="space-y-1">
625
+ {result.migrated.map((name) => (
626
+ <div key={name} className="flex items-center gap-2 p-2 rounded bg-green-500/10 text-sm text-dark-100">
627
+ <Box className="w-3.5 h-3.5 text-green-400" />
628
+ <span className="truncate">{name}</span>
629
+ </div>
630
+ ))}
631
+ </div>
632
+ </div>
633
+ )}
634
+
635
+ {result.errors.length > 0 && (
636
+ <div className="space-y-2">
637
+ <p className="text-sm font-medium text-red-400 flex items-center gap-2">
638
+ <XCircle className="w-4 h-4" />
639
+ Errors ({result.errors.length})
640
+ </p>
641
+ <div className="space-y-1">
642
+ {result.errors.map((err, i) => (
643
+ <div key={i} className="p-2 rounded bg-red-500/10 text-sm text-red-400 break-words">
644
+ {err}
645
+ </div>
646
+ ))}
647
+ </div>
648
+ </div>
649
+ )}
650
+
651
+ <div className="flex justify-end pt-2">
652
+ <button onClick={onClose} className="btn-primary">
653
+ Done
654
+ </button>
655
+ </div>
656
+ </>
657
+ )}
658
+ </div>
659
+ </div>
660
+ </div>
661
+ );
662
+ }