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,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,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
|
+
}
|