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