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,102 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { prisma } from '../lib/db.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
// GET /api/dashboard/stats - Get dashboard statistics
|
|
7
|
+
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
try {
|
|
9
|
+
const [totalSkills, totalWorkspaces, totalLinks, marketplaceSkills, recentSkills] =
|
|
10
|
+
await Promise.all([
|
|
11
|
+
prisma.skill.count(),
|
|
12
|
+
prisma.workspace.count(),
|
|
13
|
+
prisma.skillWorkspace.count(),
|
|
14
|
+
prisma.marketplaceSkill.count(),
|
|
15
|
+
prisma.skill.findMany({
|
|
16
|
+
orderBy: { installDate: 'desc' },
|
|
17
|
+
take: 5,
|
|
18
|
+
include: {
|
|
19
|
+
linkedWorkspaces: {
|
|
20
|
+
include: {
|
|
21
|
+
workspace: true,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
res.json({
|
|
29
|
+
success: true,
|
|
30
|
+
data: {
|
|
31
|
+
totalSkills,
|
|
32
|
+
totalWorkspaces,
|
|
33
|
+
totalLinks,
|
|
34
|
+
marketplaceSkills,
|
|
35
|
+
recentSkills,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
next(error);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// GET /api/dashboard/activity - Get recent activity
|
|
44
|
+
router.get('/activity', async (req: Request, res: Response, next: NextFunction) => {
|
|
45
|
+
try {
|
|
46
|
+
const [recentSkills, recentLinks, recentPublished] = await Promise.all([
|
|
47
|
+
prisma.skill.findMany({
|
|
48
|
+
orderBy: { createdAt: 'desc' },
|
|
49
|
+
take: 10,
|
|
50
|
+
select: {
|
|
51
|
+
id: true,
|
|
52
|
+
name: true,
|
|
53
|
+
source: true,
|
|
54
|
+
createdAt: true,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
prisma.skillWorkspace.findMany({
|
|
58
|
+
orderBy: { linkedAt: 'desc' },
|
|
59
|
+
take: 10,
|
|
60
|
+
include: {
|
|
61
|
+
skill: { select: { name: true } },
|
|
62
|
+
workspace: { select: { name: true } },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
prisma.marketplaceSkill.findMany({
|
|
66
|
+
orderBy: { publishDate: 'desc' },
|
|
67
|
+
take: 10,
|
|
68
|
+
include: {
|
|
69
|
+
skill: { select: { name: true } },
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// Combine and sort by date
|
|
75
|
+
const activities = [
|
|
76
|
+
...recentSkills.map((s) => ({
|
|
77
|
+
type: 'skill_added' as const,
|
|
78
|
+
date: s.createdAt,
|
|
79
|
+
data: { skillName: s.name, source: s.source },
|
|
80
|
+
})),
|
|
81
|
+
...recentLinks.map((l) => ({
|
|
82
|
+
type: 'skill_linked' as const,
|
|
83
|
+
date: l.linkedAt,
|
|
84
|
+
data: { skillName: l.skill.name, workspaceName: l.workspace.name },
|
|
85
|
+
})),
|
|
86
|
+
...recentPublished.map((p) => ({
|
|
87
|
+
type: 'skill_published' as const,
|
|
88
|
+
date: p.publishDate,
|
|
89
|
+
data: { skillName: p.skill.name },
|
|
90
|
+
})),
|
|
91
|
+
].sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
92
|
+
|
|
93
|
+
res.json({
|
|
94
|
+
success: true,
|
|
95
|
+
data: activities.slice(0, 20),
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
next(error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export default router;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { prisma } from '../lib/db.js';
|
|
3
|
+
import { skillService } from '../services/skillService.js';
|
|
4
|
+
import { bundleService } from '../services/bundleService.js';
|
|
5
|
+
import { AppError } from '../middleware/errorHandler.js';
|
|
6
|
+
import { ErrorCode } from '@skillverse/shared';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
// GET /api/marketplace/skills - Get all marketplace skills
|
|
12
|
+
router.get('/skills', async (req: Request, res: Response, next: NextFunction) => {
|
|
13
|
+
try {
|
|
14
|
+
const { search, page = '1', pageSize = '20' } = req.query;
|
|
15
|
+
|
|
16
|
+
const where: any = {};
|
|
17
|
+
if (search) {
|
|
18
|
+
where.skill = {
|
|
19
|
+
OR: [
|
|
20
|
+
{ name: { contains: search as string } },
|
|
21
|
+
{ description: { contains: search as string } },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [items, total] = await Promise.all([
|
|
27
|
+
prisma.marketplaceSkill.findMany({
|
|
28
|
+
where,
|
|
29
|
+
include: {
|
|
30
|
+
skill: true,
|
|
31
|
+
},
|
|
32
|
+
orderBy: {
|
|
33
|
+
downloads: 'desc',
|
|
34
|
+
},
|
|
35
|
+
skip: (parseInt(page as string) - 1) * parseInt(pageSize as string),
|
|
36
|
+
take: parseInt(pageSize as string),
|
|
37
|
+
}),
|
|
38
|
+
prisma.marketplaceSkill.count({ where }),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
res.json({
|
|
42
|
+
success: true,
|
|
43
|
+
data: {
|
|
44
|
+
items,
|
|
45
|
+
total,
|
|
46
|
+
page: parseInt(page as string),
|
|
47
|
+
pageSize: parseInt(pageSize as string),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
next(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// GET /api/marketplace/skills/:id - Get marketplace skill by ID
|
|
56
|
+
router.get('/skills/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
57
|
+
try {
|
|
58
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
59
|
+
where: { id: req.params.id },
|
|
60
|
+
include: {
|
|
61
|
+
skill: true,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!marketplaceSkill) {
|
|
66
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Marketplace skill not found', 404);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.json({
|
|
70
|
+
success: true,
|
|
71
|
+
data: marketplaceSkill,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
next(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// GET /api/marketplace/download/:id - Download skill bundle
|
|
79
|
+
router.get('/download/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
80
|
+
try {
|
|
81
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
82
|
+
where: { id: req.params.id },
|
|
83
|
+
include: {
|
|
84
|
+
skill: true,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!marketplaceSkill) {
|
|
89
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Marketplace skill not found', 404);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// For Git skills, redirect to the source URL or return info
|
|
93
|
+
if (marketplaceSkill.skill.source === 'git' && marketplaceSkill.skill.sourceUrl) {
|
|
94
|
+
return res.json({
|
|
95
|
+
success: true,
|
|
96
|
+
data: {
|
|
97
|
+
type: 'git',
|
|
98
|
+
sourceUrl: marketplaceSkill.skill.sourceUrl,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For bundled skills, serve the bundle file
|
|
104
|
+
if (marketplaceSkill.bundlePath && existsSync(marketplaceSkill.bundlePath)) {
|
|
105
|
+
res.setHeader('Content-Type', 'application/gzip');
|
|
106
|
+
res.setHeader(
|
|
107
|
+
'Content-Disposition',
|
|
108
|
+
`attachment; filename="${marketplaceSkill.skill.name}.tar.gz"`
|
|
109
|
+
);
|
|
110
|
+
return res.sendFile(marketplaceSkill.bundlePath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Bundle not available for this skill', 404);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// POST /api/marketplace/publish - Publish skill to marketplace
|
|
120
|
+
router.post('/publish', async (req: Request, res: Response, next: NextFunction) => {
|
|
121
|
+
try {
|
|
122
|
+
const { skillId, publisherName } = req.body;
|
|
123
|
+
|
|
124
|
+
if (!skillId) {
|
|
125
|
+
return res.status(400).json({
|
|
126
|
+
success: false,
|
|
127
|
+
error: 'skillId is required',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if skill exists
|
|
132
|
+
const skill = await prisma.skill.findUnique({
|
|
133
|
+
where: { id: skillId },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!skill) {
|
|
137
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Skill not found', 404);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if already published
|
|
141
|
+
const existingEntry = await prisma.marketplaceSkill.findUnique({
|
|
142
|
+
where: { skillId },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (existingEntry) {
|
|
146
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, 'Skill is already published to marketplace', 409);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create bundle for local skills
|
|
150
|
+
let bundlePath: string | null = null;
|
|
151
|
+
let bundleSize: number | null = null;
|
|
152
|
+
|
|
153
|
+
if (skill.source === 'local' && existsSync(skill.storagePath)) {
|
|
154
|
+
bundlePath = await bundleService.createBundle(skill.storagePath, skill.name);
|
|
155
|
+
bundleSize = await bundleService.getBundleSize(bundlePath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create marketplace entry
|
|
159
|
+
const marketplaceSkill = await prisma.marketplaceSkill.create({
|
|
160
|
+
data: {
|
|
161
|
+
skillId,
|
|
162
|
+
publisherName: publisherName || 'Anonymous',
|
|
163
|
+
bundlePath,
|
|
164
|
+
bundleSize,
|
|
165
|
+
},
|
|
166
|
+
include: {
|
|
167
|
+
skill: true,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
res.status(201).json({
|
|
172
|
+
success: true,
|
|
173
|
+
data: marketplaceSkill,
|
|
174
|
+
message: 'Skill published to marketplace successfully',
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
next(error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
// POST /api/marketplace/install/:id - Install skill from marketplace
|
|
183
|
+
router.post('/install/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
184
|
+
try {
|
|
185
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
186
|
+
where: { id: req.params.id },
|
|
187
|
+
include: {
|
|
188
|
+
skill: true,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!marketplaceSkill) {
|
|
193
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Marketplace skill not found', 404);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sourceSkill = marketplaceSkill.skill;
|
|
197
|
+
|
|
198
|
+
// For Git source, clone the repository
|
|
199
|
+
if (sourceSkill.source === 'git' && sourceSkill.sourceUrl) {
|
|
200
|
+
const newSkill = await skillService.createSkillFromGit(
|
|
201
|
+
sourceSkill.sourceUrl,
|
|
202
|
+
sourceSkill.description || undefined
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Increment download count
|
|
206
|
+
await prisma.marketplaceSkill.update({
|
|
207
|
+
where: { id: req.params.id },
|
|
208
|
+
data: { downloads: { increment: 1 } },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return res.status(201).json({
|
|
212
|
+
success: true,
|
|
213
|
+
data: newSkill,
|
|
214
|
+
message: 'Skill installed from marketplace successfully',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// For local source with bundle, extract the bundle
|
|
219
|
+
if (sourceSkill.source === 'local' && marketplaceSkill.bundlePath && existsSync(marketplaceSkill.bundlePath)) {
|
|
220
|
+
const newSkill = await skillService.createSkillFromBundle(
|
|
221
|
+
marketplaceSkill.bundlePath,
|
|
222
|
+
sourceSkill.name,
|
|
223
|
+
sourceSkill.description || undefined
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Increment download count
|
|
227
|
+
await prisma.marketplaceSkill.update({
|
|
228
|
+
where: { id: req.params.id },
|
|
229
|
+
data: { downloads: { increment: 1 } },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return res.status(201).json({
|
|
233
|
+
success: true,
|
|
234
|
+
data: newSkill,
|
|
235
|
+
message: 'Skill installed from marketplace successfully',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// No bundle available
|
|
240
|
+
return res.status(400).json({
|
|
241
|
+
success: false,
|
|
242
|
+
error: 'Cannot install this skill. Bundle not available.',
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
next(error);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// DELETE /api/marketplace/unpublish/:id - Unpublish skill from marketplace
|
|
250
|
+
router.delete('/unpublish/:skillId', async (req: Request, res: Response, next: NextFunction) => {
|
|
251
|
+
try {
|
|
252
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
253
|
+
where: { skillId: req.params.skillId },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!marketplaceSkill) {
|
|
257
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Skill is not published to marketplace', 404);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await prisma.marketplaceSkill.delete({
|
|
261
|
+
where: { id: marketplaceSkill.id },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
res.json({
|
|
265
|
+
success: true,
|
|
266
|
+
message: 'Skill unpublished from marketplace successfully',
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
next(error);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export default router;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { mkdir, rm } from 'fs/promises';
|
|
6
|
+
import { skillService } from '../services/skillService.js';
|
|
7
|
+
import { workspaceService } from '../services/workspaceService.js';
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
// Configure multer for file uploads
|
|
12
|
+
const TEMP_DIR = process.env.TEMP_DIR || join(process.env.HOME || '', '.skillverse', 'temp');
|
|
13
|
+
|
|
14
|
+
// Ensure temp directory exists
|
|
15
|
+
if (!existsSync(TEMP_DIR)) {
|
|
16
|
+
mkdir(TEMP_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const storage = multer.diskStorage({
|
|
20
|
+
destination: (req, file, cb) => {
|
|
21
|
+
cb(null, TEMP_DIR);
|
|
22
|
+
},
|
|
23
|
+
filename: (req, file, cb) => {
|
|
24
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
|
25
|
+
cb(null, uniqueSuffix + '-' + file.originalname);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const upload = multer({
|
|
30
|
+
storage,
|
|
31
|
+
limits: {
|
|
32
|
+
fileSize: 100 * 1024 * 1024, // 100MB limit
|
|
33
|
+
},
|
|
34
|
+
fileFilter: (req, file, cb) => {
|
|
35
|
+
if (file.mimetype === 'application/zip' || file.originalname.endsWith('.zip')) {
|
|
36
|
+
cb(null, true);
|
|
37
|
+
} else {
|
|
38
|
+
cb(new Error('Only ZIP files are allowed'));
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// GET /api/skills - Get all skills
|
|
44
|
+
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
45
|
+
try {
|
|
46
|
+
const skills = await skillService.getAllSkills();
|
|
47
|
+
res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
data: skills,
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
next(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// GET /api/skills/:id - Get skill by ID
|
|
57
|
+
router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
58
|
+
try {
|
|
59
|
+
const skill = await skillService.getSkillById(req.params.id);
|
|
60
|
+
res.json({
|
|
61
|
+
success: true,
|
|
62
|
+
data: skill,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
next(error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// POST /api/skills/from-git - Create skill from Git repository
|
|
70
|
+
router.post('/from-git', async (req: Request, res: Response, next: NextFunction) => {
|
|
71
|
+
try {
|
|
72
|
+
const { gitUrl, description } = req.body;
|
|
73
|
+
|
|
74
|
+
if (!gitUrl) {
|
|
75
|
+
return res.status(400).json({
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'gitUrl is required',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const skill = await skillService.createSkillFromGit(gitUrl, description);
|
|
82
|
+
res.status(201).json({
|
|
83
|
+
success: true,
|
|
84
|
+
data: skill,
|
|
85
|
+
message: 'Skill created successfully from Git repository',
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
next(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// POST /api/skills/from-local - Create skill from local ZIP file
|
|
93
|
+
router.post(
|
|
94
|
+
'/from-local',
|
|
95
|
+
upload.single('file'),
|
|
96
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
97
|
+
try {
|
|
98
|
+
const { name, description } = req.body;
|
|
99
|
+
const file = req.file;
|
|
100
|
+
|
|
101
|
+
if (!name) {
|
|
102
|
+
// Clean up uploaded file
|
|
103
|
+
if (file) {
|
|
104
|
+
await rm(file.path, { force: true });
|
|
105
|
+
}
|
|
106
|
+
return res.status(400).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: 'name is required',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!file) {
|
|
113
|
+
return res.status(400).json({
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'ZIP file is required',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const skill = await skillService.createSkillFromLocal(name, file.path, description);
|
|
120
|
+
|
|
121
|
+
// Clean up temp file
|
|
122
|
+
await rm(file.path, { force: true });
|
|
123
|
+
|
|
124
|
+
res.status(201).json({
|
|
125
|
+
success: true,
|
|
126
|
+
data: skill,
|
|
127
|
+
message: 'Skill created successfully from local file',
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Clean up uploaded file on error
|
|
131
|
+
if (req.file) {
|
|
132
|
+
await rm(req.file.path, { force: true }).catch(() => { });
|
|
133
|
+
}
|
|
134
|
+
next(error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// PUT /api/skills/:id - Update skill
|
|
140
|
+
router.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
141
|
+
try {
|
|
142
|
+
const { name, description, metadata } = req.body;
|
|
143
|
+
const skill = await skillService.updateSkill(req.params.id, {
|
|
144
|
+
name,
|
|
145
|
+
description,
|
|
146
|
+
metadata,
|
|
147
|
+
});
|
|
148
|
+
res.json({
|
|
149
|
+
success: true,
|
|
150
|
+
data: skill,
|
|
151
|
+
message: 'Skill updated successfully',
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
next(error);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// DELETE /api/skills/:id - Delete skill
|
|
159
|
+
router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
|
160
|
+
try {
|
|
161
|
+
const removeFiles = req.query.removeFiles !== 'false';
|
|
162
|
+
const result = await skillService.deleteSkill(req.params.id, removeFiles);
|
|
163
|
+
res.json(result);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
next(error);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// POST /api/skills/:id/link - Link skill to workspace
|
|
170
|
+
router.post('/:id/link', async (req: Request, res: Response, next: NextFunction) => {
|
|
171
|
+
try {
|
|
172
|
+
const { workspaceId } = req.body;
|
|
173
|
+
|
|
174
|
+
if (!workspaceId) {
|
|
175
|
+
return res.status(400).json({
|
|
176
|
+
success: false,
|
|
177
|
+
error: 'workspaceId is required',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const link = await workspaceService.linkSkillToWorkspace(req.params.id, workspaceId);
|
|
182
|
+
res.status(201).json({
|
|
183
|
+
success: true,
|
|
184
|
+
data: link,
|
|
185
|
+
message: 'Skill linked to workspace successfully',
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
next(error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// DELETE /api/skills/:id/unlink/:workspaceId - Unlink skill from workspace
|
|
193
|
+
router.delete(
|
|
194
|
+
'/:id/unlink/:workspaceId',
|
|
195
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
196
|
+
try {
|
|
197
|
+
const result = await workspaceService.unlinkSkillFromWorkspace(
|
|
198
|
+
req.params.id,
|
|
199
|
+
req.params.workspaceId
|
|
200
|
+
);
|
|
201
|
+
res.json(result);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
next(error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// GET /api/skills/:id/check-update - Check for updates
|
|
209
|
+
router.get('/:id/check-update', async (req: Request, res: Response, next: NextFunction) => {
|
|
210
|
+
try {
|
|
211
|
+
const result = await skillService.checkForUpdate(req.params.id);
|
|
212
|
+
res.json({
|
|
213
|
+
success: true,
|
|
214
|
+
data: result,
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
next(error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// POST /api/skills/:id/upgrade - Upgrade skill
|
|
222
|
+
router.post('/:id/upgrade', async (req: Request, res: Response, next: NextFunction) => {
|
|
223
|
+
try {
|
|
224
|
+
const skill = await skillService.upgradeSkill(req.params.id);
|
|
225
|
+
res.json({
|
|
226
|
+
success: true,
|
|
227
|
+
data: skill,
|
|
228
|
+
message: 'Skill upgraded successfully',
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
next(error);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// POST /api/skills/check-updates - Check for updates for multiple skills
|
|
236
|
+
router.post('/check-updates', async (req: Request, res: Response, next: NextFunction) => {
|
|
237
|
+
try {
|
|
238
|
+
const { ids } = req.body; // Optional: ids array to check specific skills
|
|
239
|
+
const results = await skillService.checkUpdates(ids);
|
|
240
|
+
res.json({
|
|
241
|
+
success: true,
|
|
242
|
+
data: results,
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
next(error);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// POST /api/skills/:id/refresh-metadata - Refresh skill metadata
|
|
250
|
+
router.post('/:id/refresh-metadata', async (req: Request, res: Response, next: NextFunction) => {
|
|
251
|
+
try {
|
|
252
|
+
const skill = await skillService.refreshMetadata(req.params.id);
|
|
253
|
+
res.json({
|
|
254
|
+
success: true,
|
|
255
|
+
data: skill,
|
|
256
|
+
message: 'Metadata refreshed successfully',
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
next(error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// GET /api/skills/:id/skill-md - Get SKILL.md content for preview
|
|
264
|
+
router.get('/:id/skill-md', async (req: Request, res: Response, next: NextFunction) => {
|
|
265
|
+
try {
|
|
266
|
+
const { readFile } = await import('fs/promises');
|
|
267
|
+
const skill = await skillService.getSkillById(req.params.id);
|
|
268
|
+
const skillMdPath = join(skill.storagePath, 'SKILL.md');
|
|
269
|
+
|
|
270
|
+
if (!existsSync(skillMdPath)) {
|
|
271
|
+
return res.json({
|
|
272
|
+
success: true,
|
|
273
|
+
data: {
|
|
274
|
+
exists: false,
|
|
275
|
+
content: null,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const content = await readFile(skillMdPath, 'utf-8');
|
|
281
|
+
res.json({
|
|
282
|
+
success: true,
|
|
283
|
+
data: {
|
|
284
|
+
exists: true,
|
|
285
|
+
content,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
next(error);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export default router;
|
|
294
|
+
|