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.
- package/.prettierrc +10 -0
- package/README.md +369 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +13 -0
- package/client/package.json +41 -0
- package/client/postcss.config.js +6 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.css +42 -0
- package/client/src/App.tsx +26 -0
- package/client/src/assets/react.svg +1 -0
- package/client/src/components/AddSkillDialog.tsx +249 -0
- package/client/src/components/Layout.tsx +134 -0
- package/client/src/components/LinkWorkspaceDialog.tsx +196 -0
- package/client/src/components/LoadingSpinner.tsx +57 -0
- package/client/src/components/SkillCard.tsx +269 -0
- package/client/src/components/Toast.tsx +44 -0
- package/client/src/components/Tooltip.tsx +132 -0
- package/client/src/index.css +168 -0
- package/client/src/lib/api.ts +196 -0
- package/client/src/main.tsx +10 -0
- package/client/src/pages/Dashboard.tsx +209 -0
- package/client/src/pages/Marketplace.tsx +282 -0
- package/client/src/pages/Settings.tsx +136 -0
- package/client/src/pages/SkillLibrary.tsx +163 -0
- package/client/src/pages/Workspaces.tsx +662 -0
- package/client/src/stores/appStore.ts +222 -0
- package/client/tailwind.config.js +82 -0
- package/client/tsconfig.app.json +28 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +26 -0
- package/package.json +34 -0
- package/registry/.env.example +5 -0
- package/registry/Dockerfile +42 -0
- package/registry/docker-compose.yml +33 -0
- package/registry/package.json +37 -0
- package/registry/prisma/schema.prisma +59 -0
- package/registry/src/index.ts +34 -0
- package/registry/src/lib/db.ts +3 -0
- package/registry/src/middleware/errorHandler.ts +35 -0
- package/registry/src/routes/auth.ts +152 -0
- package/registry/src/routes/skills.ts +295 -0
- package/registry/tsconfig.json +23 -0
- package/server/.env.example +11 -0
- package/server/package.json +60 -0
- package/server/prisma/schema.prisma +73 -0
- package/server/public/assets/index-BsYtpZSa.css +1 -0
- package/server/public/assets/index-Dfr_6UV8.js +20 -0
- package/server/public/index.html +14 -0
- package/server/public/vite.svg +1 -0
- package/server/src/bin.ts +428 -0
- package/server/src/config.ts +39 -0
- package/server/src/index.ts +112 -0
- package/server/src/lib/db.ts +14 -0
- package/server/src/middleware/errorHandler.ts +40 -0
- package/server/src/middleware/logger.ts +12 -0
- package/server/src/routes/dashboard.ts +102 -0
- package/server/src/routes/marketplace.ts +273 -0
- package/server/src/routes/skills.ts +294 -0
- package/server/src/routes/workspaces.ts +168 -0
- package/server/src/services/bundleService.ts +123 -0
- package/server/src/services/skillService.ts +722 -0
- package/server/src/services/workspaceService.ts +521 -0
- package/server/src/verify-sync.ts +91 -0
- package/server/tsconfig.json +19 -0
- package/server/tsup.config.ts +18 -0
- package/shared/package.json +21 -0
- package/shared/pnpm-lock.yaml +24 -0
- package/shared/src/index.ts +169 -0
- package/shared/tsconfig.json +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { Skill, Workspace, DashboardStats, MarketplaceSkill } from '@skillverse/shared';
|
|
3
|
+
import { skillsApi, workspacesApi, dashboardApi, marketplaceApi } from '../lib/api';
|
|
4
|
+
|
|
5
|
+
interface AppState {
|
|
6
|
+
// Skills
|
|
7
|
+
skills: Skill[];
|
|
8
|
+
skillsLoading: boolean;
|
|
9
|
+
skillsError: string | null;
|
|
10
|
+
fetchSkills: () => Promise<void>;
|
|
11
|
+
addSkill: (skill: Skill) => void;
|
|
12
|
+
updateSkill: (id: string, skill: Partial<Skill>) => void;
|
|
13
|
+
removeSkill: (id: string) => void;
|
|
14
|
+
checkUpdate: (id: string) => Promise<void>;
|
|
15
|
+
checkAllUpdates: () => Promise<void>;
|
|
16
|
+
upgradeSkill: (id: string) => Promise<void>;
|
|
17
|
+
skillUpdates: Record<string, boolean>; // Map of skillId -> hasUpdate
|
|
18
|
+
skillUpdateChecking: Record<string, boolean>; // Map of skillId -> isChecking
|
|
19
|
+
|
|
20
|
+
// Workspaces
|
|
21
|
+
workspaces: Workspace[];
|
|
22
|
+
workspacesLoading: boolean;
|
|
23
|
+
workspacesError: string | null;
|
|
24
|
+
fetchWorkspaces: () => Promise<void>;
|
|
25
|
+
addWorkspace: (workspace: Workspace) => void;
|
|
26
|
+
updateWorkspace: (id: string, workspace: Partial<Workspace>) => void;
|
|
27
|
+
removeWorkspace: (id: string) => void;
|
|
28
|
+
|
|
29
|
+
// Dashboard
|
|
30
|
+
stats: DashboardStats | null;
|
|
31
|
+
statsLoading: boolean;
|
|
32
|
+
fetchStats: () => Promise<void>;
|
|
33
|
+
|
|
34
|
+
// Marketplace
|
|
35
|
+
marketplaceSkills: MarketplaceSkill[];
|
|
36
|
+
marketplaceLoading: boolean;
|
|
37
|
+
marketplaceTotal: number;
|
|
38
|
+
fetchMarketplace: (page?: number, search?: string) => Promise<void>;
|
|
39
|
+
|
|
40
|
+
// UI State
|
|
41
|
+
toast: { message: string; type: 'success' | 'error' | 'info' } | null;
|
|
42
|
+
showToast: (message: string, type?: 'success' | 'error' | 'info') => void;
|
|
43
|
+
hideToast: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const useAppStore = create<AppState>((set, get) => ({
|
|
47
|
+
// Skills state
|
|
48
|
+
skills: [],
|
|
49
|
+
skillsLoading: false,
|
|
50
|
+
skillsError: null,
|
|
51
|
+
|
|
52
|
+
fetchSkills: async () => {
|
|
53
|
+
set({ skillsLoading: true, skillsError: null });
|
|
54
|
+
try {
|
|
55
|
+
const skills = await skillsApi.getAll();
|
|
56
|
+
set({ skills, skillsLoading: false });
|
|
57
|
+
} catch (error: any) {
|
|
58
|
+
set({ skillsError: error.message, skillsLoading: false });
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
addSkill: (skill) => {
|
|
63
|
+
set((state) => ({ skills: [skill, ...state.skills] }));
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
updateSkill: (id, updatedSkill) => {
|
|
67
|
+
set((state) => ({
|
|
68
|
+
skills: state.skills.map((s) => (s.id === id ? { ...s, ...updatedSkill } : s)),
|
|
69
|
+
}));
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
removeSkill: (id) => {
|
|
73
|
+
set((state) => ({ skills: state.skills.filter((s) => s.id !== id) }));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
skillUpdates: {},
|
|
77
|
+
skillUpdateChecking: {},
|
|
78
|
+
|
|
79
|
+
checkUpdate: async (id) => {
|
|
80
|
+
set((state) => ({
|
|
81
|
+
skillUpdateChecking: { ...state.skillUpdateChecking, [id]: true }
|
|
82
|
+
}));
|
|
83
|
+
try {
|
|
84
|
+
const result = await skillsApi.checkUpdate(id);
|
|
85
|
+
if (result.hasUpdate) {
|
|
86
|
+
set((state) => ({
|
|
87
|
+
skillUpdates: { ...state.skillUpdates, [id]: true }
|
|
88
|
+
}));
|
|
89
|
+
// Optionally show toast or notification
|
|
90
|
+
get().showToast(`Update available for skill`, 'info');
|
|
91
|
+
} else {
|
|
92
|
+
get().showToast(`Skill is up to date`, 'success');
|
|
93
|
+
}
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
get().showToast(error.message || 'Failed to check for updates', 'error');
|
|
96
|
+
} finally {
|
|
97
|
+
set((state) => ({
|
|
98
|
+
skillUpdateChecking: { ...state.skillUpdateChecking, [id]: false }
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
checkAllUpdates: async () => {
|
|
104
|
+
// Mark all git skills as checking? Or adds a global checking state?
|
|
105
|
+
// For now just background check
|
|
106
|
+
try {
|
|
107
|
+
const results = await skillsApi.checkUpdates();
|
|
108
|
+
const updates: Record<string, boolean> = {};
|
|
109
|
+
|
|
110
|
+
Object.entries(results).forEach(([id, result]) => {
|
|
111
|
+
if (result.hasUpdate) {
|
|
112
|
+
updates[id] = true;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (Object.keys(updates).length > 0) {
|
|
117
|
+
set(state => ({
|
|
118
|
+
skillUpdates: { ...state.skillUpdates, ...updates }
|
|
119
|
+
}));
|
|
120
|
+
get().showToast(`${Object.keys(updates).length} skills have updates available`, 'info');
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Failed to check all updates:', error);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
upgradeSkill: async (id) => {
|
|
128
|
+
// Can add loading state for upgrade if needed, for now reuse skillsLoading or local state
|
|
129
|
+
set({ skillsLoading: true });
|
|
130
|
+
try {
|
|
131
|
+
const upgradedSkill = await skillsApi.upgrade(id);
|
|
132
|
+
set((state) => ({
|
|
133
|
+
skills: state.skills.map(s => s.id === id ? upgradedSkill : s),
|
|
134
|
+
skillUpdates: { ...state.skillUpdates, [id]: false }, // Clear update flag
|
|
135
|
+
skillsLoading: false
|
|
136
|
+
}));
|
|
137
|
+
get().showToast(`Skill "${upgradedSkill.name}" upgraded successfully`, 'success');
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
set({ skillsLoading: false });
|
|
140
|
+
get().showToast(error.message || 'Failed to upgrade skill', 'error');
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Workspaces state
|
|
145
|
+
workspaces: [],
|
|
146
|
+
workspacesLoading: false,
|
|
147
|
+
workspacesError: null,
|
|
148
|
+
|
|
149
|
+
fetchWorkspaces: async () => {
|
|
150
|
+
set({ workspacesLoading: true, workspacesError: null });
|
|
151
|
+
try {
|
|
152
|
+
const workspaces = await workspacesApi.getAll();
|
|
153
|
+
set({ workspaces, workspacesLoading: false });
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
set({ workspacesError: error.message, workspacesLoading: false });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
addWorkspace: (workspace) => {
|
|
160
|
+
set((state) => ({ workspaces: [workspace, ...state.workspaces] }));
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
updateWorkspace: (id, updatedWorkspace) => {
|
|
164
|
+
set((state) => ({
|
|
165
|
+
workspaces: state.workspaces.map((w) => (w.id === id ? { ...w, ...updatedWorkspace } : w)),
|
|
166
|
+
}));
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
removeWorkspace: (id) => {
|
|
170
|
+
set((state) => ({ workspaces: state.workspaces.filter((w) => w.id !== id) }));
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Dashboard state
|
|
174
|
+
stats: null,
|
|
175
|
+
statsLoading: false,
|
|
176
|
+
|
|
177
|
+
fetchStats: async () => {
|
|
178
|
+
set({ statsLoading: true });
|
|
179
|
+
try {
|
|
180
|
+
const stats = await dashboardApi.getStats();
|
|
181
|
+
set({ stats, statsLoading: false });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
set({ statsLoading: false });
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Marketplace state
|
|
188
|
+
marketplaceSkills: [],
|
|
189
|
+
marketplaceLoading: false,
|
|
190
|
+
marketplaceTotal: 0,
|
|
191
|
+
|
|
192
|
+
fetchMarketplace: async (page = 1, search) => {
|
|
193
|
+
set({ marketplaceLoading: true });
|
|
194
|
+
try {
|
|
195
|
+
const result = await marketplaceApi.getSkills(page, 20, search);
|
|
196
|
+
set({
|
|
197
|
+
marketplaceSkills: result.items,
|
|
198
|
+
marketplaceTotal: result.total,
|
|
199
|
+
marketplaceLoading: false,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
set({ marketplaceLoading: false });
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// UI State
|
|
207
|
+
toast: null,
|
|
208
|
+
|
|
209
|
+
showToast: (message, type = 'info') => {
|
|
210
|
+
set({ toast: { message, type } });
|
|
211
|
+
// Auto hide after 4 seconds
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (get().toast?.message === message) {
|
|
214
|
+
set({ toast: null });
|
|
215
|
+
}
|
|
216
|
+
}, 4000);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
hideToast: () => {
|
|
220
|
+
set({ toast: null });
|
|
221
|
+
},
|
|
222
|
+
}));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
"./index.html",
|
|
5
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
6
|
+
],
|
|
7
|
+
darkMode: 'class',
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
colors: {
|
|
11
|
+
primary: {
|
|
12
|
+
50: '#f0f9ff',
|
|
13
|
+
100: '#e0f2fe',
|
|
14
|
+
200: '#bae6fd',
|
|
15
|
+
300: '#7dd3fc',
|
|
16
|
+
400: '#38bdf8',
|
|
17
|
+
500: '#0ea5e9',
|
|
18
|
+
600: '#0284c7',
|
|
19
|
+
700: '#0369a1',
|
|
20
|
+
800: '#075985',
|
|
21
|
+
900: '#0c4a6e',
|
|
22
|
+
950: '#082f49',
|
|
23
|
+
},
|
|
24
|
+
accent: {
|
|
25
|
+
50: '#fdf4ff',
|
|
26
|
+
100: '#fae8ff',
|
|
27
|
+
200: '#f5d0fe',
|
|
28
|
+
300: '#f0abfc',
|
|
29
|
+
400: '#e879f9',
|
|
30
|
+
500: '#d946ef',
|
|
31
|
+
600: '#c026d3',
|
|
32
|
+
700: '#a21caf',
|
|
33
|
+
800: '#86198f',
|
|
34
|
+
900: '#701a75',
|
|
35
|
+
950: '#4a044e',
|
|
36
|
+
},
|
|
37
|
+
dark: {
|
|
38
|
+
50: '#f8fafc',
|
|
39
|
+
100: '#f1f5f9',
|
|
40
|
+
200: '#e2e8f0',
|
|
41
|
+
300: '#cbd5e1',
|
|
42
|
+
400: '#94a3b8',
|
|
43
|
+
500: '#64748b',
|
|
44
|
+
600: '#475569',
|
|
45
|
+
700: '#334155',
|
|
46
|
+
800: '#1e293b',
|
|
47
|
+
900: '#0f172a',
|
|
48
|
+
950: '#020617',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
animation: {
|
|
52
|
+
'fade-in': 'fadeIn 0.3s ease-out',
|
|
53
|
+
'slide-in': 'slideIn 0.3s ease-out',
|
|
54
|
+
'scale-in': 'scaleIn 0.2s ease-out',
|
|
55
|
+
'pulse-subtle': 'pulseSubtle 2s ease-in-out infinite',
|
|
56
|
+
},
|
|
57
|
+
keyframes: {
|
|
58
|
+
fadeIn: {
|
|
59
|
+
'0%': { opacity: '0' },
|
|
60
|
+
'100%': { opacity: '1' },
|
|
61
|
+
},
|
|
62
|
+
slideIn: {
|
|
63
|
+
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
|
64
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
65
|
+
},
|
|
66
|
+
scaleIn: {
|
|
67
|
+
'0%': { opacity: '0', transform: 'scale(0.95)' },
|
|
68
|
+
'100%': { opacity: '1', transform: 'scale(1)' },
|
|
69
|
+
},
|
|
70
|
+
pulseSubtle: {
|
|
71
|
+
'0%, 100%': { opacity: '1' },
|
|
72
|
+
'50%': { opacity: '0.8' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
boxShadow: {
|
|
76
|
+
'glow': '0 0 20px rgba(14, 165, 233, 0.3)',
|
|
77
|
+
'glow-accent': '0 0 20px rgba(217, 70, 239, 0.3)',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
plugins: [],
|
|
82
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
// https://vite.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@': path.resolve(__dirname, './src'),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
build: {
|
|
14
|
+
outDir: '../server/public',
|
|
15
|
+
emptyOutDir: true,
|
|
16
|
+
},
|
|
17
|
+
server: {
|
|
18
|
+
port: 5173,
|
|
19
|
+
proxy: {
|
|
20
|
+
'/api': {
|
|
21
|
+
target: 'http://localhost:3001',
|
|
22
|
+
changeOrigin: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillverse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "A unified management platform for Claude Skills",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"client",
|
|
8
|
+
"server",
|
|
9
|
+
"shared",
|
|
10
|
+
"registry"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
|
14
|
+
"dev:server": "npm run dev --workspace=server",
|
|
15
|
+
"dev:client": "npm run dev --workspace=client",
|
|
16
|
+
"start": "npm run start:cli --workspace=server",
|
|
17
|
+
"build": "npm run build --workspace=shared && npm run build --workspace=client && npm run build --workspace=server",
|
|
18
|
+
"build:server": "npm run build --workspace=server",
|
|
19
|
+
"build:client": "npm run build --workspace=client",
|
|
20
|
+
"lint": "npm run lint --workspaces",
|
|
21
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
|
22
|
+
"typecheck": "npm run typecheck --workspaces"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.11.5",
|
|
26
|
+
"concurrently": "^8.2.2",
|
|
27
|
+
"prettier": "^3.2.4",
|
|
28
|
+
"typescript": "^5.3.3"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0",
|
|
32
|
+
"npm": ">=9.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SkillVerse Registry Server Dockerfile
|
|
2
|
+
FROM node:20-alpine AS builder
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Copy package files
|
|
7
|
+
COPY package.json package-lock.json* ./
|
|
8
|
+
COPY registry/package.json ./registry/
|
|
9
|
+
|
|
10
|
+
# Install dependencies
|
|
11
|
+
RUN npm install --workspace=registry
|
|
12
|
+
|
|
13
|
+
# Copy source
|
|
14
|
+
COPY registry ./registry
|
|
15
|
+
|
|
16
|
+
# Generate Prisma client and build
|
|
17
|
+
WORKDIR /app/registry
|
|
18
|
+
RUN npx prisma generate
|
|
19
|
+
RUN npm run build
|
|
20
|
+
|
|
21
|
+
# Production image
|
|
22
|
+
FROM node:20-alpine AS runner
|
|
23
|
+
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
|
|
26
|
+
# Copy built files
|
|
27
|
+
COPY --from=builder /app/registry/dist ./dist
|
|
28
|
+
COPY --from=builder /app/registry/package.json ./
|
|
29
|
+
COPY --from=builder /app/registry/prisma ./prisma
|
|
30
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
31
|
+
|
|
32
|
+
# Create uploads directory
|
|
33
|
+
RUN mkdir -p uploads/bundles
|
|
34
|
+
|
|
35
|
+
# Environment
|
|
36
|
+
ENV NODE_ENV=production
|
|
37
|
+
ENV PORT=4000
|
|
38
|
+
ENV STORAGE_PATH=/app/uploads
|
|
39
|
+
|
|
40
|
+
EXPOSE 4000
|
|
41
|
+
|
|
42
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
registry:
|
|
5
|
+
build:
|
|
6
|
+
context: ..
|
|
7
|
+
dockerfile: registry/Dockerfile
|
|
8
|
+
ports:
|
|
9
|
+
- "4000:4000"
|
|
10
|
+
environment:
|
|
11
|
+
- DATABASE_URL=postgresql://postgres:postgres@db:5432/skillverse_registry
|
|
12
|
+
- JWT_SECRET=${JWT_SECRET:-change-this-in-production}
|
|
13
|
+
- PORT=4000
|
|
14
|
+
- STORAGE_PATH=/app/uploads
|
|
15
|
+
volumes:
|
|
16
|
+
- registry_uploads:/app/uploads
|
|
17
|
+
depends_on:
|
|
18
|
+
- db
|
|
19
|
+
restart: unless-stopped
|
|
20
|
+
|
|
21
|
+
db:
|
|
22
|
+
image: postgres:15-alpine
|
|
23
|
+
environment:
|
|
24
|
+
- POSTGRES_USER=postgres
|
|
25
|
+
- POSTGRES_PASSWORD=postgres
|
|
26
|
+
- POSTGRES_DB=skillverse_registry
|
|
27
|
+
volumes:
|
|
28
|
+
- postgres_data:/var/lib/postgresql/data
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
|
|
31
|
+
volumes:
|
|
32
|
+
registry_uploads:
|
|
33
|
+
postgres_data:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillverse/registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SkillVerse Registry Server - Centralized skill catalog and distribution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"db:generate": "prisma generate",
|
|
12
|
+
"db:push": "prisma db push",
|
|
13
|
+
"db:migrate": "prisma migrate dev"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@prisma/client": "^5.22.0",
|
|
17
|
+
"bcryptjs": "^2.4.3",
|
|
18
|
+
"cors": "^2.8.5",
|
|
19
|
+
"dotenv": "^16.4.5",
|
|
20
|
+
"express": "^4.21.0",
|
|
21
|
+
"jsonwebtoken": "^9.0.2",
|
|
22
|
+
"multer": "^1.4.5-lts.1",
|
|
23
|
+
"uuid": "^10.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bcryptjs": "^2.4.6",
|
|
27
|
+
"@types/cors": "^2.8.17",
|
|
28
|
+
"@types/express": "^4.17.21",
|
|
29
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
30
|
+
"@types/multer": "^1.4.11",
|
|
31
|
+
"@types/node": "^20.11.24",
|
|
32
|
+
"@types/uuid": "^10.0.0",
|
|
33
|
+
"prisma": "^5.22.0",
|
|
34
|
+
"tsx": "^4.7.1",
|
|
35
|
+
"typescript": "^5.3.3"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// This is the Prisma schema for the SkillVerse Registry Server
|
|
2
|
+
// Uses PostgreSQL for production scalability
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
model User {
|
|
14
|
+
id String @id @default(uuid())
|
|
15
|
+
username String @unique
|
|
16
|
+
email String @unique
|
|
17
|
+
passwordHash String
|
|
18
|
+
displayName String?
|
|
19
|
+
avatarUrl String?
|
|
20
|
+
|
|
21
|
+
skills RegistrySkill[]
|
|
22
|
+
|
|
23
|
+
createdAt DateTime @default(now())
|
|
24
|
+
updatedAt DateTime @updatedAt
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
model RegistrySkill {
|
|
28
|
+
id String @id @default(uuid())
|
|
29
|
+
name String @unique
|
|
30
|
+
version String @default("1.0.0")
|
|
31
|
+
description String?
|
|
32
|
+
readme String? // Full README content
|
|
33
|
+
keywords String[] // Tags for search
|
|
34
|
+
|
|
35
|
+
// Publisher info
|
|
36
|
+
publisherId String
|
|
37
|
+
publisher User @relation(fields: [publisherId], references: [id])
|
|
38
|
+
|
|
39
|
+
// Storage
|
|
40
|
+
bundlePath String // Path to stored bundle
|
|
41
|
+
bundleSize Int // Size in bytes
|
|
42
|
+
checksum String? // SHA256 of bundle
|
|
43
|
+
|
|
44
|
+
// Source info (optional)
|
|
45
|
+
sourceType String? // 'git' or null
|
|
46
|
+
sourceUrl String? // Git URL if applicable
|
|
47
|
+
|
|
48
|
+
// Stats
|
|
49
|
+
downloads Int @default(0)
|
|
50
|
+
|
|
51
|
+
// Versioning
|
|
52
|
+
isLatest Boolean @default(true)
|
|
53
|
+
|
|
54
|
+
createdAt DateTime @default(now())
|
|
55
|
+
updatedAt DateTime @updatedAt
|
|
56
|
+
|
|
57
|
+
@@index([name])
|
|
58
|
+
@@index([publisherId])
|
|
59
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import { config } from 'dotenv';
|
|
4
|
+
import authRoutes from './routes/auth.js';
|
|
5
|
+
import skillsRoutes from './routes/skills.js';
|
|
6
|
+
import { errorHandler } from './middleware/errorHandler.js';
|
|
7
|
+
|
|
8
|
+
config();
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
const PORT = process.env.PORT || 4000;
|
|
12
|
+
|
|
13
|
+
// Middleware
|
|
14
|
+
app.use(cors());
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
|
|
17
|
+
// Health check
|
|
18
|
+
app.get('/health', (req, res) => {
|
|
19
|
+
res.json({ status: 'ok', service: 'skillverse-registry' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Routes
|
|
23
|
+
app.use('/api/auth', authRoutes);
|
|
24
|
+
app.use('/api/skills', skillsRoutes);
|
|
25
|
+
|
|
26
|
+
// Error handling
|
|
27
|
+
app.use(errorHandler);
|
|
28
|
+
|
|
29
|
+
// Start server
|
|
30
|
+
app.listen(PORT, () => {
|
|
31
|
+
console.log(`🚀 SkillVerse Registry running on http://localhost:${PORT}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default app;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export class AppError extends Error {
|
|
4
|
+
statusCode: number;
|
|
5
|
+
code: string;
|
|
6
|
+
|
|
7
|
+
constructor(code: string, message: string, statusCode: number = 500) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function errorHandler(
|
|
15
|
+
err: Error | AppError,
|
|
16
|
+
req: Request,
|
|
17
|
+
res: Response,
|
|
18
|
+
next: NextFunction
|
|
19
|
+
) {
|
|
20
|
+
console.error('Error:', err);
|
|
21
|
+
|
|
22
|
+
if (err instanceof AppError) {
|
|
23
|
+
return res.status(err.statusCode).json({
|
|
24
|
+
success: false,
|
|
25
|
+
error: err.message,
|
|
26
|
+
code: err.code,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
res.status(500).json({
|
|
31
|
+
success: false,
|
|
32
|
+
error: 'Internal server error',
|
|
33
|
+
code: 'INTERNAL_ERROR',
|
|
34
|
+
});
|
|
35
|
+
}
|