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,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: {}