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