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,152 @@
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import bcrypt from 'bcryptjs';
3
+ import jwt from 'jsonwebtoken';
4
+ import { prisma } from '../lib/db.js';
5
+ import { AppError } from '../middleware/errorHandler.js';
6
+
7
+ const router = Router();
8
+ const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-me';
9
+
10
+ // POST /api/auth/register - Create new account
11
+ router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
12
+ try {
13
+ const { username, email, password, displayName } = req.body;
14
+
15
+ if (!username || !email || !password) {
16
+ throw new AppError('VALIDATION_ERROR', 'Username, email, and password are required', 400);
17
+ }
18
+
19
+ // Check if user exists
20
+ const existingUser = await prisma.user.findFirst({
21
+ where: {
22
+ OR: [{ username }, { email }],
23
+ },
24
+ });
25
+
26
+ if (existingUser) {
27
+ throw new AppError('ALREADY_EXISTS', 'Username or email already exists', 409);
28
+ }
29
+
30
+ // Hash password
31
+ const passwordHash = await bcrypt.hash(password, 12);
32
+
33
+ // Create user
34
+ const user = await prisma.user.create({
35
+ data: {
36
+ username,
37
+ email,
38
+ passwordHash,
39
+ displayName: displayName || username,
40
+ },
41
+ select: {
42
+ id: true,
43
+ username: true,
44
+ email: true,
45
+ displayName: true,
46
+ createdAt: true,
47
+ },
48
+ });
49
+
50
+ // Generate token
51
+ const token = jwt.sign({ userId: user.id, username: user.username }, JWT_SECRET, {
52
+ expiresIn: '30d',
53
+ });
54
+
55
+ res.status(201).json({
56
+ success: true,
57
+ data: { user, token },
58
+ message: 'Account created successfully',
59
+ });
60
+ } catch (error) {
61
+ next(error);
62
+ }
63
+ });
64
+
65
+ // POST /api/auth/login - Login
66
+ router.post('/login', async (req: Request, res: Response, next: NextFunction) => {
67
+ try {
68
+ const { username, password } = req.body;
69
+
70
+ if (!username || !password) {
71
+ throw new AppError('VALIDATION_ERROR', 'Username and password are required', 400);
72
+ }
73
+
74
+ // Find user
75
+ const user = await prisma.user.findFirst({
76
+ where: {
77
+ OR: [{ username }, { email: username }],
78
+ },
79
+ });
80
+
81
+ if (!user) {
82
+ throw new AppError('AUTH_FAILED', 'Invalid credentials', 401);
83
+ }
84
+
85
+ // Verify password
86
+ const isValid = await bcrypt.compare(password, user.passwordHash);
87
+ if (!isValid) {
88
+ throw new AppError('AUTH_FAILED', 'Invalid credentials', 401);
89
+ }
90
+
91
+ // Generate token
92
+ const token = jwt.sign({ userId: user.id, username: user.username }, JWT_SECRET, {
93
+ expiresIn: '30d',
94
+ });
95
+
96
+ res.json({
97
+ success: true,
98
+ data: {
99
+ user: {
100
+ id: user.id,
101
+ username: user.username,
102
+ email: user.email,
103
+ displayName: user.displayName,
104
+ },
105
+ token,
106
+ },
107
+ });
108
+ } catch (error) {
109
+ next(error);
110
+ }
111
+ });
112
+
113
+ // GET /api/auth/me - Get current user
114
+ router.get('/me', async (req: Request, res: Response, next: NextFunction) => {
115
+ try {
116
+ const authHeader = req.headers.authorization;
117
+ if (!authHeader?.startsWith('Bearer ')) {
118
+ throw new AppError('AUTH_REQUIRED', 'Authentication required', 401);
119
+ }
120
+
121
+ const token = authHeader.substring(7);
122
+ const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
123
+
124
+ const user = await prisma.user.findUnique({
125
+ where: { id: decoded.userId },
126
+ select: {
127
+ id: true,
128
+ username: true,
129
+ email: true,
130
+ displayName: true,
131
+ avatarUrl: true,
132
+ createdAt: true,
133
+ },
134
+ });
135
+
136
+ if (!user) {
137
+ throw new AppError('NOT_FOUND', 'User not found', 404);
138
+ }
139
+
140
+ res.json({
141
+ success: true,
142
+ data: user,
143
+ });
144
+ } catch (error) {
145
+ if (error instanceof jwt.JsonWebTokenError) {
146
+ return next(new AppError('AUTH_INVALID', 'Invalid token', 401));
147
+ }
148
+ next(error);
149
+ }
150
+ });
151
+
152
+ export default router;
@@ -0,0 +1,295 @@
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import multer from 'multer';
3
+ import jwt from 'jsonwebtoken';
4
+ import { existsSync } from 'fs';
5
+ import { mkdir, readFile, writeFile, rm } from 'fs/promises';
6
+ import { join } from 'path';
7
+ import { prisma } from '../lib/db.js';
8
+ import { AppError } from '../middleware/errorHandler.js';
9
+
10
+ const router = Router();
11
+ const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-me';
12
+ const STORAGE_PATH = process.env.STORAGE_PATH || './uploads';
13
+
14
+ // Configure multer for file uploads
15
+ const storage = multer.diskStorage({
16
+ destination: async (req, file, cb) => {
17
+ const uploadDir = join(STORAGE_PATH, 'bundles');
18
+ if (!existsSync(uploadDir)) {
19
+ await mkdir(uploadDir, { recursive: true });
20
+ }
21
+ cb(null, uploadDir);
22
+ },
23
+ filename: (req, file, cb) => {
24
+ const uniqueName = `${Date.now()}-${file.originalname}`;
25
+ cb(null, uniqueName);
26
+ },
27
+ });
28
+
29
+ const upload = multer({
30
+ storage,
31
+ limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
32
+ fileFilter: (req, file, cb) => {
33
+ if (file.mimetype === 'application/gzip' || file.originalname.endsWith('.tar.gz')) {
34
+ cb(null, true);
35
+ } else {
36
+ cb(new Error('Only .tar.gz files are allowed'));
37
+ }
38
+ },
39
+ });
40
+
41
+ // Auth middleware
42
+ async function requireAuth(req: Request, res: Response, next: NextFunction) {
43
+ try {
44
+ const authHeader = req.headers.authorization;
45
+ if (!authHeader?.startsWith('Bearer ')) {
46
+ throw new AppError('AUTH_REQUIRED', 'Authentication required', 401);
47
+ }
48
+
49
+ const token = authHeader.substring(7);
50
+ const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
51
+ (req as any).userId = decoded.userId;
52
+ next();
53
+ } catch (error) {
54
+ if (error instanceof jwt.JsonWebTokenError) {
55
+ return next(new AppError('AUTH_INVALID', 'Invalid token', 401));
56
+ }
57
+ next(error);
58
+ }
59
+ }
60
+
61
+ // GET /api/skills - Search/list skills
62
+ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
63
+ try {
64
+ const { search, page = '1', pageSize = '20' } = req.query;
65
+
66
+ const where: any = {};
67
+ if (search) {
68
+ where.OR = [
69
+ { name: { contains: search as string, mode: 'insensitive' } },
70
+ { description: { contains: search as string, mode: 'insensitive' } },
71
+ ];
72
+ }
73
+
74
+ const [items, total] = await Promise.all([
75
+ prisma.registrySkill.findMany({
76
+ where,
77
+ include: {
78
+ publisher: {
79
+ select: {
80
+ id: true,
81
+ username: true,
82
+ displayName: true,
83
+ },
84
+ },
85
+ },
86
+ orderBy: { downloads: 'desc' },
87
+ skip: (parseInt(page as string) - 1) * parseInt(pageSize as string),
88
+ take: parseInt(pageSize as string),
89
+ }),
90
+ prisma.registrySkill.count({ where }),
91
+ ]);
92
+
93
+ res.json({
94
+ success: true,
95
+ data: {
96
+ items,
97
+ total,
98
+ page: parseInt(page as string),
99
+ pageSize: parseInt(pageSize as string),
100
+ },
101
+ });
102
+ } catch (error) {
103
+ next(error);
104
+ }
105
+ });
106
+
107
+ // GET /api/skills/:name - Get skill details
108
+ router.get('/:name', async (req: Request, res: Response, next: NextFunction) => {
109
+ try {
110
+ const skill = await prisma.registrySkill.findUnique({
111
+ where: { name: req.params.name },
112
+ include: {
113
+ publisher: {
114
+ select: {
115
+ id: true,
116
+ username: true,
117
+ displayName: true,
118
+ },
119
+ },
120
+ },
121
+ });
122
+
123
+ if (!skill) {
124
+ throw new AppError('NOT_FOUND', 'Skill not found', 404);
125
+ }
126
+
127
+ res.json({
128
+ success: true,
129
+ data: skill,
130
+ });
131
+ } catch (error) {
132
+ next(error);
133
+ }
134
+ });
135
+
136
+ // GET /api/skills/:name/download - Download skill bundle
137
+ router.get('/:name/download', async (req: Request, res: Response, next: NextFunction) => {
138
+ try {
139
+ const skill = await prisma.registrySkill.findUnique({
140
+ where: { name: req.params.name },
141
+ });
142
+
143
+ if (!skill) {
144
+ throw new AppError('NOT_FOUND', 'Skill not found', 404);
145
+ }
146
+
147
+ if (!existsSync(skill.bundlePath)) {
148
+ throw new AppError('NOT_FOUND', 'Bundle file not found', 404);
149
+ }
150
+
151
+ // Increment download count
152
+ await prisma.registrySkill.update({
153
+ where: { id: skill.id },
154
+ data: { downloads: { increment: 1 } },
155
+ });
156
+
157
+ res.setHeader('Content-Type', 'application/gzip');
158
+ res.setHeader('Content-Disposition', `attachment; filename="${skill.name}.tar.gz"`);
159
+ res.sendFile(skill.bundlePath, { root: process.cwd() });
160
+ } catch (error) {
161
+ next(error);
162
+ }
163
+ });
164
+
165
+ // POST /api/skills/publish - Upload new skill
166
+ router.post(
167
+ '/publish',
168
+ requireAuth,
169
+ upload.single('bundle'),
170
+ async (req: Request, res: Response, next: NextFunction) => {
171
+ try {
172
+ const { name, description, version, keywords, sourceType, sourceUrl, readme } = req.body;
173
+ const userId = (req as any).userId;
174
+ const file = req.file;
175
+
176
+ if (!name) {
177
+ throw new AppError('VALIDATION_ERROR', 'Skill name is required', 400);
178
+ }
179
+
180
+ if (!file) {
181
+ throw new AppError('VALIDATION_ERROR', 'Bundle file is required', 400);
182
+ }
183
+
184
+ // Check if skill already exists
185
+ const existingSkill = await prisma.registrySkill.findUnique({
186
+ where: { name },
187
+ });
188
+
189
+ if (existingSkill) {
190
+ // Only owner can update
191
+ if (existingSkill.publisherId !== userId) {
192
+ throw new AppError('FORBIDDEN', 'You can only update your own skills', 403);
193
+ }
194
+
195
+ // Delete old bundle
196
+ if (existsSync(existingSkill.bundlePath)) {
197
+ await rm(existingSkill.bundlePath);
198
+ }
199
+
200
+ // Update existing skill
201
+ const updatedSkill = await prisma.registrySkill.update({
202
+ where: { id: existingSkill.id },
203
+ data: {
204
+ version: version || existingSkill.version,
205
+ description: description || existingSkill.description,
206
+ readme: readme || existingSkill.readme,
207
+ keywords: keywords ? JSON.parse(keywords) : existingSkill.keywords,
208
+ bundlePath: file.path,
209
+ bundleSize: file.size,
210
+ sourceType,
211
+ sourceUrl,
212
+ },
213
+ include: {
214
+ publisher: {
215
+ select: { id: true, username: true, displayName: true },
216
+ },
217
+ },
218
+ });
219
+
220
+ return res.json({
221
+ success: true,
222
+ data: updatedSkill,
223
+ message: 'Skill updated successfully',
224
+ });
225
+ }
226
+
227
+ // Create new skill
228
+ const skill = await prisma.registrySkill.create({
229
+ data: {
230
+ name,
231
+ version: version || '1.0.0',
232
+ description: description || '',
233
+ readme: readme || '',
234
+ keywords: keywords ? JSON.parse(keywords) : [],
235
+ publisherId: userId,
236
+ bundlePath: file.path,
237
+ bundleSize: file.size,
238
+ sourceType,
239
+ sourceUrl,
240
+ },
241
+ include: {
242
+ publisher: {
243
+ select: { id: true, username: true, displayName: true },
244
+ },
245
+ },
246
+ });
247
+
248
+ res.status(201).json({
249
+ success: true,
250
+ data: skill,
251
+ message: 'Skill published successfully',
252
+ });
253
+ } catch (error) {
254
+ next(error);
255
+ }
256
+ }
257
+ );
258
+
259
+ // DELETE /api/skills/:name - Unpublish skill
260
+ router.delete('/:name', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
261
+ try {
262
+ const userId = (req as any).userId;
263
+
264
+ const skill = await prisma.registrySkill.findUnique({
265
+ where: { name: req.params.name },
266
+ });
267
+
268
+ if (!skill) {
269
+ throw new AppError('NOT_FOUND', 'Skill not found', 404);
270
+ }
271
+
272
+ if (skill.publisherId !== userId) {
273
+ throw new AppError('FORBIDDEN', 'You can only delete your own skills', 403);
274
+ }
275
+
276
+ // Delete bundle file
277
+ if (existsSync(skill.bundlePath)) {
278
+ await rm(skill.bundlePath);
279
+ }
280
+
281
+ // Delete from database
282
+ await prisma.registrySkill.delete({
283
+ where: { id: skill.id },
284
+ });
285
+
286
+ res.json({
287
+ success: true,
288
+ message: 'Skill deleted successfully',
289
+ });
290
+ } catch (error) {
291
+ next(error);
292
+ }
293
+ });
294
+
295
+ export default router;
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "target": "ES2022",
9
+ "lib": [
10
+ "ES2022"
11
+ ],
12
+ "types": [
13
+ "node"
14
+ ]
15
+ },
16
+ "include": [
17
+ "src/**/*"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+ }
@@ -0,0 +1,11 @@
1
+ # Database
2
+ DATABASE_URL="file:./dev.db"
3
+
4
+ # Server
5
+ PORT=3001
6
+ NODE_ENV=development
7
+
8
+ # SkillVerse Storage
9
+ SKILLVERSE_HOME="${HOME}/.skillverse"
10
+ SKILLS_DIR="${SKILLVERSE_HOME}/skills"
11
+ MARKETPLACE_DIR="${SKILLVERSE_HOME}/marketplace"
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "skillverse-cli",
3
+ "version": "0.1.0",
4
+ "description": "SkillVerse - A local-first skill management platform for AI coding assistants",
5
+ "author": "SkillVerse Team",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "tsx watch src/index.ts",
10
+ "build": "tsup",
11
+ "prepublishOnly": "npm run build:client && npm run build",
12
+ "build:client": "cd .. && npm run build --workspace=client",
13
+ "start": "node dist/index.js",
14
+ "postinstall": "prisma generate",
15
+ "typecheck": "tsc --noEmit",
16
+ "db:generate": "prisma generate",
17
+ "db:push": "prisma db push",
18
+ "db:migrate": "prisma migrate dev",
19
+ "db:studio": "prisma studio",
20
+ "start:cli": "tsx src/bin.ts"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "public",
25
+ "prisma",
26
+ "README.md"
27
+ ],
28
+ "bin": {
29
+ "skillverse": "./dist/bin.js"
30
+ },
31
+ "dependencies": {
32
+ "@prisma/client": "^5.8.1",
33
+ "@skillverse/shared": "*",
34
+ "@types/archiver": "^7.0.0",
35
+ "@types/tar": "^6.1.13",
36
+ "adm-zip": "^0.5.10",
37
+ "archiver": "^7.0.1",
38
+ "commander": "^14.0.3",
39
+ "cors": "^2.8.5",
40
+ "dotenv": "^16.3.1",
41
+ "express": "^4.18.2",
42
+ "form-data": "^4.0.5",
43
+ "gray-matter": "^4.0.3",
44
+ "multer": "^1.4.5-lts.1",
45
+ "open": "^11.0.0",
46
+ "simple-git": "^3.22.0",
47
+ "tar": "^7.5.7",
48
+ "zod": "^3.22.4"
49
+ },
50
+ "devDependencies": {
51
+ "@types/adm-zip": "^0.5.5",
52
+ "@types/cors": "^2.8.17",
53
+ "@types/express": "^4.17.21",
54
+ "@types/multer": "^1.4.11",
55
+ "prisma": "^5.8.1",
56
+ "tsup": "^8.5.1",
57
+ "tsx": "^4.7.0",
58
+ "typescript": "^5.3.3"
59
+ }
60
+ }
@@ -0,0 +1,73 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "sqlite"
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ model Skill {
14
+ id String @id @default(uuid())
15
+ name String @unique
16
+ source String // 'git' or 'local'
17
+ sourceUrl String?
18
+ description String?
19
+ commitHash String? // Git commit hash
20
+ repoUrl String? // Root repository URL (if different from sourceUrl)
21
+ updateAvailable Boolean @default(false)
22
+ lastUpdateCheck DateTime?
23
+ installDate DateTime @default(now())
24
+ metadata String? // JSON string
25
+ storagePath String // Path to skill files in central storage
26
+
27
+ linkedWorkspaces SkillWorkspace[]
28
+ marketplaceEntry MarketplaceSkill?
29
+
30
+ createdAt DateTime @default(now())
31
+ updatedAt DateTime @updatedAt
32
+ }
33
+
34
+ model Workspace {
35
+ id String @id @default(uuid())
36
+ name String
37
+ path String @unique
38
+ type String // 'vscode', 'cursor', 'claude-desktop', 'codex', 'antigravity', 'custom'
39
+ scope String @default("project") // 'project' or 'global'
40
+
41
+ linkedSkills SkillWorkspace[]
42
+
43
+ createdAt DateTime @default(now())
44
+ updatedAt DateTime @updatedAt
45
+ }
46
+
47
+ model SkillWorkspace {
48
+ id String @id @default(uuid())
49
+ skillId String
50
+ workspaceId String
51
+ linkedAt DateTime @default(now())
52
+
53
+ skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade)
54
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
55
+
56
+ @@unique([skillId, workspaceId])
57
+ }
58
+
59
+ model MarketplaceSkill {
60
+ id String @id @default(uuid())
61
+ skillId String @unique
62
+ publisherId String?
63
+ publisherName String?
64
+ publishDate DateTime @default(now())
65
+ downloads Int @default(0)
66
+ bundlePath String? // Path to the skill bundle file
67
+ bundleSize Int? // Size of the bundle in bytes
68
+
69
+ skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade)
70
+
71
+ createdAt DateTime @default(now())
72
+ updatedAt DateTime @updatedAt
73
+ }