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,521 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { symlink, unlink, rm, mkdir, appendFile, readFile } from 'fs/promises';
|
|
4
|
+
import { existsSync, lstatSync } from 'fs';
|
|
5
|
+
import { prisma } from '../lib/db.js';
|
|
6
|
+
import { AppError } from '../middleware/errorHandler.js';
|
|
7
|
+
import { ErrorCode, WorkspaceType, WorkspaceScope, WORKSPACE_SKILLS_PATHS } from '@skillverse/shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute the final skills path based on project path, type, and scope
|
|
11
|
+
*/
|
|
12
|
+
function computeSkillsPath(projectPath: string, type: WorkspaceType, scope: WorkspaceScope): string {
|
|
13
|
+
const pathConfig = WORKSPACE_SKILLS_PATHS[type];
|
|
14
|
+
|
|
15
|
+
if (scope === 'global') {
|
|
16
|
+
// Global path: replace ~ with actual home directory
|
|
17
|
+
return pathConfig.global.replace('~', homedir());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Project path = projectPath + sub-directory
|
|
21
|
+
return join(projectPath, pathConfig.project);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class WorkspaceService {
|
|
25
|
+
/**
|
|
26
|
+
* Helper to get expected skills path for a project
|
|
27
|
+
*/
|
|
28
|
+
getSkillsPath(projectPath: string, type: WorkspaceType, scope: WorkspaceScope): string {
|
|
29
|
+
return computeSkillsPath(projectPath, type, scope);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sync database links with filesystem state
|
|
34
|
+
* Removes links from DB if the symlink is missing from the workspace
|
|
35
|
+
*/
|
|
36
|
+
private async syncLinks(workspace: any) {
|
|
37
|
+
if (!workspace.linkedSkills || workspace.linkedSkills.length === 0) return;
|
|
38
|
+
|
|
39
|
+
for (const link of workspace.linkedSkills) {
|
|
40
|
+
if (!link.skill) continue;
|
|
41
|
+
|
|
42
|
+
const linkPath = join(workspace.path, link.skill.name);
|
|
43
|
+
|
|
44
|
+
// Check if symlink exists
|
|
45
|
+
if (!existsSync(linkPath)) {
|
|
46
|
+
console.log(`Link missing for skill "${link.skill.name}" in workspace "${workspace.name}". Removing from DB.`);
|
|
47
|
+
try {
|
|
48
|
+
await prisma.skillWorkspace.delete({
|
|
49
|
+
where: { id: link.id },
|
|
50
|
+
});
|
|
51
|
+
// Update the in-memory object so the returned response is correct
|
|
52
|
+
workspace.linkedSkills = workspace.linkedSkills.filter((l: any) => l.id !== link.id);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`Failed to sync link for ${link.skill.name}:`, err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async findWorkspaceByPath(path: string) {
|
|
61
|
+
const workspace = await prisma.workspace.findUnique({
|
|
62
|
+
where: { path },
|
|
63
|
+
include: {
|
|
64
|
+
linkedSkills: {
|
|
65
|
+
include: {
|
|
66
|
+
skill: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return workspace;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getAllWorkspaces() {
|
|
75
|
+
const workspaces = await prisma.workspace.findMany({
|
|
76
|
+
include: {
|
|
77
|
+
linkedSkills: {
|
|
78
|
+
include: {
|
|
79
|
+
skill: true,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
orderBy: {
|
|
84
|
+
createdAt: 'desc',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Sync links for all workspaces (in parallel)
|
|
89
|
+
await Promise.all(workspaces.map(w => this.syncLinks(w)));
|
|
90
|
+
|
|
91
|
+
// Add isPathValid status for each workspace
|
|
92
|
+
return workspaces.map(workspace => ({
|
|
93
|
+
...workspace,
|
|
94
|
+
isPathValid: existsSync(workspace.path),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getWorkspaceById(id: string) {
|
|
99
|
+
const workspace = await prisma.workspace.findUnique({
|
|
100
|
+
where: { id },
|
|
101
|
+
include: {
|
|
102
|
+
linkedSkills: {
|
|
103
|
+
include: {
|
|
104
|
+
skill: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!workspace) {
|
|
111
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Workspace not found', 404);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sync links with filesystem
|
|
115
|
+
await this.syncLinks(workspace);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...workspace,
|
|
119
|
+
isPathValid: existsSync(workspace.path),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async createWorkspace(name: string, projectPath: string, type: WorkspaceType, scope: WorkspaceScope) {
|
|
124
|
+
// Compute the final skills path
|
|
125
|
+
const skillsPath = computeSkillsPath(projectPath, type, scope);
|
|
126
|
+
|
|
127
|
+
// For project scope, check if project path exists
|
|
128
|
+
if (scope === 'project' && projectPath && !existsSync(projectPath)) {
|
|
129
|
+
throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, `Project path does not exist: ${projectPath}`, 400);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Auto-create skills directory if it doesn't exist
|
|
133
|
+
if (!existsSync(skillsPath)) {
|
|
134
|
+
try {
|
|
135
|
+
await mkdir(skillsPath, { recursive: true });
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
throw new AppError(
|
|
138
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
139
|
+
`Failed to create skills directory: ${err.message}`,
|
|
140
|
+
500
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if workspace already exists at this path
|
|
146
|
+
const existingWorkspace = await prisma.workspace.findUnique({
|
|
147
|
+
where: { path: skillsPath },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (existingWorkspace) {
|
|
151
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, 'Workspace already exists at this path', 409);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const workspace = await prisma.workspace.create({
|
|
155
|
+
data: {
|
|
156
|
+
name,
|
|
157
|
+
path: skillsPath,
|
|
158
|
+
type,
|
|
159
|
+
scope,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// For project scope, try to add skills directory to .gitignore
|
|
164
|
+
if (scope === 'project' && projectPath && existsSync(projectPath)) {
|
|
165
|
+
try {
|
|
166
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
167
|
+
const relativeSkillsPath = WORKSPACE_SKILLS_PATHS[type].project;
|
|
168
|
+
|
|
169
|
+
let shouldAppend = false;
|
|
170
|
+
|
|
171
|
+
if (existsSync(gitignorePath)) {
|
|
172
|
+
const content = await readFile(gitignorePath, 'utf-8');
|
|
173
|
+
// Check if path is already ignored (simple check)
|
|
174
|
+
if (!content.includes(relativeSkillsPath)) {
|
|
175
|
+
shouldAppend = true;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// Create .gitignore if it doesn't exist
|
|
179
|
+
shouldAppend = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (shouldAppend) {
|
|
183
|
+
const ignoreEntry = `\n\n# SkillVerse\n${relativeSkillsPath}/\n`;
|
|
184
|
+
await appendFile(gitignorePath, ignoreEntry);
|
|
185
|
+
console.log(`Added ${relativeSkillsPath}/ to .gitignore in ${projectPath}`);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Non-fatal error, just log it
|
|
189
|
+
console.warn('Failed to update .gitignore:', err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...workspace,
|
|
195
|
+
isPathValid: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async updateWorkspace(id: string, data: { name?: string; path?: string; type?: WorkspaceType; scope?: WorkspaceScope }) {
|
|
200
|
+
await this.getWorkspaceById(id);
|
|
201
|
+
|
|
202
|
+
const workspace = await prisma.workspace.update({
|
|
203
|
+
where: { id },
|
|
204
|
+
data,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
...workspace,
|
|
209
|
+
isPathValid: existsSync(workspace.path),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async deleteWorkspace(id: string) {
|
|
214
|
+
const workspace = await this.getWorkspaceById(id);
|
|
215
|
+
|
|
216
|
+
// Remove all symlinks for this workspace
|
|
217
|
+
const links = await prisma.skillWorkspace.findMany({
|
|
218
|
+
where: { workspaceId: id },
|
|
219
|
+
include: { skill: true },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for (const link of links) {
|
|
223
|
+
try {
|
|
224
|
+
const linkPath = join(workspace.path, link.skill.name);
|
|
225
|
+
if (existsSync(linkPath)) {
|
|
226
|
+
await rm(linkPath, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(`Failed to remove symlink for ${link.skill.name}:`, error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Delete workspace
|
|
234
|
+
await prisma.workspace.delete({ where: { id } });
|
|
235
|
+
|
|
236
|
+
return { success: true, message: 'Workspace deleted successfully' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async linkSkillToWorkspace(skillId: string, workspaceId: string) {
|
|
240
|
+
const skill = await prisma.skill.findUnique({ where: { id: skillId } });
|
|
241
|
+
const workspace = await prisma.workspace.findUnique({ where: { id: workspaceId } });
|
|
242
|
+
|
|
243
|
+
if (!skill) {
|
|
244
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Skill not found', 404);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!workspace) {
|
|
248
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Workspace not found', 404);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate workspace path still exists
|
|
252
|
+
if (!existsSync(workspace.path)) {
|
|
253
|
+
throw new AppError(
|
|
254
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
255
|
+
`Workspace skills directory does not exist: ${workspace.path}. The folder may have been deleted. Please recreate it or remove this workspace.`,
|
|
256
|
+
400
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check if link already exists
|
|
261
|
+
const existingLink = await prisma.skillWorkspace.findFirst({
|
|
262
|
+
where: {
|
|
263
|
+
skillId,
|
|
264
|
+
workspaceId,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (existingLink) {
|
|
269
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, 'Skill is already linked to this workspace', 409);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Create symlink
|
|
273
|
+
const targetPath = join(workspace.path, skill.name);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Remove existing file/link if it exists
|
|
277
|
+
if (existsSync(targetPath)) {
|
|
278
|
+
const stats = lstatSync(targetPath);
|
|
279
|
+
if (stats.isSymbolicLink()) {
|
|
280
|
+
await unlink(targetPath);
|
|
281
|
+
} else {
|
|
282
|
+
throw new AppError(
|
|
283
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
284
|
+
`A file or directory already exists at ${targetPath}`,
|
|
285
|
+
409
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Create symlink
|
|
291
|
+
await symlink(skill.storagePath, targetPath, 'dir');
|
|
292
|
+
|
|
293
|
+
// Record link in database
|
|
294
|
+
const link = await prisma.skillWorkspace.create({
|
|
295
|
+
data: {
|
|
296
|
+
skillId,
|
|
297
|
+
workspaceId,
|
|
298
|
+
},
|
|
299
|
+
include: {
|
|
300
|
+
skill: true,
|
|
301
|
+
workspace: true,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return link;
|
|
306
|
+
} catch (error: any) {
|
|
307
|
+
if (error instanceof AppError) throw error;
|
|
308
|
+
|
|
309
|
+
console.error('Symlink creation error:', error);
|
|
310
|
+
throw new AppError(
|
|
311
|
+
ErrorCode.SYMLINK_ERROR,
|
|
312
|
+
`Failed to create symlink: ${error.message}`,
|
|
313
|
+
500,
|
|
314
|
+
error
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async unlinkSkillFromWorkspace(skillId: string, workspaceId: string) {
|
|
320
|
+
const link = await prisma.skillWorkspace.findFirst({
|
|
321
|
+
where: {
|
|
322
|
+
skillId,
|
|
323
|
+
workspaceId,
|
|
324
|
+
},
|
|
325
|
+
include: {
|
|
326
|
+
skill: true,
|
|
327
|
+
workspace: true,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (!link) {
|
|
332
|
+
throw new AppError(ErrorCode.NOT_FOUND, 'Link not found', 404);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Remove symlink
|
|
336
|
+
const linkPath = join(link.workspace.path, link.skill.name);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
if (existsSync(linkPath)) {
|
|
340
|
+
await rm(linkPath, { recursive: true, force: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Remove link from database
|
|
344
|
+
await prisma.skillWorkspace.delete({
|
|
345
|
+
where: { id: link.id },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return { success: true, message: 'Skill unlinked successfully' };
|
|
349
|
+
} catch (error: any) {
|
|
350
|
+
console.error('Unlink error:', error);
|
|
351
|
+
throw new AppError(
|
|
352
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
353
|
+
`Failed to unlink skill: ${error.message}`,
|
|
354
|
+
500,
|
|
355
|
+
error
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Detect existing skills in a workspace skills directory
|
|
362
|
+
* Returns list of skill names that could be migrated
|
|
363
|
+
*/
|
|
364
|
+
async detectExistingSkills(skillsPath: string): Promise<{ name: string; hasSkillMd: boolean; path: string }[]> {
|
|
365
|
+
if (!existsSync(skillsPath)) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { readdir, stat, readFile } = await import('fs/promises');
|
|
370
|
+
const items = await readdir(skillsPath);
|
|
371
|
+
const existingSkills: { name: string; hasSkillMd: boolean; path: string }[] = [];
|
|
372
|
+
|
|
373
|
+
for (const item of items) {
|
|
374
|
+
// Skip hidden files/directories
|
|
375
|
+
if (item.startsWith('.')) continue;
|
|
376
|
+
|
|
377
|
+
const itemPath = join(skillsPath, item);
|
|
378
|
+
try {
|
|
379
|
+
const itemStat = await stat(itemPath);
|
|
380
|
+
|
|
381
|
+
// Skip symlinks (already linked skills)
|
|
382
|
+
const lstats = lstatSync(itemPath);
|
|
383
|
+
if (lstats.isSymbolicLink()) continue;
|
|
384
|
+
|
|
385
|
+
if (itemStat.isDirectory()) {
|
|
386
|
+
// Check if it has SKILL.md or other skill markers
|
|
387
|
+
const hasSkillMd = existsSync(join(itemPath, 'SKILL.md'));
|
|
388
|
+
const hasSkillJson = existsSync(join(itemPath, 'skill.json'));
|
|
389
|
+
const hasPackageJson = existsSync(join(itemPath, 'package.json'));
|
|
390
|
+
|
|
391
|
+
if (hasSkillMd || hasSkillJson || hasPackageJson) {
|
|
392
|
+
existingSkills.push({
|
|
393
|
+
name: item,
|
|
394
|
+
hasSkillMd,
|
|
395
|
+
path: itemPath,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.warn(`Failed to check ${itemPath}:`, error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return existingSkills;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Migrate existing skills from workspace to unified storage
|
|
409
|
+
* 1. Move skills to ~/.skillverse/skills/
|
|
410
|
+
* 2. Register in database
|
|
411
|
+
* 3. Create symlinks back to workspace
|
|
412
|
+
*/
|
|
413
|
+
async migrateExistingSkills(workspaceId: string, skillNames: string[]): Promise<{ success: boolean; migrated: string[]; errors: string[] }> {
|
|
414
|
+
const workspace = await this.getWorkspaceById(workspaceId);
|
|
415
|
+
const SKILLVERSE_HOME = process.env.SKILLVERSE_HOME || join(homedir(), '.skillverse');
|
|
416
|
+
const SKILLS_DIR = process.env.SKILLS_DIR || join(SKILLVERSE_HOME, 'skills');
|
|
417
|
+
|
|
418
|
+
// Ensure skills directory exists
|
|
419
|
+
if (!existsSync(SKILLS_DIR)) {
|
|
420
|
+
await mkdir(SKILLS_DIR, { recursive: true });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const { rename, readFile, cp } = await import('fs/promises');
|
|
424
|
+
const migrated: string[] = [];
|
|
425
|
+
const errors: string[] = [];
|
|
426
|
+
|
|
427
|
+
for (const skillName of skillNames) {
|
|
428
|
+
const sourcePath = join(workspace.path, skillName);
|
|
429
|
+
const targetPath = join(SKILLS_DIR, skillName);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Check if source exists and is not a symlink
|
|
433
|
+
if (!existsSync(sourcePath)) {
|
|
434
|
+
errors.push(`${skillName}: Source path not found`);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const lstats = lstatSync(sourcePath);
|
|
439
|
+
if (lstats.isSymbolicLink()) {
|
|
440
|
+
errors.push(`${skillName}: Already a symlink, skipping`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if skill already exists in unified storage
|
|
445
|
+
if (existsSync(targetPath)) {
|
|
446
|
+
// Check if it's the same skill by comparing paths or skip
|
|
447
|
+
errors.push(`${skillName}: Already exists in unified storage`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check if skill already exists in database
|
|
452
|
+
const existingSkill = await prisma.skill.findUnique({
|
|
453
|
+
where: { name: skillName },
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
if (existingSkill) {
|
|
457
|
+
errors.push(`${skillName}: Already registered in database`);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Parse metadata before moving
|
|
462
|
+
const skillMdPath = join(sourcePath, 'SKILL.md');
|
|
463
|
+
let description = '';
|
|
464
|
+
let metadata: any = {};
|
|
465
|
+
|
|
466
|
+
if (existsSync(skillMdPath)) {
|
|
467
|
+
try {
|
|
468
|
+
const matter = await import('gray-matter');
|
|
469
|
+
const fileContent = await readFile(skillMdPath, 'utf-8');
|
|
470
|
+
const parsed = matter.default(fileContent);
|
|
471
|
+
description = parsed.data.description || '';
|
|
472
|
+
metadata = parsed.data;
|
|
473
|
+
} catch (e) {
|
|
474
|
+
console.warn(`Failed to parse SKILL.md for ${skillName}:`, e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Move skill to unified storage
|
|
479
|
+
await cp(sourcePath, targetPath, { recursive: true });
|
|
480
|
+
await rm(sourcePath, { recursive: true, force: true });
|
|
481
|
+
|
|
482
|
+
// Register in database
|
|
483
|
+
const skill = await prisma.skill.create({
|
|
484
|
+
data: {
|
|
485
|
+
name: skillName,
|
|
486
|
+
source: 'local',
|
|
487
|
+
description,
|
|
488
|
+
storagePath: targetPath,
|
|
489
|
+
metadata: JSON.stringify(metadata),
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Create symlink back to workspace
|
|
494
|
+
await symlink(targetPath, sourcePath, 'dir');
|
|
495
|
+
|
|
496
|
+
// Create skill-workspace link in database
|
|
497
|
+
await prisma.skillWorkspace.create({
|
|
498
|
+
data: {
|
|
499
|
+
skillId: skill.id,
|
|
500
|
+
workspaceId: workspace.id,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
migrated.push(skillName);
|
|
505
|
+
console.log(`✅ Migrated skill "${skillName}" to unified storage`);
|
|
506
|
+
|
|
507
|
+
} catch (error: any) {
|
|
508
|
+
console.error(`Failed to migrate ${skillName}:`, error);
|
|
509
|
+
errors.push(`${skillName}: ${error.message}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
success: errors.length === 0,
|
|
515
|
+
migrated,
|
|
516
|
+
errors,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export const workspaceService = new WorkspaceService();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { workspaceService } from './services/workspaceService';
|
|
2
|
+
import { prisma } from './lib/db';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { mkdir, symlink, rm, writeFile } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
console.log('Starting verification...');
|
|
10
|
+
|
|
11
|
+
// Setup paths
|
|
12
|
+
const tmpDir = join(tmpdir(), 'skillverse-verify-' + Date.now());
|
|
13
|
+
const projectPath = join(tmpDir, 'project');
|
|
14
|
+
const skillsPath = join(projectPath, '.agent/skills'); // antigravity default
|
|
15
|
+
const skillStoragePath = join(tmpDir, 'storage', 'test-skill');
|
|
16
|
+
|
|
17
|
+
console.log(`Temp dir: ${tmpDir}`);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// 1. Create directories
|
|
21
|
+
await mkdir(projectPath, { recursive: true });
|
|
22
|
+
await mkdir(skillStoragePath, { recursive: true });
|
|
23
|
+
await writeFile(join(skillStoragePath, 'SKILL.md'), 'Test Skill');
|
|
24
|
+
|
|
25
|
+
// 2. Create Skill in DB
|
|
26
|
+
const skillName = 'test-skill-verify-' + Date.now();
|
|
27
|
+
const skill = await prisma.skill.create({
|
|
28
|
+
data: {
|
|
29
|
+
name: skillName,
|
|
30
|
+
source: 'local',
|
|
31
|
+
storagePath: skillStoragePath,
|
|
32
|
+
installDate: new Date(),
|
|
33
|
+
updateAvailable: false
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
console.log(`Created skill: ${skill.id}`);
|
|
37
|
+
|
|
38
|
+
// 3. Create Workspace in DB
|
|
39
|
+
// Use service to create so it creates directory structure
|
|
40
|
+
const workspace = await workspaceService.createWorkspace(
|
|
41
|
+
'verify-workspace-' + Date.now(),
|
|
42
|
+
projectPath,
|
|
43
|
+
'antigravity',
|
|
44
|
+
'project'
|
|
45
|
+
);
|
|
46
|
+
console.log(`Created workspace: ${workspace.id}`);
|
|
47
|
+
|
|
48
|
+
// 4. Link Skill
|
|
49
|
+
// This creates the symlink
|
|
50
|
+
await workspaceService.linkSkillToWorkspace(skill.id, workspace.id);
|
|
51
|
+
console.log('Linked skill to workspace');
|
|
52
|
+
|
|
53
|
+
// 5. Verify Link exists in DB and FS
|
|
54
|
+
let ws = await workspaceService.getWorkspaceById(workspace.id);
|
|
55
|
+
if (ws.linkedSkills.length !== 1) throw new Error('Link not found in DB after linking');
|
|
56
|
+
if (!existsSync(join(skillsPath, skillName))) throw new Error('Symlink not found');
|
|
57
|
+
console.log('Verified link exists.');
|
|
58
|
+
|
|
59
|
+
// 6. Manually delete symlink
|
|
60
|
+
await rm(join(skillsPath, skillName));
|
|
61
|
+
console.log('Deleted symlink manually');
|
|
62
|
+
|
|
63
|
+
// 7. Call getWorkspaceById to trigger sync
|
|
64
|
+
ws = await workspaceService.getWorkspaceById(workspace.id);
|
|
65
|
+
|
|
66
|
+
// 8. Assert link is gone
|
|
67
|
+
if (ws.linkedSkills.length !== 0) {
|
|
68
|
+
// Double check DB directly to be sure it's not just the return value
|
|
69
|
+
const dbLink = await prisma.skillWorkspace.findFirst({ where: { workspaceId: workspace.id, skillId: skill.id } });
|
|
70
|
+
if (dbLink) {
|
|
71
|
+
throw new Error('FAILED: Link still exists in DB after manual deletion!');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log('PASSED: Link removed from DB after manual deletion.');
|
|
75
|
+
|
|
76
|
+
// Cleanup DB
|
|
77
|
+
await prisma.skillWorkspace.deleteMany({ where: { workspaceId: workspace.id } });
|
|
78
|
+
await prisma.workspace.delete({ where: { id: workspace.id } });
|
|
79
|
+
await prisma.skill.delete({ where: { id: skill.id } });
|
|
80
|
+
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Verification failed:', err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
} finally {
|
|
85
|
+
// Cleanup FS
|
|
86
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
87
|
+
await prisma.$disconnect();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"target": "ES2022",
|
|
9
|
+
"lib": ["ES2022"],
|
|
10
|
+
"types": ["node"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules", "dist"],
|
|
14
|
+
"references": [
|
|
15
|
+
{
|
|
16
|
+
"path": "../shared"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts', 'src/bin.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
clean: true,
|
|
7
|
+
splitting: false,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
noExternal: ['@skillverse/shared'], // Bundle shared library
|
|
10
|
+
external: [
|
|
11
|
+
'express',
|
|
12
|
+
'cors',
|
|
13
|
+
'dotenv',
|
|
14
|
+
'prisma',
|
|
15
|
+
'@prisma/client',
|
|
16
|
+
// Add other dependencies that should remain external
|
|
17
|
+
],
|
|
18
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillverse/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.3.3"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
lockfileVersion: '9.0'
|
|
2
|
+
|
|
3
|
+
settings:
|
|
4
|
+
autoInstallPeers: true
|
|
5
|
+
excludeLinksFromLockfile: false
|
|
6
|
+
|
|
7
|
+
importers:
|
|
8
|
+
|
|
9
|
+
.:
|
|
10
|
+
devDependencies:
|
|
11
|
+
typescript:
|
|
12
|
+
specifier: ^5.3.3
|
|
13
|
+
version: 5.9.3
|
|
14
|
+
|
|
15
|
+
packages:
|
|
16
|
+
|
|
17
|
+
typescript@5.9.3:
|
|
18
|
+
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
|
19
|
+
engines: {node: '>=14.17'}
|
|
20
|
+
hasBin: true
|
|
21
|
+
|
|
22
|
+
snapshots:
|
|
23
|
+
|
|
24
|
+
typescript@5.9.3: {}
|