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,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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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,5 @@
1
+ // Registry Server Environment Variables
2
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/skillverse_registry"
3
+ JWT_SECRET="your-super-secret-jwt-key-change-in-production"
4
+ PORT=4000
5
+ STORAGE_PATH="./uploads"
@@ -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,3 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ export const prisma = new PrismaClient();
@@ -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
+ }