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,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>client</title>
8
+ <script type="module" crossorigin src="/assets/index-Dfr_6UV8.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BsYtpZSa.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from './index.js';
3
+ import { program } from 'commander';
4
+ import open from 'open';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import readline from 'readline';
8
+
9
+ import { config } from './config.js';
10
+
11
+ const { skillverseHome: SKILLVERSE_HOME } = config;
12
+ const AUTH_FILE = join(SKILLVERSE_HOME, 'auth.json');
13
+ const CONFIG_FILE = join(SKILLVERSE_HOME, 'config.json');
14
+
15
+ // Ensure config directory exists (handled by config.ts)
16
+
17
+ // Helper to get registry URL
18
+ function getRegistryUrl(): string {
19
+ if (existsSync(CONFIG_FILE)) {
20
+ const config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
21
+ return config.registryUrl || 'http://localhost:4000';
22
+ }
23
+ return process.env.SKILLVERSE_REGISTRY || 'http://localhost:4000';
24
+ }
25
+
26
+ // Helper to get auth token
27
+ function getAuthToken(): string | null {
28
+ if (existsSync(AUTH_FILE)) {
29
+ const auth = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
30
+ return auth.token || null;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ // Helper to save auth
36
+ function saveAuth(token: string, user: any): void {
37
+ writeFileSync(AUTH_FILE, JSON.stringify({ token, user }, null, 2));
38
+ }
39
+
40
+ // Helper for prompts
41
+ async function prompt(question: string, hidden = false): Promise<string> {
42
+ const rl = readline.createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout,
45
+ });
46
+
47
+ return new Promise((resolve) => {
48
+ if (hidden) {
49
+ process.stdout.write(question);
50
+ let input = '';
51
+ process.stdin.setRawMode(true);
52
+ process.stdin.resume();
53
+ process.stdin.on('data', (char) => {
54
+ const c = char.toString();
55
+ if (c === '\n' || c === '\r') {
56
+ process.stdin.setRawMode(false);
57
+ process.stdin.pause();
58
+ console.log();
59
+ rl.close();
60
+ resolve(input);
61
+ } else if (c === '\u0003') {
62
+ process.exit();
63
+ } else if (c === '\u007f') {
64
+ input = input.slice(0, -1);
65
+ } else {
66
+ input += c;
67
+ }
68
+ });
69
+ } else {
70
+ rl.question(question, (answer) => {
71
+ rl.close();
72
+ resolve(answer);
73
+ });
74
+ }
75
+ });
76
+ }
77
+
78
+ program
79
+ .name('skillverse')
80
+ .description('SkillVerse - Local-first skill management platform for AI coding assistants')
81
+ .version('0.1.0');
82
+
83
+ program
84
+ .command('start')
85
+ .description('Start the SkillVerse local server')
86
+ .option('-p, --port <number>', 'Port to run server on', '3001')
87
+ .option('--no-open', 'Do not open browser on start')
88
+ .action(async (options) => {
89
+ const port = parseInt(options.port, 10);
90
+
91
+ console.log('');
92
+ console.log(' ╔═══════════════════════════════════════╗');
93
+ console.log(' ║ 🚀 SkillVerse CLI ║');
94
+ console.log(' ╚═══════════════════════════════════════╝');
95
+ console.log('');
96
+
97
+ await startServer(port);
98
+
99
+ if (options.open) {
100
+ const url = `http://localhost:${port}`;
101
+ console.log(`\n🌐 Opening ${url} in your browser...`);
102
+ open(url).catch(err => console.error('Failed to open browser:', err));
103
+ }
104
+
105
+ console.log('\n💡 Press Ctrl+C to stop the server\n');
106
+ });
107
+
108
+ program
109
+ .command('login')
110
+ .description('Login to SkillVerse Registry')
111
+ .option('-r, --registry <url>', 'Registry URL')
112
+ .action(async (options) => {
113
+ const registryUrl = options.registry || getRegistryUrl();
114
+ console.log(`\n🔐 Logging in to ${registryUrl}...\n`);
115
+
116
+ const username = await prompt('Username: ');
117
+ const password = await prompt('Password: ', true);
118
+
119
+ try {
120
+ const response = await fetch(`${registryUrl}/api/auth/login`, {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify({ username, password }),
124
+ });
125
+
126
+ const data = await response.json() as any;
127
+
128
+ if (!response.ok) {
129
+ console.error(`\n❌ Login failed: ${data.error || 'Unknown error'}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ saveAuth(data.data.token, data.data.user);
134
+ console.log(`\n✅ Logged in as ${data.data.user.username}`);
135
+ } catch (error: any) {
136
+ console.error(`\n❌ Failed to connect to registry: ${error.message}`);
137
+ process.exit(1);
138
+ }
139
+ });
140
+
141
+ program
142
+ .command('publish [path]')
143
+ .description('Publish a skill to the Registry')
144
+ .option('-n, --name <name>', 'Skill name (defaults to directory name)')
145
+ .option('-d, --description <desc>', 'Skill description')
146
+ .option('-r, --registry <url>', 'Registry URL')
147
+ .action(async (skillPath, options) => {
148
+ const token = getAuthToken();
149
+ if (!token) {
150
+ console.error('\n❌ Not logged in. Run `skillverse login` first.');
151
+ process.exit(1);
152
+ }
153
+
154
+ const targetPath = skillPath || process.cwd();
155
+ const registryUrl = options.registry || getRegistryUrl();
156
+
157
+ if (!existsSync(targetPath)) {
158
+ console.error(`\n❌ Path not found: ${targetPath}`);
159
+ process.exit(1);
160
+ }
161
+
162
+ const skillName = options.name || targetPath.split('/').pop();
163
+ console.log(`\n📦 Publishing skill "${skillName}" to ${registryUrl}...`);
164
+
165
+ try {
166
+ // Create bundle
167
+ const { bundleService } = await import('./services/bundleService.js');
168
+ const bundlePath = await bundleService.createBundle(targetPath, skillName);
169
+
170
+ // Upload to registry
171
+ const FormData = (await import('form-data')).default;
172
+ const form = new FormData();
173
+ form.append('name', skillName);
174
+ if (options.description) form.append('description', options.description);
175
+ form.append('bundle', readFileSync(bundlePath), {
176
+ filename: `${skillName}.tar.gz`,
177
+ contentType: 'application/gzip',
178
+ });
179
+
180
+ const response = await fetch(`${registryUrl}/api/skills/publish`, {
181
+ method: 'POST',
182
+ headers: {
183
+ 'Authorization': `Bearer ${token}`,
184
+ ...form.getHeaders(),
185
+ },
186
+ body: form as any,
187
+ });
188
+
189
+ const data = await response.json() as any;
190
+
191
+ if (!response.ok) {
192
+ console.error(`\n❌ Publish failed: ${data.error || 'Unknown error'}`);
193
+ process.exit(1);
194
+ }
195
+
196
+ console.log(`\n✅ Published ${skillName}@${data.data.version}`);
197
+ console.log(` 📥 Install: skillverse install ${skillName}`);
198
+ } catch (error: any) {
199
+ console.error(`\n❌ Publish failed: ${error.message}`);
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ program
205
+ .command('search <query>')
206
+ .description('Search for skills in the Registry')
207
+ .option('-r, --registry <url>', 'Registry URL')
208
+ .action(async (query, options) => {
209
+ const registryUrl = options.registry || getRegistryUrl();
210
+ console.log(`\n🔍 Searching for "${query}"...\n`);
211
+
212
+ try {
213
+ const response = await fetch(`${registryUrl}/api/skills?search=${encodeURIComponent(query)}`);
214
+ const data = await response.json() as any;
215
+
216
+ if (!response.ok) {
217
+ console.error(`❌ Search failed: ${data.error || 'Unknown error'}`);
218
+ process.exit(1);
219
+ }
220
+
221
+ if (data.data.items.length === 0) {
222
+ console.log('No skills found.');
223
+ return;
224
+ }
225
+
226
+ console.log(`Found ${data.data.total} skill(s):\n`);
227
+ for (const skill of data.data.items) {
228
+ console.log(` 📦 ${skill.name}@${skill.version}`);
229
+ console.log(` ${skill.description || 'No description'}`);
230
+ console.log(` by ${skill.publisher?.displayName || skill.publisher?.username}`);
231
+ console.log(` 📥 ${skill.downloads} downloads\n`);
232
+ }
233
+ } catch (error: any) {
234
+ console.error(`\n❌ Failed to search: ${error.message}`);
235
+ process.exit(1);
236
+ }
237
+ });
238
+
239
+ program
240
+ .command('config')
241
+ .description('Configure SkillVerse settings')
242
+ .option('-r, --registry <url>', 'Set default registry URL')
243
+ .action((options) => {
244
+ const config: any = existsSync(CONFIG_FILE)
245
+ ? JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'))
246
+ : {};
247
+
248
+ if (options.registry) {
249
+ config.registryUrl = options.registry;
250
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
251
+ console.log(`✅ Registry URL set to: ${options.registry}`);
252
+ } else {
253
+ console.log('\nCurrent Configuration:');
254
+ console.log(` Registry: ${config.registryUrl || getRegistryUrl()}`);
255
+ }
256
+ });
257
+
258
+ import { skillService } from './services/skillService.js';
259
+ import { workspaceService } from './services/workspaceService.js';
260
+ import { WorkspaceType, WorkspaceScope } from '@skillverse/shared';
261
+
262
+ // ... (existing code)
263
+
264
+ program
265
+ .command('install [gitUrl]')
266
+ .description('Install a skill from Git URL')
267
+ .option('-a, --agent <agent>', 'Link to agent workspace (vscode, cursor, etc.)')
268
+ .action(async (gitUrl, options) => {
269
+ if (!gitUrl) {
270
+ console.error('❌ Git URL is required (Registry install not yet supported via CLI)');
271
+ process.exit(1);
272
+ }
273
+
274
+ try {
275
+ console.log(`\n⬇️ Installing skill from ${gitUrl}...`);
276
+ const skill = await skillService.createSkillFromGit(gitUrl);
277
+ console.log(`✅ Installed skill "${skill.name}"`);
278
+
279
+ if (options.agent) {
280
+ const projectPath = process.cwd();
281
+ const type = options.agent as WorkspaceType;
282
+
283
+ // Validate agent type
284
+ const validAgents = ['vscode', 'cursor', 'claude-desktop', 'codex', 'antigravity', 'custom'];
285
+ if (!validAgents.includes(type)) {
286
+ console.error(`❌ Invalid agent type. Valid types: ${validAgents.join(', ')}`);
287
+ process.exit(1);
288
+ }
289
+
290
+ console.log(`\n🔗 Linking to ${type} workspace...`);
291
+
292
+ // Find or create workspace
293
+ const skillsPath = workspaceService.getSkillsPath(projectPath, type, 'project');
294
+ const existingWorkspace = await workspaceService.findWorkspaceByPath(skillsPath);
295
+ let workspaceId = existingWorkspace?.id;
296
+
297
+ if (!workspaceId) {
298
+ console.log(` Creating new workspace for ${type}...`);
299
+ const newWorkspace = await workspaceService.createWorkspace(
300
+ `${type}-workspace`,
301
+ projectPath,
302
+ type,
303
+ 'project'
304
+ );
305
+ workspaceId = newWorkspace.id;
306
+ }
307
+
308
+ await workspaceService.linkSkillToWorkspace(skill.id, workspaceId);
309
+ console.log(`✅ Linked "${skill.name}" to ${type} workspace at ${skillsPath}`);
310
+ }
311
+ } catch (error: any) {
312
+ console.error(`\n❌ Install failed: ${error.message}`);
313
+ process.exit(1);
314
+ }
315
+ });
316
+
317
+ program
318
+ .command('add')
319
+ .description('Add a local skill')
320
+ .requiredOption('-p, --path <path>', 'Path to skill directory')
321
+ .option('-a, --agent <agent>', 'Link to agent workspace')
322
+ .action(async (options) => {
323
+ const sourcePath = options.path.startsWith('/') ? options.path : join(process.cwd(), options.path);
324
+
325
+ if (!existsSync(sourcePath)) {
326
+ console.error(`❌ Source path not found: ${sourcePath}`);
327
+ process.exit(1);
328
+ }
329
+
330
+ try {
331
+ console.log(`\n📦 Adding skill from ${sourcePath}...`);
332
+ const skill = await skillService.createSkillFromDirectory(sourcePath);
333
+ console.log(`✅ Added skill "${skill.name}"`);
334
+
335
+ if (options.agent) {
336
+ const projectPath = process.cwd();
337
+ const type = options.agent as WorkspaceType;
338
+ // Validate agent type
339
+ const validAgents = ['vscode', 'cursor', 'claude-desktop', 'codex', 'antigravity', 'custom'];
340
+ if (!validAgents.includes(type)) {
341
+ console.error(`❌ Invalid agent type. Valid types: ${validAgents.join(', ')}`);
342
+ process.exit(1);
343
+ }
344
+
345
+
346
+ console.log(`\n🔗 Linking to ${type} workspace...`);
347
+ // Find or create workspace
348
+ const skillsPath = workspaceService.getSkillsPath(projectPath, type, 'project');
349
+ const existingWorkspace = await workspaceService.findWorkspaceByPath(skillsPath);
350
+ let workspaceId = existingWorkspace?.id;
351
+
352
+ if (!workspaceId) {
353
+ console.log(` Creating new workspace for ${type}...`);
354
+ const newWorkspace = await workspaceService.createWorkspace(
355
+ `${type}-workspace`,
356
+ projectPath,
357
+ type,
358
+ 'project'
359
+ );
360
+ workspaceId = newWorkspace.id;
361
+ }
362
+
363
+ await workspaceService.linkSkillToWorkspace(skill.id, workspaceId);
364
+ console.log(`✅ Linked "${skill.name}" to ${type} workspace at ${skillsPath}`);
365
+ }
366
+ } catch (error: any) {
367
+ console.error(`\n❌ Add failed: ${error.message}`);
368
+ process.exit(1);
369
+ }
370
+ });
371
+
372
+ program
373
+ .command('list')
374
+ .description('List installed skills')
375
+ .action(async () => {
376
+ try {
377
+ const skills = await skillService.getAllSkills();
378
+
379
+ if (skills.length === 0) {
380
+ console.log('\nNo skills installed.');
381
+ return;
382
+ }
383
+
384
+ console.log(`\n📦 Installed Skills (${skills.length}):\n`);
385
+ console.log('Name'.padEnd(50) + 'Source'.padEnd(10) + 'Linked');
386
+ console.log('-'.repeat(80));
387
+
388
+ for (const skill of skills) {
389
+ const source = skill.source;
390
+ const linkedCount = skill.linkedWorkspaces?.length || 0;
391
+ const updateStatus = skill.updateAvailable ? '*' : '';
392
+
393
+ console.log(
394
+ `${skill.name}${updateStatus}`.padEnd(50) +
395
+ source.padEnd(10) +
396
+ `${linkedCount} workspace(s)`
397
+ );
398
+ }
399
+ console.log('\n(* indicates update available)');
400
+
401
+ } catch (error: any) {
402
+ console.error(`\n❌ List failed: ${error.message}`);
403
+ process.exit(1);
404
+ }
405
+ });
406
+
407
+ program
408
+ .command('remove <name>')
409
+ .description('Remove a skill')
410
+ .action(async (name) => {
411
+ try {
412
+ const skill = await skillService.getSkillByName(name);
413
+ console.log(`\n🗑️ Removing skill "${name}"...`);
414
+
415
+ await skillService.deleteSkill(skill.id);
416
+ console.log(`✅ Skill "${name}" removed successfully`);
417
+ } catch (error: any) {
418
+ console.error(`\n❌ Remove failed: ${error.message}`);
419
+ process.exit(1);
420
+ }
421
+ });
422
+
423
+ program.parse();
424
+
425
+ // Default to start if no command provided
426
+ if (!process.argv.slice(2).length) {
427
+ program.parse(['node', 'skillverse', 'start']);
428
+ }
@@ -0,0 +1,39 @@
1
+ import { join, dirname } from 'path';
2
+ import { existsSync, mkdirSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import dotenv from 'dotenv';
5
+
6
+ // Load .env file
7
+ dotenv.config();
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ export function setupEnvironment() {
13
+ const home = process.env.HOME || process.env.USERPROFILE || '';
14
+ const skillverseHome = process.env.SKILLVERSE_HOME || join(home, '.skillverse');
15
+ const dbUrl = process.env.DATABASE_URL;
16
+
17
+ // Ensure config directory exists
18
+ if (!existsSync(skillverseHome)) {
19
+ try {
20
+ mkdirSync(skillverseHome, { recursive: true });
21
+ } catch (error) {
22
+ console.error('Failed to create .skillverse directory:', error);
23
+ }
24
+ }
25
+
26
+ // Set default database URL if not provided
27
+ if (!dbUrl) {
28
+ const dbPath = join(skillverseHome, 'skillverse.db');
29
+ process.env.DATABASE_URL = `file:${dbPath}`;
30
+ }
31
+
32
+ return {
33
+ skillverseHome,
34
+ databaseUrl: process.env.DATABASE_URL,
35
+ };
36
+ }
37
+
38
+ // Run setup immediately
39
+ export const config = setupEnvironment();
@@ -0,0 +1,112 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import dotenv from 'dotenv';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import { mkdir } from 'fs/promises';
7
+ import { existsSync } from 'fs';
8
+
9
+ // Load environment variables
10
+ dotenv.config();
11
+
12
+ // Import routes
13
+ import skillRoutes from './routes/skills.js';
14
+ import workspaceRoutes from './routes/workspaces.js';
15
+ import marketplaceRoutes from './routes/marketplace.js';
16
+ import dashboardRoutes from './routes/dashboard.js';
17
+
18
+ // Import middleware
19
+ import { errorHandler } from './middleware/errorHandler.js';
20
+ import { requestLogger } from './middleware/logger.js';
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ const app = express();
26
+ const PORT = process.env.PORT || 3001;
27
+
28
+ // Middleware
29
+ app.use(cors());
30
+ app.use(express.json());
31
+ app.use(express.urlencoded({ extended: true }));
32
+ app.use(requestLogger);
33
+
34
+ // Serve static files from 'public' directory (shared with client build)
35
+ // In tsx mode: __dirname is server/src, so ../../public doesn't exist
36
+ // In compiled mode: __dirname is server/dist, so ../public works
37
+ // We try multiple paths to support both modes
38
+ const possiblePublicDirs = [
39
+ join(__dirname, '../public'), // Production: dist/index.js -> public
40
+ join(__dirname, '../../public'), // Dev tsx might resolve here
41
+ join(process.cwd(), 'public'), // Fallback: relative to cwd
42
+ join(process.cwd(), 'server/public'), // Fallback: from root
43
+ ];
44
+
45
+ const publicDir = possiblePublicDirs.find(dir => existsSync(dir));
46
+ if (publicDir) {
47
+ app.use(express.static(publicDir));
48
+ console.log(`📂 Serving static files from: ${publicDir}`);
49
+ } else {
50
+ console.warn('⚠️ No public directory found. Run "npm run build" in client first.');
51
+ }
52
+
53
+ // Routes
54
+ app.use('/api/skills', skillRoutes);
55
+ app.use('/api/workspaces', workspaceRoutes);
56
+ app.use('/api/marketplace', marketplaceRoutes);
57
+ app.use('/api/dashboard', dashboardRoutes);
58
+
59
+ // Health check
60
+ app.get('/health', (req, res) => {
61
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
62
+ });
63
+
64
+ // Serve index.html for all other routes (SPA support)
65
+ // Only if public dir exists, otherwise we might be in dev mode without build
66
+ if (publicDir && existsSync(publicDir)) {
67
+ app.get('*', (req, res) => {
68
+ res.sendFile(join(publicDir, 'index.html'));
69
+ });
70
+ }
71
+
72
+ // Error handling
73
+ app.use(errorHandler);
74
+
75
+ // Initialize storage directories
76
+ async function initializeStorage() {
77
+ const skillverseHome = process.env.SKILLVERSE_HOME || join(process.env.HOME || '', '.skillverse');
78
+ const skillsDir = process.env.SKILLS_DIR || join(skillverseHome, 'skills');
79
+ const marketplaceDir = process.env.MARKETPLACE_DIR || join(skillverseHome, 'marketplace');
80
+
81
+ const dirs = [skillverseHome, skillsDir, marketplaceDir];
82
+
83
+ for (const dir of dirs) {
84
+ if (!existsSync(dir)) {
85
+ await mkdir(dir, { recursive: true });
86
+ console.log(`Created directory: ${dir}`);
87
+ }
88
+ }
89
+ }
90
+
91
+ // Start server
92
+ export async function startServer(port: number = parseInt(process.env.PORT || '3001')) {
93
+ try {
94
+ await initializeStorage();
95
+
96
+ return new Promise<void>((resolve) => {
97
+ app.listen(port, () => {
98
+ console.log(`🚀 SkillVerse server running on http://localhost:${port}`);
99
+ console.log(`📁 Storage: ${process.env.SKILLVERSE_HOME || join(process.env.HOME || '', '.skillverse')}`);
100
+ resolve();
101
+ });
102
+ });
103
+ } catch (error) {
104
+ console.error('Failed to start server:', error);
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ // Auto-start if run directly
110
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
111
+ startServer();
112
+ }
@@ -0,0 +1,14 @@
1
+ import '../config.js';
2
+ import { PrismaClient } from '@prisma/client';
3
+
4
+ const globalForPrisma = globalThis as unknown as {
5
+ prisma: PrismaClient | undefined;
6
+ };
7
+
8
+ export const prisma =
9
+ globalForPrisma.prisma ??
10
+ new PrismaClient({
11
+ log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
12
+ });
13
+
14
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
@@ -0,0 +1,40 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { ErrorCode } from '@skillverse/shared';
3
+
4
+ export class AppError extends Error {
5
+ constructor(
6
+ public code: ErrorCode,
7
+ message: string,
8
+ public statusCode: number = 500,
9
+ public details?: any
10
+ ) {
11
+ super(message);
12
+ this.name = 'AppError';
13
+ }
14
+ }
15
+
16
+ export function errorHandler(
17
+ err: Error | AppError,
18
+ req: Request,
19
+ res: Response,
20
+ next: NextFunction
21
+ ) {
22
+ console.error('Error:', err);
23
+
24
+ if (err instanceof AppError) {
25
+ return res.status(err.statusCode).json({
26
+ success: false,
27
+ error: err.message,
28
+ code: err.code,
29
+ details: err.details,
30
+ });
31
+ }
32
+
33
+ // Handle other errors
34
+ return res.status(500).json({
35
+ success: false,
36
+ error: 'Internal server error',
37
+ code: ErrorCode.INTERNAL_ERROR,
38
+ message: process.env.NODE_ENV === 'development' ? err.message : undefined,
39
+ });
40
+ }
@@ -0,0 +1,12 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ export function requestLogger(req: Request, res: Response, next: NextFunction) {
4
+ const start = Date.now();
5
+
6
+ res.on('finish', () => {
7
+ const duration = Date.now() - start;
8
+ console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
9
+ });
10
+
11
+ next();
12
+ }