skillverse 0.1.0 → 0.1.2
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/dist/bin.js +2421 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.js +1907 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -26
- package/prisma/dev.db +0 -0
- package/.prettierrc +0 -10
- package/README.md +0 -369
- package/client/README.md +0 -73
- package/client/eslint.config.js +0 -23
- package/client/index.html +0 -13
- package/client/package.json +0 -41
- package/client/postcss.config.js +0 -6
- package/client/src/App.css +0 -42
- package/client/src/App.tsx +0 -26
- package/client/src/assets/react.svg +0 -1
- package/client/src/components/AddSkillDialog.tsx +0 -249
- package/client/src/components/Layout.tsx +0 -134
- package/client/src/components/LinkWorkspaceDialog.tsx +0 -196
- package/client/src/components/LoadingSpinner.tsx +0 -57
- package/client/src/components/SkillCard.tsx +0 -269
- package/client/src/components/Toast.tsx +0 -44
- package/client/src/components/Tooltip.tsx +0 -132
- package/client/src/index.css +0 -168
- package/client/src/lib/api.ts +0 -196
- package/client/src/main.tsx +0 -10
- package/client/src/pages/Dashboard.tsx +0 -209
- package/client/src/pages/Marketplace.tsx +0 -282
- package/client/src/pages/Settings.tsx +0 -136
- package/client/src/pages/SkillLibrary.tsx +0 -163
- package/client/src/pages/Workspaces.tsx +0 -662
- package/client/src/stores/appStore.ts +0 -222
- package/client/tailwind.config.js +0 -82
- package/client/tsconfig.app.json +0 -28
- package/client/tsconfig.json +0 -7
- package/client/tsconfig.node.json +0 -26
- package/client/vite.config.ts +0 -26
- package/registry/.env.example +0 -5
- package/registry/Dockerfile +0 -42
- package/registry/docker-compose.yml +0 -33
- package/registry/package.json +0 -37
- package/registry/prisma/schema.prisma +0 -59
- package/registry/src/index.ts +0 -34
- package/registry/src/lib/db.ts +0 -3
- package/registry/src/middleware/errorHandler.ts +0 -35
- package/registry/src/routes/auth.ts +0 -152
- package/registry/src/routes/skills.ts +0 -295
- package/registry/tsconfig.json +0 -23
- package/server/.env.example +0 -11
- package/server/package.json +0 -60
- package/server/public/vite.svg +0 -1
- package/server/src/bin.ts +0 -428
- package/server/src/config.ts +0 -39
- package/server/src/index.ts +0 -112
- package/server/src/lib/db.ts +0 -14
- package/server/src/middleware/errorHandler.ts +0 -40
- package/server/src/middleware/logger.ts +0 -12
- package/server/src/routes/dashboard.ts +0 -102
- package/server/src/routes/marketplace.ts +0 -273
- package/server/src/routes/skills.ts +0 -294
- package/server/src/routes/workspaces.ts +0 -168
- package/server/src/services/bundleService.ts +0 -123
- package/server/src/services/skillService.ts +0 -722
- package/server/src/services/workspaceService.ts +0 -521
- package/server/src/verify-sync.ts +0 -91
- package/server/tsconfig.json +0 -19
- package/server/tsup.config.ts +0 -18
- package/shared/package.json +0 -21
- package/shared/pnpm-lock.yaml +0 -24
- package/shared/src/index.ts +0 -169
- package/shared/tsconfig.json +0 -10
- package/tsconfig.json +0 -25
- /package/{server/prisma → prisma}/schema.prisma +0 -0
- /package/{server/public → public}/assets/index-BsYtpZSa.css +0 -0
- /package/{server/public → public}/assets/index-Dfr_6UV8.js +0 -0
- /package/{server/public → public}/index.html +0 -0
- /package/{client/public → public}/vite.svg +0 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { existsSync, mkdirSync } from "fs";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import dotenv from "dotenv";
|
|
17
|
+
function setupEnvironment() {
|
|
18
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
19
|
+
const skillverseHome = process.env.SKILLVERSE_HOME || join(home, ".skillverse");
|
|
20
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
21
|
+
if (!existsSync(skillverseHome)) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(skillverseHome, { recursive: true });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Failed to create .skillverse directory:", error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!dbUrl) {
|
|
29
|
+
const dbPath = join(skillverseHome, "skillverse.db");
|
|
30
|
+
process.env.DATABASE_URL = `file:${dbPath}`;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
skillverseHome,
|
|
34
|
+
databaseUrl: process.env.DATABASE_URL
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var __filename, __dirname, config;
|
|
38
|
+
var init_config = __esm({
|
|
39
|
+
"src/config.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
dotenv.config();
|
|
42
|
+
__filename = fileURLToPath(import.meta.url);
|
|
43
|
+
__dirname = dirname(__filename);
|
|
44
|
+
config = setupEnvironment();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// src/lib/db.ts
|
|
49
|
+
import { PrismaClient } from "@prisma/client";
|
|
50
|
+
var globalForPrisma, prisma;
|
|
51
|
+
var init_db = __esm({
|
|
52
|
+
"src/lib/db.ts"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
init_config();
|
|
55
|
+
globalForPrisma = globalThis;
|
|
56
|
+
prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
57
|
+
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"]
|
|
58
|
+
});
|
|
59
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ../shared/dist/index.js
|
|
64
|
+
var WORKSPACE_SKILLS_PATHS, ErrorCode;
|
|
65
|
+
var init_dist = __esm({
|
|
66
|
+
"../shared/dist/index.js"() {
|
|
67
|
+
"use strict";
|
|
68
|
+
WORKSPACE_SKILLS_PATHS = {
|
|
69
|
+
vscode: {
|
|
70
|
+
project: ".github/skills",
|
|
71
|
+
global: "~/.copilot/skills"
|
|
72
|
+
},
|
|
73
|
+
cursor: {
|
|
74
|
+
project: ".cursor/skills",
|
|
75
|
+
global: "~/.cursor/skills"
|
|
76
|
+
},
|
|
77
|
+
"claude-desktop": {
|
|
78
|
+
project: ".claude/skills",
|
|
79
|
+
global: "~/.claude/skills"
|
|
80
|
+
},
|
|
81
|
+
codex: {
|
|
82
|
+
project: ".codex/skills",
|
|
83
|
+
global: "~/.codex/skills"
|
|
84
|
+
},
|
|
85
|
+
antigravity: {
|
|
86
|
+
project: ".agent/skills",
|
|
87
|
+
global: "~/.agent/skills"
|
|
88
|
+
},
|
|
89
|
+
custom: {
|
|
90
|
+
project: "skills",
|
|
91
|
+
global: ""
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
ErrorCode = {
|
|
95
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
96
|
+
NOT_FOUND: "NOT_FOUND",
|
|
97
|
+
ALREADY_EXISTS: "ALREADY_EXISTS",
|
|
98
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
99
|
+
GIT_ERROR: "GIT_ERROR",
|
|
100
|
+
FILE_SYSTEM_ERROR: "FILE_SYSTEM_ERROR",
|
|
101
|
+
PERMISSION_ERROR: "PERMISSION_ERROR",
|
|
102
|
+
SYMLINK_ERROR: "SYMLINK_ERROR"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/middleware/errorHandler.ts
|
|
108
|
+
function errorHandler(err, req, res, next) {
|
|
109
|
+
console.error("Error:", err);
|
|
110
|
+
if (err instanceof AppError) {
|
|
111
|
+
return res.status(err.statusCode).json({
|
|
112
|
+
success: false,
|
|
113
|
+
error: err.message,
|
|
114
|
+
code: err.code,
|
|
115
|
+
details: err.details
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return res.status(500).json({
|
|
119
|
+
success: false,
|
|
120
|
+
error: "Internal server error",
|
|
121
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
122
|
+
message: process.env.NODE_ENV === "development" ? err.message : void 0
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
var AppError;
|
|
126
|
+
var init_errorHandler = __esm({
|
|
127
|
+
"src/middleware/errorHandler.ts"() {
|
|
128
|
+
"use strict";
|
|
129
|
+
init_dist();
|
|
130
|
+
AppError = class extends Error {
|
|
131
|
+
constructor(code, message, statusCode = 500, details) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.code = code;
|
|
134
|
+
this.statusCode = statusCode;
|
|
135
|
+
this.details = details;
|
|
136
|
+
this.name = "AppError";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/services/skillService.ts
|
|
143
|
+
import { join as join3, basename } from "path";
|
|
144
|
+
import { existsSync as existsSync3 } from "fs";
|
|
145
|
+
import { mkdir, rm, cp, readFile, unlink } from "fs/promises";
|
|
146
|
+
import matter from "gray-matter";
|
|
147
|
+
import simpleGit from "simple-git";
|
|
148
|
+
import AdmZip from "adm-zip";
|
|
149
|
+
function parseGitUrl(url) {
|
|
150
|
+
if (!url.includes("/tree/")) {
|
|
151
|
+
const urlParts = url.split("/");
|
|
152
|
+
const repoName = urlParts[urlParts.length - 1].replace(".git", "");
|
|
153
|
+
return { repoUrl: url, skillName: repoName };
|
|
154
|
+
}
|
|
155
|
+
const parts = url.split("/tree/");
|
|
156
|
+
const repoUrl = parts[0] + (parts[0].endsWith(".git") ? "" : ".git");
|
|
157
|
+
const pathParts = parts[1].split("/");
|
|
158
|
+
const branch = pathParts[0];
|
|
159
|
+
const subdir = pathParts.slice(1).join("/");
|
|
160
|
+
const skillName = pathParts[pathParts.length - 1];
|
|
161
|
+
return { repoUrl, subdir, skillName };
|
|
162
|
+
}
|
|
163
|
+
var SKILLVERSE_HOME, SKILLS_DIR, TEMP_DIR, SkillService, skillService;
|
|
164
|
+
var init_skillService = __esm({
|
|
165
|
+
"src/services/skillService.ts"() {
|
|
166
|
+
"use strict";
|
|
167
|
+
init_db();
|
|
168
|
+
init_errorHandler();
|
|
169
|
+
init_dist();
|
|
170
|
+
SKILLVERSE_HOME = process.env.SKILLVERSE_HOME || join3(process.env.HOME || "", ".skillverse");
|
|
171
|
+
SKILLS_DIR = process.env.SKILLS_DIR || join3(SKILLVERSE_HOME, "skills");
|
|
172
|
+
TEMP_DIR = process.env.TEMP_DIR || join3(SKILLVERSE_HOME, "temp");
|
|
173
|
+
SkillService = class {
|
|
174
|
+
async getAllSkills() {
|
|
175
|
+
const skills = await prisma.skill.findMany({
|
|
176
|
+
include: {
|
|
177
|
+
linkedWorkspaces: {
|
|
178
|
+
include: {
|
|
179
|
+
workspace: true
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
marketplaceEntry: true
|
|
183
|
+
},
|
|
184
|
+
orderBy: {
|
|
185
|
+
installDate: "desc"
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return skills;
|
|
189
|
+
}
|
|
190
|
+
async getSkillById(id) {
|
|
191
|
+
const skill = await prisma.skill.findUnique({
|
|
192
|
+
where: { id },
|
|
193
|
+
include: {
|
|
194
|
+
linkedWorkspaces: {
|
|
195
|
+
include: {
|
|
196
|
+
workspace: true
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
marketplaceEntry: true
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
if (!skill) {
|
|
203
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
|
|
204
|
+
}
|
|
205
|
+
return skill;
|
|
206
|
+
}
|
|
207
|
+
async getSkillByName(name) {
|
|
208
|
+
const skill = await prisma.skill.findUnique({
|
|
209
|
+
where: { name },
|
|
210
|
+
include: {
|
|
211
|
+
linkedWorkspaces: {
|
|
212
|
+
include: {
|
|
213
|
+
workspace: true
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
marketplaceEntry: true
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
if (!skill) {
|
|
220
|
+
throw new AppError(ErrorCode.NOT_FOUND, `Skill "${name}" not found`, 404);
|
|
221
|
+
}
|
|
222
|
+
return skill;
|
|
223
|
+
}
|
|
224
|
+
async parseSkillMetadata(skillPath) {
|
|
225
|
+
try {
|
|
226
|
+
const skillMdPath = join3(skillPath, "SKILL.md");
|
|
227
|
+
let description = "";
|
|
228
|
+
let metadata = {};
|
|
229
|
+
if (existsSync3(skillMdPath)) {
|
|
230
|
+
const fileContent = await readFile(skillMdPath, "utf-8");
|
|
231
|
+
const parsed = matter(fileContent);
|
|
232
|
+
description = parsed.data.description || "";
|
|
233
|
+
metadata = parsed.data;
|
|
234
|
+
}
|
|
235
|
+
if (!description) {
|
|
236
|
+
const packageJsonPath = join3(skillPath, "package.json");
|
|
237
|
+
const skillJsonPath = join3(skillPath, "skill.json");
|
|
238
|
+
if (existsSync3(skillJsonPath)) {
|
|
239
|
+
const content = await readFile(skillJsonPath, "utf-8");
|
|
240
|
+
const json = JSON.parse(content);
|
|
241
|
+
description = json.description || "";
|
|
242
|
+
metadata = { ...metadata, ...json };
|
|
243
|
+
} else if (existsSync3(packageJsonPath)) {
|
|
244
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
245
|
+
const pkg = JSON.parse(content);
|
|
246
|
+
description = pkg.description || "";
|
|
247
|
+
metadata = { ...metadata, name: pkg.name, version: pkg.version };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { description, metadata };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.warn("Failed to parse skill metadata:", error);
|
|
253
|
+
return { description: "", metadata: {} };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async createSkillFromGit(gitUrl, description) {
|
|
257
|
+
let tempPath = null;
|
|
258
|
+
try {
|
|
259
|
+
const { repoUrl, subdir, skillName } = parseGitUrl(gitUrl);
|
|
260
|
+
const existingSkill = await prisma.skill.findUnique({
|
|
261
|
+
where: { name: skillName }
|
|
262
|
+
});
|
|
263
|
+
if (existingSkill) {
|
|
264
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
|
|
265
|
+
}
|
|
266
|
+
const skillPath = join3(SKILLS_DIR, skillName);
|
|
267
|
+
if (existsSync3(skillPath)) {
|
|
268
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
|
|
269
|
+
}
|
|
270
|
+
let commitHash = "";
|
|
271
|
+
if (subdir) {
|
|
272
|
+
tempPath = join3(TEMP_DIR, `git-clone-${Date.now()}`);
|
|
273
|
+
await mkdir(tempPath, { recursive: true });
|
|
274
|
+
console.log(`Cloning ${repoUrl} to temp path for extraction...`);
|
|
275
|
+
const git = simpleGit();
|
|
276
|
+
await git.clone(repoUrl, tempPath);
|
|
277
|
+
try {
|
|
278
|
+
commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
|
|
279
|
+
console.log(`Captured commit hash: ${commitHash}`);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.warn("Failed to capture commit hash:", e);
|
|
282
|
+
}
|
|
283
|
+
const sourcePath = join3(tempPath, subdir);
|
|
284
|
+
if (!existsSync3(sourcePath)) {
|
|
285
|
+
throw new AppError(
|
|
286
|
+
ErrorCode.GIT_ERROR,
|
|
287
|
+
`Subdirectory "${subdir}" not found in repository`,
|
|
288
|
+
400
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const hasSkillMd = existsSync3(join3(sourcePath, "SKILL.md"));
|
|
292
|
+
const hasSkillJson = existsSync3(join3(sourcePath, "skill.json"));
|
|
293
|
+
const hasPackageJson = existsSync3(join3(sourcePath, "package.json"));
|
|
294
|
+
if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
|
|
295
|
+
throw new AppError(
|
|
296
|
+
ErrorCode.VALIDATION_ERROR,
|
|
297
|
+
`Invalid skill structure: "${subdir}" does not contain SKILL.md, skill.json, or package.json`,
|
|
298
|
+
400
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
await mkdir(skillPath, { recursive: true });
|
|
302
|
+
await cp(sourcePath, skillPath, { recursive: true });
|
|
303
|
+
} else {
|
|
304
|
+
if (!existsSync3(skillPath)) {
|
|
305
|
+
await mkdir(skillPath, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
const git = simpleGit();
|
|
308
|
+
console.log(`Cloning ${gitUrl} to ${skillPath}...`);
|
|
309
|
+
await git.clone(gitUrl, skillPath);
|
|
310
|
+
try {
|
|
311
|
+
commitHash = await git.cwd(skillPath).revparse(["HEAD"]);
|
|
312
|
+
console.log(`Captured commit hash: ${commitHash}`);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.warn("Failed to capture commit hash:", e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const parsed = await this.parseSkillMetadata(skillPath);
|
|
318
|
+
const skill = await prisma.skill.create({
|
|
319
|
+
data: {
|
|
320
|
+
name: skillName,
|
|
321
|
+
source: "git",
|
|
322
|
+
sourceUrl: gitUrl,
|
|
323
|
+
repoUrl,
|
|
324
|
+
commitHash,
|
|
325
|
+
description: description || parsed.description || "",
|
|
326
|
+
storagePath: skillPath,
|
|
327
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
return skill;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw error;
|
|
333
|
+
} finally {
|
|
334
|
+
if (tempPath && existsSync3(tempPath)) {
|
|
335
|
+
await rm(tempPath, { recursive: true, force: true }).catch(console.error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async createSkillFromDirectory(path, name, description) {
|
|
340
|
+
if (!existsSync3(path)) {
|
|
341
|
+
throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, `Source path not found: ${path}`, 400);
|
|
342
|
+
}
|
|
343
|
+
const skillName = name || basename(path);
|
|
344
|
+
const skillPath = join3(SKILLS_DIR, skillName);
|
|
345
|
+
const existingSkill = await prisma.skill.findUnique({
|
|
346
|
+
where: { name: skillName }
|
|
347
|
+
});
|
|
348
|
+
if (existingSkill) {
|
|
349
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
|
|
350
|
+
}
|
|
351
|
+
if (existsSync3(skillPath)) {
|
|
352
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
await mkdir(skillPath, { recursive: true });
|
|
356
|
+
await cp(path, skillPath, { recursive: true });
|
|
357
|
+
const parsed = await this.parseSkillMetadata(skillPath);
|
|
358
|
+
const skill = await prisma.skill.create({
|
|
359
|
+
data: {
|
|
360
|
+
name: skillName,
|
|
361
|
+
source: "local",
|
|
362
|
+
description: description || parsed.description || "",
|
|
363
|
+
storagePath: skillPath,
|
|
364
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return skill;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (existsSync3(skillPath)) {
|
|
370
|
+
await rm(skillPath, { recursive: true, force: true }).catch(() => {
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Helper to handle cases where the archive contains a single top-level directory.
|
|
378
|
+
* Moves contents up one level if that's the case.
|
|
379
|
+
*/
|
|
380
|
+
async stripTopLevelDirectory(targetPath) {
|
|
381
|
+
const { readdir: readdir2, rename, rmdir, stat: stat2 } = await import("fs/promises");
|
|
382
|
+
const items = await readdir2(targetPath);
|
|
383
|
+
const validItems = items.filter((i) => i !== ".DS_Store" && i !== "__MACOSX");
|
|
384
|
+
if (validItems.length === 1) {
|
|
385
|
+
const topLevelItem = validItems[0];
|
|
386
|
+
const topLevelPath = join3(targetPath, topLevelItem);
|
|
387
|
+
const stats = await stat2(topLevelPath);
|
|
388
|
+
if (stats.isDirectory()) {
|
|
389
|
+
console.log(`Striping top-level directory: ${topLevelItem}`);
|
|
390
|
+
const subItems = await readdir2(topLevelPath);
|
|
391
|
+
for (const item of subItems) {
|
|
392
|
+
await rename(join3(topLevelPath, item), join3(targetPath, item));
|
|
393
|
+
}
|
|
394
|
+
await rmdir(topLevelPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async createSkillFromLocal(name, zipPath, description) {
|
|
399
|
+
const skillPath = join3(SKILLS_DIR, name);
|
|
400
|
+
const existingSkill = await prisma.skill.findUnique({
|
|
401
|
+
where: { name }
|
|
402
|
+
});
|
|
403
|
+
if (existingSkill) {
|
|
404
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${name}" already exists`, 409);
|
|
405
|
+
}
|
|
406
|
+
if (existsSync3(skillPath)) {
|
|
407
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
|
|
408
|
+
}
|
|
409
|
+
const extractPath = join3(TEMP_DIR, `extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
|
410
|
+
await mkdir(extractPath, { recursive: true });
|
|
411
|
+
try {
|
|
412
|
+
const zip = new AdmZip(zipPath);
|
|
413
|
+
zip.extractAllTo(extractPath, true);
|
|
414
|
+
await this.stripTopLevelDirectory(extractPath);
|
|
415
|
+
const parsed = await this.parseSkillMetadata(extractPath);
|
|
416
|
+
if (existsSync3(skillPath)) {
|
|
417
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
|
|
418
|
+
}
|
|
419
|
+
await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
|
|
420
|
+
const skill = await prisma.skill.create({
|
|
421
|
+
data: {
|
|
422
|
+
name,
|
|
423
|
+
source: "local",
|
|
424
|
+
description: description || parsed.description || "",
|
|
425
|
+
storagePath: skillPath,
|
|
426
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return skill;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (existsSync3(skillPath)) {
|
|
432
|
+
await rm(skillPath, { recursive: true, force: true }).catch(() => {
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (existsSync3(extractPath)) {
|
|
436
|
+
await rm(extractPath, { recursive: true, force: true }).catch(() => {
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Create a skill from a tar.gz bundle (marketplace install)
|
|
444
|
+
* @param bundlePath - Path to the .tar.gz bundle
|
|
445
|
+
* @param originalName - Original skill name
|
|
446
|
+
* @param description - Optional skill description
|
|
447
|
+
*/
|
|
448
|
+
async createSkillFromBundle(bundlePath, originalName, description) {
|
|
449
|
+
let name = originalName;
|
|
450
|
+
let counter = 1;
|
|
451
|
+
while (await prisma.skill.findUnique({ where: { name } })) {
|
|
452
|
+
name = `${originalName}-${counter}`;
|
|
453
|
+
counter++;
|
|
454
|
+
}
|
|
455
|
+
const skillPath = join3(SKILLS_DIR, name);
|
|
456
|
+
const extractPath = join3(TEMP_DIR, `extract-bundle-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
|
457
|
+
await mkdir(extractPath, { recursive: true });
|
|
458
|
+
try {
|
|
459
|
+
const { createReadStream: createReadStream2 } = await import("fs");
|
|
460
|
+
const { createGunzip: createGunzip2 } = await import("zlib");
|
|
461
|
+
const tar = await import("tar");
|
|
462
|
+
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
463
|
+
await pipeline2(
|
|
464
|
+
createReadStream2(bundlePath),
|
|
465
|
+
createGunzip2(),
|
|
466
|
+
tar.extract({ cwd: extractPath })
|
|
467
|
+
);
|
|
468
|
+
await this.stripTopLevelDirectory(extractPath);
|
|
469
|
+
const parsed = await this.parseSkillMetadata(extractPath);
|
|
470
|
+
if (existsSync3(skillPath)) {
|
|
471
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
|
|
472
|
+
}
|
|
473
|
+
await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
|
|
474
|
+
const skill = await prisma.skill.create({
|
|
475
|
+
data: {
|
|
476
|
+
name,
|
|
477
|
+
source: "local",
|
|
478
|
+
description: description || parsed.description || "",
|
|
479
|
+
storagePath: skillPath,
|
|
480
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
console.log(`\u{1F4E6} Installed skill "${name}" from bundle`);
|
|
484
|
+
return skill;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (existsSync3(skillPath)) {
|
|
487
|
+
await rm(skillPath, { recursive: true, force: true }).catch(() => {
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (existsSync3(extractPath)) {
|
|
491
|
+
await rm(extractPath, { recursive: true, force: true }).catch(() => {
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async updateSkill(id, data) {
|
|
498
|
+
const skill = await this.getSkillById(id);
|
|
499
|
+
const updatedSkill = await prisma.skill.update({
|
|
500
|
+
where: { id },
|
|
501
|
+
data: {
|
|
502
|
+
...data.name && { name: data.name },
|
|
503
|
+
...data.description !== void 0 && { description: data.description },
|
|
504
|
+
...data.metadata && { metadata: JSON.stringify(data.metadata) }
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
return updatedSkill;
|
|
508
|
+
}
|
|
509
|
+
async deleteSkill(id, removeFiles = true) {
|
|
510
|
+
const skill = await this.getSkillById(id);
|
|
511
|
+
if (skill.linkedWorkspaces && skill.linkedWorkspaces.length > 0) {
|
|
512
|
+
for (const link of skill.linkedWorkspaces) {
|
|
513
|
+
try {
|
|
514
|
+
const workspacePath = link.workspace.path;
|
|
515
|
+
const symlinkPath = join3(workspacePath, skill.name);
|
|
516
|
+
if (existsSync3(symlinkPath)) {
|
|
517
|
+
const stats = await import("fs/promises").then((fs) => fs.lstat(symlinkPath));
|
|
518
|
+
if (stats.isSymbolicLink()) {
|
|
519
|
+
await unlink(symlinkPath);
|
|
520
|
+
console.log(`Removed symlink for ${skill.name} in workspace ${workspacePath}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.warn(`Failed to remove symlink for ${skill.name} in workspace ${link.workspace.path}:`, error);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
await prisma.skill.delete({
|
|
529
|
+
where: { id }
|
|
530
|
+
});
|
|
531
|
+
if (removeFiles && skill.storagePath) {
|
|
532
|
+
await rm(skill.storagePath, { recursive: true, force: true }).catch((err) => {
|
|
533
|
+
console.warn(`Failed to delete skill directory ${skill.storagePath}:`, err);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
return { success: true, message: "Skill deleted successfully" };
|
|
537
|
+
}
|
|
538
|
+
// Check for updates
|
|
539
|
+
async checkForUpdate(id) {
|
|
540
|
+
const skill = await this.getSkillById(id);
|
|
541
|
+
if (skill.source !== "git" || !skill.repoUrl) {
|
|
542
|
+
return {
|
|
543
|
+
hasUpdate: false,
|
|
544
|
+
currentHash: skill.commitHash,
|
|
545
|
+
remoteHash: null,
|
|
546
|
+
message: "Not a git skill or missing repo URL"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const tempPath = join3(TEMP_DIR, `check-update-${Date.now()}`);
|
|
550
|
+
await mkdir(tempPath, { recursive: true });
|
|
551
|
+
try {
|
|
552
|
+
const git = simpleGit();
|
|
553
|
+
console.log(`Checking updates for ${skill.name} from ${skill.repoUrl}...`);
|
|
554
|
+
const remote = await git.listRemote([skill.repoUrl, "HEAD"]);
|
|
555
|
+
if (!remote) {
|
|
556
|
+
throw new Error("Failed to get remote HEAD");
|
|
557
|
+
}
|
|
558
|
+
const remoteHash = remote.split(" ")[0];
|
|
559
|
+
const hasUpdate = remoteHash !== skill.commitHash;
|
|
560
|
+
await prisma.skill.update({
|
|
561
|
+
where: { id },
|
|
562
|
+
data: {
|
|
563
|
+
lastUpdateCheck: /* @__PURE__ */ new Date(),
|
|
564
|
+
updateAvailable: hasUpdate
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
hasUpdate,
|
|
569
|
+
currentHash: skill.commitHash,
|
|
570
|
+
remoteHash
|
|
571
|
+
};
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.error("Update check error:", error);
|
|
574
|
+
throw new AppError(ErrorCode.GIT_ERROR, `Failed to check for updates: ${error.message}`, 500);
|
|
575
|
+
} finally {
|
|
576
|
+
await rm(tempPath, { recursive: true, force: true }).catch(() => {
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async checkUpdates(ids) {
|
|
581
|
+
const whereClause = {
|
|
582
|
+
source: "git",
|
|
583
|
+
repoUrl: { not: null }
|
|
584
|
+
};
|
|
585
|
+
if (ids && ids.length > 0) {
|
|
586
|
+
whereClause.id = { in: ids };
|
|
587
|
+
}
|
|
588
|
+
const skills = await prisma.skill.findMany({ where: whereClause });
|
|
589
|
+
const results = {};
|
|
590
|
+
console.log(`Checking updates for ${skills.length} skills...`);
|
|
591
|
+
await Promise.all(skills.map(async (skill) => {
|
|
592
|
+
try {
|
|
593
|
+
const result = await this.checkForUpdate(skill.id);
|
|
594
|
+
results[skill.id] = result;
|
|
595
|
+
} catch (e) {
|
|
596
|
+
results[skill.id] = { error: e.message };
|
|
597
|
+
}
|
|
598
|
+
}));
|
|
599
|
+
return results;
|
|
600
|
+
}
|
|
601
|
+
async refreshMetadata(id) {
|
|
602
|
+
const skill = await this.getSkillById(id);
|
|
603
|
+
const parsed = await this.parseSkillMetadata(skill.storagePath);
|
|
604
|
+
const updated = await prisma.skill.update({
|
|
605
|
+
where: { id },
|
|
606
|
+
data: {
|
|
607
|
+
description: parsed.description,
|
|
608
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
return updated;
|
|
612
|
+
}
|
|
613
|
+
async upgradeSkill(id) {
|
|
614
|
+
const skill = await this.getSkillById(id);
|
|
615
|
+
if (skill.source !== "git" || !skill.sourceUrl) {
|
|
616
|
+
throw new AppError(ErrorCode.VALIDATION_ERROR, "Cannot upgrade non-git skill", 400);
|
|
617
|
+
}
|
|
618
|
+
let tempPath = null;
|
|
619
|
+
let tempSkillPath = null;
|
|
620
|
+
try {
|
|
621
|
+
const { repoUrl, subdir } = parseGitUrl(skill.sourceUrl);
|
|
622
|
+
tempPath = join3(TEMP_DIR, `upgrade-${Date.now()}`);
|
|
623
|
+
await mkdir(tempPath, { recursive: true });
|
|
624
|
+
const git = simpleGit();
|
|
625
|
+
console.log(`Cloning ${repoUrl} to temp for upgrade...`);
|
|
626
|
+
await git.clone(repoUrl, tempPath);
|
|
627
|
+
let commitHash = "";
|
|
628
|
+
try {
|
|
629
|
+
commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
|
|
630
|
+
} catch (e) {
|
|
631
|
+
console.warn("Failed to capture commit hash during upgrade:", e);
|
|
632
|
+
}
|
|
633
|
+
let sourcePath = tempPath;
|
|
634
|
+
if (subdir) {
|
|
635
|
+
sourcePath = join3(tempPath, subdir);
|
|
636
|
+
if (!existsSync3(sourcePath)) {
|
|
637
|
+
throw new AppError(ErrorCode.GIT_ERROR, `Subdirectory "${subdir}" not found`, 400);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const hasSkillMd = existsSync3(join3(sourcePath, "SKILL.md"));
|
|
641
|
+
const hasSkillJson = existsSync3(join3(sourcePath, "skill.json"));
|
|
642
|
+
const hasPackageJson = existsSync3(join3(sourcePath, "package.json"));
|
|
643
|
+
if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
|
|
644
|
+
throw new AppError(ErrorCode.VALIDATION_ERROR, "Invalid skill structure in new version", 400);
|
|
645
|
+
}
|
|
646
|
+
const files = await import("fs/promises").then((fs) => fs.readdir(skill.storagePath));
|
|
647
|
+
for (const file of files) {
|
|
648
|
+
await rm(join3(skill.storagePath, file), { recursive: true, force: true });
|
|
649
|
+
}
|
|
650
|
+
await cp(sourcePath, skill.storagePath, { recursive: true });
|
|
651
|
+
const parsed = await this.parseSkillMetadata(skill.storagePath);
|
|
652
|
+
const updatedSkill = await prisma.skill.update({
|
|
653
|
+
where: { id },
|
|
654
|
+
data: {
|
|
655
|
+
commitHash,
|
|
656
|
+
updateAvailable: false,
|
|
657
|
+
lastUpdateCheck: /* @__PURE__ */ new Date(),
|
|
658
|
+
installDate: /* @__PURE__ */ new Date(),
|
|
659
|
+
// Considering upgrade as reinstall? Or user prefer generic updatedAt
|
|
660
|
+
// Keeping installDate as "last installed/upgraded"
|
|
661
|
+
description: parsed.description,
|
|
662
|
+
metadata: JSON.stringify(parsed.metadata)
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
return updatedSkill;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.error("Upgrade error:", error);
|
|
668
|
+
throw new AppError(ErrorCode.GIT_ERROR, `Failed to upgrade skill: ${error.message}`, 500);
|
|
669
|
+
} finally {
|
|
670
|
+
if (tempPath) await rm(tempPath, { recursive: true, force: true }).catch(() => {
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
skillService = new SkillService();
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// src/services/workspaceService.ts
|
|
680
|
+
import { join as join4 } from "path";
|
|
681
|
+
import { homedir } from "os";
|
|
682
|
+
import { symlink, unlink as unlink2, rm as rm2, mkdir as mkdir2, appendFile, readFile as readFile2 } from "fs/promises";
|
|
683
|
+
import { existsSync as existsSync4, lstatSync } from "fs";
|
|
684
|
+
function computeSkillsPath(projectPath, type, scope) {
|
|
685
|
+
const pathConfig = WORKSPACE_SKILLS_PATHS[type];
|
|
686
|
+
if (scope === "global") {
|
|
687
|
+
return pathConfig.global.replace("~", homedir());
|
|
688
|
+
}
|
|
689
|
+
return join4(projectPath, pathConfig.project);
|
|
690
|
+
}
|
|
691
|
+
var WorkspaceService, workspaceService;
|
|
692
|
+
var init_workspaceService = __esm({
|
|
693
|
+
"src/services/workspaceService.ts"() {
|
|
694
|
+
"use strict";
|
|
695
|
+
init_db();
|
|
696
|
+
init_errorHandler();
|
|
697
|
+
init_dist();
|
|
698
|
+
WorkspaceService = class {
|
|
699
|
+
/**
|
|
700
|
+
* Helper to get expected skills path for a project
|
|
701
|
+
*/
|
|
702
|
+
getSkillsPath(projectPath, type, scope) {
|
|
703
|
+
return computeSkillsPath(projectPath, type, scope);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Sync database links with filesystem state
|
|
707
|
+
* Removes links from DB if the symlink is missing from the workspace
|
|
708
|
+
*/
|
|
709
|
+
async syncLinks(workspace) {
|
|
710
|
+
if (!workspace.linkedSkills || workspace.linkedSkills.length === 0) return;
|
|
711
|
+
for (const link of workspace.linkedSkills) {
|
|
712
|
+
if (!link.skill) continue;
|
|
713
|
+
const linkPath = join4(workspace.path, link.skill.name);
|
|
714
|
+
if (!existsSync4(linkPath)) {
|
|
715
|
+
console.log(`Link missing for skill "${link.skill.name}" in workspace "${workspace.name}". Removing from DB.`);
|
|
716
|
+
try {
|
|
717
|
+
await prisma.skillWorkspace.delete({
|
|
718
|
+
where: { id: link.id }
|
|
719
|
+
});
|
|
720
|
+
workspace.linkedSkills = workspace.linkedSkills.filter((l) => l.id !== link.id);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.error(`Failed to sync link for ${link.skill.name}:`, err);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async findWorkspaceByPath(path) {
|
|
728
|
+
const workspace = await prisma.workspace.findUnique({
|
|
729
|
+
where: { path },
|
|
730
|
+
include: {
|
|
731
|
+
linkedSkills: {
|
|
732
|
+
include: {
|
|
733
|
+
skill: true
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
return workspace;
|
|
739
|
+
}
|
|
740
|
+
async getAllWorkspaces() {
|
|
741
|
+
const workspaces = await prisma.workspace.findMany({
|
|
742
|
+
include: {
|
|
743
|
+
linkedSkills: {
|
|
744
|
+
include: {
|
|
745
|
+
skill: true
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
orderBy: {
|
|
750
|
+
createdAt: "desc"
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
await Promise.all(workspaces.map((w) => this.syncLinks(w)));
|
|
754
|
+
return workspaces.map((workspace) => ({
|
|
755
|
+
...workspace,
|
|
756
|
+
isPathValid: existsSync4(workspace.path)
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
async getWorkspaceById(id) {
|
|
760
|
+
const workspace = await prisma.workspace.findUnique({
|
|
761
|
+
where: { id },
|
|
762
|
+
include: {
|
|
763
|
+
linkedSkills: {
|
|
764
|
+
include: {
|
|
765
|
+
skill: true
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
if (!workspace) {
|
|
771
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Workspace not found", 404);
|
|
772
|
+
}
|
|
773
|
+
await this.syncLinks(workspace);
|
|
774
|
+
return {
|
|
775
|
+
...workspace,
|
|
776
|
+
isPathValid: existsSync4(workspace.path)
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
async createWorkspace(name, projectPath, type, scope) {
|
|
780
|
+
const skillsPath = computeSkillsPath(projectPath, type, scope);
|
|
781
|
+
if (scope === "project" && projectPath && !existsSync4(projectPath)) {
|
|
782
|
+
throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, `Project path does not exist: ${projectPath}`, 400);
|
|
783
|
+
}
|
|
784
|
+
if (!existsSync4(skillsPath)) {
|
|
785
|
+
try {
|
|
786
|
+
await mkdir2(skillsPath, { recursive: true });
|
|
787
|
+
} catch (err) {
|
|
788
|
+
throw new AppError(
|
|
789
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
790
|
+
`Failed to create skills directory: ${err.message}`,
|
|
791
|
+
500
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const existingWorkspace = await prisma.workspace.findUnique({
|
|
796
|
+
where: { path: skillsPath }
|
|
797
|
+
});
|
|
798
|
+
if (existingWorkspace) {
|
|
799
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, "Workspace already exists at this path", 409);
|
|
800
|
+
}
|
|
801
|
+
const workspace = await prisma.workspace.create({
|
|
802
|
+
data: {
|
|
803
|
+
name,
|
|
804
|
+
path: skillsPath,
|
|
805
|
+
type,
|
|
806
|
+
scope
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
if (scope === "project" && projectPath && existsSync4(projectPath)) {
|
|
810
|
+
try {
|
|
811
|
+
const gitignorePath = join4(projectPath, ".gitignore");
|
|
812
|
+
const relativeSkillsPath = WORKSPACE_SKILLS_PATHS[type].project;
|
|
813
|
+
let shouldAppend = false;
|
|
814
|
+
if (existsSync4(gitignorePath)) {
|
|
815
|
+
const content = await readFile2(gitignorePath, "utf-8");
|
|
816
|
+
if (!content.includes(relativeSkillsPath)) {
|
|
817
|
+
shouldAppend = true;
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
shouldAppend = true;
|
|
821
|
+
}
|
|
822
|
+
if (shouldAppend) {
|
|
823
|
+
const ignoreEntry = `
|
|
824
|
+
|
|
825
|
+
# SkillVerse
|
|
826
|
+
${relativeSkillsPath}/
|
|
827
|
+
`;
|
|
828
|
+
await appendFile(gitignorePath, ignoreEntry);
|
|
829
|
+
console.log(`Added ${relativeSkillsPath}/ to .gitignore in ${projectPath}`);
|
|
830
|
+
}
|
|
831
|
+
} catch (err) {
|
|
832
|
+
console.warn("Failed to update .gitignore:", err);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
...workspace,
|
|
837
|
+
isPathValid: true
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async updateWorkspace(id, data) {
|
|
841
|
+
await this.getWorkspaceById(id);
|
|
842
|
+
const workspace = await prisma.workspace.update({
|
|
843
|
+
where: { id },
|
|
844
|
+
data
|
|
845
|
+
});
|
|
846
|
+
return {
|
|
847
|
+
...workspace,
|
|
848
|
+
isPathValid: existsSync4(workspace.path)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
async deleteWorkspace(id) {
|
|
852
|
+
const workspace = await this.getWorkspaceById(id);
|
|
853
|
+
const links = await prisma.skillWorkspace.findMany({
|
|
854
|
+
where: { workspaceId: id },
|
|
855
|
+
include: { skill: true }
|
|
856
|
+
});
|
|
857
|
+
for (const link of links) {
|
|
858
|
+
try {
|
|
859
|
+
const linkPath = join4(workspace.path, link.skill.name);
|
|
860
|
+
if (existsSync4(linkPath)) {
|
|
861
|
+
await rm2(linkPath, { recursive: true, force: true });
|
|
862
|
+
}
|
|
863
|
+
} catch (error) {
|
|
864
|
+
console.error(`Failed to remove symlink for ${link.skill.name}:`, error);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
await prisma.workspace.delete({ where: { id } });
|
|
868
|
+
return { success: true, message: "Workspace deleted successfully" };
|
|
869
|
+
}
|
|
870
|
+
async linkSkillToWorkspace(skillId, workspaceId) {
|
|
871
|
+
const skill = await prisma.skill.findUnique({ where: { id: skillId } });
|
|
872
|
+
const workspace = await prisma.workspace.findUnique({ where: { id: workspaceId } });
|
|
873
|
+
if (!skill) {
|
|
874
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
|
|
875
|
+
}
|
|
876
|
+
if (!workspace) {
|
|
877
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Workspace not found", 404);
|
|
878
|
+
}
|
|
879
|
+
if (!existsSync4(workspace.path)) {
|
|
880
|
+
throw new AppError(
|
|
881
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
882
|
+
`Workspace skills directory does not exist: ${workspace.path}. The folder may have been deleted. Please recreate it or remove this workspace.`,
|
|
883
|
+
400
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const existingLink = await prisma.skillWorkspace.findFirst({
|
|
887
|
+
where: {
|
|
888
|
+
skillId,
|
|
889
|
+
workspaceId
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
if (existingLink) {
|
|
893
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, "Skill is already linked to this workspace", 409);
|
|
894
|
+
}
|
|
895
|
+
const targetPath = join4(workspace.path, skill.name);
|
|
896
|
+
try {
|
|
897
|
+
if (existsSync4(targetPath)) {
|
|
898
|
+
const stats = lstatSync(targetPath);
|
|
899
|
+
if (stats.isSymbolicLink()) {
|
|
900
|
+
await unlink2(targetPath);
|
|
901
|
+
} else {
|
|
902
|
+
throw new AppError(
|
|
903
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
904
|
+
`A file or directory already exists at ${targetPath}`,
|
|
905
|
+
409
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
await symlink(skill.storagePath, targetPath, "dir");
|
|
910
|
+
const link = await prisma.skillWorkspace.create({
|
|
911
|
+
data: {
|
|
912
|
+
skillId,
|
|
913
|
+
workspaceId
|
|
914
|
+
},
|
|
915
|
+
include: {
|
|
916
|
+
skill: true,
|
|
917
|
+
workspace: true
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return link;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (error instanceof AppError) throw error;
|
|
923
|
+
console.error("Symlink creation error:", error);
|
|
924
|
+
throw new AppError(
|
|
925
|
+
ErrorCode.SYMLINK_ERROR,
|
|
926
|
+
`Failed to create symlink: ${error.message}`,
|
|
927
|
+
500,
|
|
928
|
+
error
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async unlinkSkillFromWorkspace(skillId, workspaceId) {
|
|
933
|
+
const link = await prisma.skillWorkspace.findFirst({
|
|
934
|
+
where: {
|
|
935
|
+
skillId,
|
|
936
|
+
workspaceId
|
|
937
|
+
},
|
|
938
|
+
include: {
|
|
939
|
+
skill: true,
|
|
940
|
+
workspace: true
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
if (!link) {
|
|
944
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Link not found", 404);
|
|
945
|
+
}
|
|
946
|
+
const linkPath = join4(link.workspace.path, link.skill.name);
|
|
947
|
+
try {
|
|
948
|
+
if (existsSync4(linkPath)) {
|
|
949
|
+
await rm2(linkPath, { recursive: true, force: true });
|
|
950
|
+
}
|
|
951
|
+
await prisma.skillWorkspace.delete({
|
|
952
|
+
where: { id: link.id }
|
|
953
|
+
});
|
|
954
|
+
return { success: true, message: "Skill unlinked successfully" };
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.error("Unlink error:", error);
|
|
957
|
+
throw new AppError(
|
|
958
|
+
ErrorCode.FILE_SYSTEM_ERROR,
|
|
959
|
+
`Failed to unlink skill: ${error.message}`,
|
|
960
|
+
500,
|
|
961
|
+
error
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Detect existing skills in a workspace skills directory
|
|
967
|
+
* Returns list of skill names that could be migrated
|
|
968
|
+
*/
|
|
969
|
+
async detectExistingSkills(skillsPath) {
|
|
970
|
+
if (!existsSync4(skillsPath)) {
|
|
971
|
+
return [];
|
|
972
|
+
}
|
|
973
|
+
const { readdir: readdir2, stat: stat2, readFile: readFile3 } = await import("fs/promises");
|
|
974
|
+
const items = await readdir2(skillsPath);
|
|
975
|
+
const existingSkills = [];
|
|
976
|
+
for (const item of items) {
|
|
977
|
+
if (item.startsWith(".")) continue;
|
|
978
|
+
const itemPath = join4(skillsPath, item);
|
|
979
|
+
try {
|
|
980
|
+
const itemStat = await stat2(itemPath);
|
|
981
|
+
const lstats = lstatSync(itemPath);
|
|
982
|
+
if (lstats.isSymbolicLink()) continue;
|
|
983
|
+
if (itemStat.isDirectory()) {
|
|
984
|
+
const hasSkillMd = existsSync4(join4(itemPath, "SKILL.md"));
|
|
985
|
+
const hasSkillJson = existsSync4(join4(itemPath, "skill.json"));
|
|
986
|
+
const hasPackageJson = existsSync4(join4(itemPath, "package.json"));
|
|
987
|
+
if (hasSkillMd || hasSkillJson || hasPackageJson) {
|
|
988
|
+
existingSkills.push({
|
|
989
|
+
name: item,
|
|
990
|
+
hasSkillMd,
|
|
991
|
+
path: itemPath
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
} catch (error) {
|
|
996
|
+
console.warn(`Failed to check ${itemPath}:`, error);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return existingSkills;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Migrate existing skills from workspace to unified storage
|
|
1003
|
+
* 1. Move skills to ~/.skillverse/skills/
|
|
1004
|
+
* 2. Register in database
|
|
1005
|
+
* 3. Create symlinks back to workspace
|
|
1006
|
+
*/
|
|
1007
|
+
async migrateExistingSkills(workspaceId, skillNames) {
|
|
1008
|
+
const workspace = await this.getWorkspaceById(workspaceId);
|
|
1009
|
+
const SKILLVERSE_HOME3 = process.env.SKILLVERSE_HOME || join4(homedir(), ".skillverse");
|
|
1010
|
+
const SKILLS_DIR2 = process.env.SKILLS_DIR || join4(SKILLVERSE_HOME3, "skills");
|
|
1011
|
+
if (!existsSync4(SKILLS_DIR2)) {
|
|
1012
|
+
await mkdir2(SKILLS_DIR2, { recursive: true });
|
|
1013
|
+
}
|
|
1014
|
+
const { rename, readFile: readFile3, cp: cp2 } = await import("fs/promises");
|
|
1015
|
+
const migrated = [];
|
|
1016
|
+
const errors = [];
|
|
1017
|
+
for (const skillName of skillNames) {
|
|
1018
|
+
const sourcePath = join4(workspace.path, skillName);
|
|
1019
|
+
const targetPath = join4(SKILLS_DIR2, skillName);
|
|
1020
|
+
try {
|
|
1021
|
+
if (!existsSync4(sourcePath)) {
|
|
1022
|
+
errors.push(`${skillName}: Source path not found`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
const lstats = lstatSync(sourcePath);
|
|
1026
|
+
if (lstats.isSymbolicLink()) {
|
|
1027
|
+
errors.push(`${skillName}: Already a symlink, skipping`);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (existsSync4(targetPath)) {
|
|
1031
|
+
errors.push(`${skillName}: Already exists in unified storage`);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const existingSkill = await prisma.skill.findUnique({
|
|
1035
|
+
where: { name: skillName }
|
|
1036
|
+
});
|
|
1037
|
+
if (existingSkill) {
|
|
1038
|
+
errors.push(`${skillName}: Already registered in database`);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const skillMdPath = join4(sourcePath, "SKILL.md");
|
|
1042
|
+
let description = "";
|
|
1043
|
+
let metadata = {};
|
|
1044
|
+
if (existsSync4(skillMdPath)) {
|
|
1045
|
+
try {
|
|
1046
|
+
const matter2 = await import("gray-matter");
|
|
1047
|
+
const fileContent = await readFile3(skillMdPath, "utf-8");
|
|
1048
|
+
const parsed = matter2.default(fileContent);
|
|
1049
|
+
description = parsed.data.description || "";
|
|
1050
|
+
metadata = parsed.data;
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
console.warn(`Failed to parse SKILL.md for ${skillName}:`, e);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
await cp2(sourcePath, targetPath, { recursive: true });
|
|
1056
|
+
await rm2(sourcePath, { recursive: true, force: true });
|
|
1057
|
+
const skill = await prisma.skill.create({
|
|
1058
|
+
data: {
|
|
1059
|
+
name: skillName,
|
|
1060
|
+
source: "local",
|
|
1061
|
+
description,
|
|
1062
|
+
storagePath: targetPath,
|
|
1063
|
+
metadata: JSON.stringify(metadata)
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
await symlink(targetPath, sourcePath, "dir");
|
|
1067
|
+
await prisma.skillWorkspace.create({
|
|
1068
|
+
data: {
|
|
1069
|
+
skillId: skill.id,
|
|
1070
|
+
workspaceId: workspace.id
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
migrated.push(skillName);
|
|
1074
|
+
console.log(`\u2705 Migrated skill "${skillName}" to unified storage`);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
console.error(`Failed to migrate ${skillName}:`, error);
|
|
1077
|
+
errors.push(`${skillName}: ${error.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
success: errors.length === 0,
|
|
1082
|
+
migrated,
|
|
1083
|
+
errors
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
workspaceService = new WorkspaceService();
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// src/routes/skills.ts
|
|
1092
|
+
import { Router } from "express";
|
|
1093
|
+
import multer from "multer";
|
|
1094
|
+
import { join as join5 } from "path";
|
|
1095
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1096
|
+
import { mkdir as mkdir3, rm as rm3 } from "fs/promises";
|
|
1097
|
+
var router, TEMP_DIR2, storage, upload, skills_default;
|
|
1098
|
+
var init_skills = __esm({
|
|
1099
|
+
"src/routes/skills.ts"() {
|
|
1100
|
+
"use strict";
|
|
1101
|
+
init_skillService();
|
|
1102
|
+
init_workspaceService();
|
|
1103
|
+
router = Router();
|
|
1104
|
+
TEMP_DIR2 = process.env.TEMP_DIR || join5(process.env.HOME || "", ".skillverse", "temp");
|
|
1105
|
+
if (!existsSync5(TEMP_DIR2)) {
|
|
1106
|
+
mkdir3(TEMP_DIR2, { recursive: true });
|
|
1107
|
+
}
|
|
1108
|
+
storage = multer.diskStorage({
|
|
1109
|
+
destination: (req, file, cb) => {
|
|
1110
|
+
cb(null, TEMP_DIR2);
|
|
1111
|
+
},
|
|
1112
|
+
filename: (req, file, cb) => {
|
|
1113
|
+
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
1114
|
+
cb(null, uniqueSuffix + "-" + file.originalname);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
upload = multer({
|
|
1118
|
+
storage,
|
|
1119
|
+
limits: {
|
|
1120
|
+
fileSize: 100 * 1024 * 1024
|
|
1121
|
+
// 100MB limit
|
|
1122
|
+
},
|
|
1123
|
+
fileFilter: (req, file, cb) => {
|
|
1124
|
+
if (file.mimetype === "application/zip" || file.originalname.endsWith(".zip")) {
|
|
1125
|
+
cb(null, true);
|
|
1126
|
+
} else {
|
|
1127
|
+
cb(new Error("Only ZIP files are allowed"));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
router.get("/", async (req, res, next) => {
|
|
1132
|
+
try {
|
|
1133
|
+
const skills = await skillService.getAllSkills();
|
|
1134
|
+
res.json({
|
|
1135
|
+
success: true,
|
|
1136
|
+
data: skills
|
|
1137
|
+
});
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
next(error);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
router.get("/:id", async (req, res, next) => {
|
|
1143
|
+
try {
|
|
1144
|
+
const skill = await skillService.getSkillById(req.params.id);
|
|
1145
|
+
res.json({
|
|
1146
|
+
success: true,
|
|
1147
|
+
data: skill
|
|
1148
|
+
});
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
next(error);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
router.post("/from-git", async (req, res, next) => {
|
|
1154
|
+
try {
|
|
1155
|
+
const { gitUrl, description } = req.body;
|
|
1156
|
+
if (!gitUrl) {
|
|
1157
|
+
return res.status(400).json({
|
|
1158
|
+
success: false,
|
|
1159
|
+
error: "gitUrl is required"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
const skill = await skillService.createSkillFromGit(gitUrl, description);
|
|
1163
|
+
res.status(201).json({
|
|
1164
|
+
success: true,
|
|
1165
|
+
data: skill,
|
|
1166
|
+
message: "Skill created successfully from Git repository"
|
|
1167
|
+
});
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
next(error);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
router.post(
|
|
1173
|
+
"/from-local",
|
|
1174
|
+
upload.single("file"),
|
|
1175
|
+
async (req, res, next) => {
|
|
1176
|
+
try {
|
|
1177
|
+
const { name, description } = req.body;
|
|
1178
|
+
const file = req.file;
|
|
1179
|
+
if (!name) {
|
|
1180
|
+
if (file) {
|
|
1181
|
+
await rm3(file.path, { force: true });
|
|
1182
|
+
}
|
|
1183
|
+
return res.status(400).json({
|
|
1184
|
+
success: false,
|
|
1185
|
+
error: "name is required"
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (!file) {
|
|
1189
|
+
return res.status(400).json({
|
|
1190
|
+
success: false,
|
|
1191
|
+
error: "ZIP file is required"
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
const skill = await skillService.createSkillFromLocal(name, file.path, description);
|
|
1195
|
+
await rm3(file.path, { force: true });
|
|
1196
|
+
res.status(201).json({
|
|
1197
|
+
success: true,
|
|
1198
|
+
data: skill,
|
|
1199
|
+
message: "Skill created successfully from local file"
|
|
1200
|
+
});
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
if (req.file) {
|
|
1203
|
+
await rm3(req.file.path, { force: true }).catch(() => {
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
next(error);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
);
|
|
1210
|
+
router.put("/:id", async (req, res, next) => {
|
|
1211
|
+
try {
|
|
1212
|
+
const { name, description, metadata } = req.body;
|
|
1213
|
+
const skill = await skillService.updateSkill(req.params.id, {
|
|
1214
|
+
name,
|
|
1215
|
+
description,
|
|
1216
|
+
metadata
|
|
1217
|
+
});
|
|
1218
|
+
res.json({
|
|
1219
|
+
success: true,
|
|
1220
|
+
data: skill,
|
|
1221
|
+
message: "Skill updated successfully"
|
|
1222
|
+
});
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
next(error);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
router.delete("/:id", async (req, res, next) => {
|
|
1228
|
+
try {
|
|
1229
|
+
const removeFiles = req.query.removeFiles !== "false";
|
|
1230
|
+
const result = await skillService.deleteSkill(req.params.id, removeFiles);
|
|
1231
|
+
res.json(result);
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
next(error);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
router.post("/:id/link", async (req, res, next) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const { workspaceId } = req.body;
|
|
1239
|
+
if (!workspaceId) {
|
|
1240
|
+
return res.status(400).json({
|
|
1241
|
+
success: false,
|
|
1242
|
+
error: "workspaceId is required"
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const link = await workspaceService.linkSkillToWorkspace(req.params.id, workspaceId);
|
|
1246
|
+
res.status(201).json({
|
|
1247
|
+
success: true,
|
|
1248
|
+
data: link,
|
|
1249
|
+
message: "Skill linked to workspace successfully"
|
|
1250
|
+
});
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
next(error);
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
router.delete(
|
|
1256
|
+
"/:id/unlink/:workspaceId",
|
|
1257
|
+
async (req, res, next) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const result = await workspaceService.unlinkSkillFromWorkspace(
|
|
1260
|
+
req.params.id,
|
|
1261
|
+
req.params.workspaceId
|
|
1262
|
+
);
|
|
1263
|
+
res.json(result);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
next(error);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
router.get("/:id/check-update", async (req, res, next) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const result = await skillService.checkForUpdate(req.params.id);
|
|
1272
|
+
res.json({
|
|
1273
|
+
success: true,
|
|
1274
|
+
data: result
|
|
1275
|
+
});
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
next(error);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
router.post("/:id/upgrade", async (req, res, next) => {
|
|
1281
|
+
try {
|
|
1282
|
+
const skill = await skillService.upgradeSkill(req.params.id);
|
|
1283
|
+
res.json({
|
|
1284
|
+
success: true,
|
|
1285
|
+
data: skill,
|
|
1286
|
+
message: "Skill upgraded successfully"
|
|
1287
|
+
});
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
next(error);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
router.post("/check-updates", async (req, res, next) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const { ids } = req.body;
|
|
1295
|
+
const results = await skillService.checkUpdates(ids);
|
|
1296
|
+
res.json({
|
|
1297
|
+
success: true,
|
|
1298
|
+
data: results
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
next(error);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
router.post("/:id/refresh-metadata", async (req, res, next) => {
|
|
1305
|
+
try {
|
|
1306
|
+
const skill = await skillService.refreshMetadata(req.params.id);
|
|
1307
|
+
res.json({
|
|
1308
|
+
success: true,
|
|
1309
|
+
data: skill,
|
|
1310
|
+
message: "Metadata refreshed successfully"
|
|
1311
|
+
});
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
next(error);
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
router.get("/:id/skill-md", async (req, res, next) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
1319
|
+
const skill = await skillService.getSkillById(req.params.id);
|
|
1320
|
+
const skillMdPath = join5(skill.storagePath, "SKILL.md");
|
|
1321
|
+
if (!existsSync5(skillMdPath)) {
|
|
1322
|
+
return res.json({
|
|
1323
|
+
success: true,
|
|
1324
|
+
data: {
|
|
1325
|
+
exists: false,
|
|
1326
|
+
content: null
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
const content = await readFile3(skillMdPath, "utf-8");
|
|
1331
|
+
res.json({
|
|
1332
|
+
success: true,
|
|
1333
|
+
data: {
|
|
1334
|
+
exists: true,
|
|
1335
|
+
content
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
next(error);
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
skills_default = router;
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
// src/routes/workspaces.ts
|
|
1347
|
+
import { Router as Router2 } from "express";
|
|
1348
|
+
var router2, workspaces_default;
|
|
1349
|
+
var init_workspaces = __esm({
|
|
1350
|
+
"src/routes/workspaces.ts"() {
|
|
1351
|
+
"use strict";
|
|
1352
|
+
init_workspaceService();
|
|
1353
|
+
router2 = Router2();
|
|
1354
|
+
router2.get("/", async (req, res, next) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const workspaces = await workspaceService.getAllWorkspaces();
|
|
1357
|
+
res.json({
|
|
1358
|
+
success: true,
|
|
1359
|
+
data: workspaces
|
|
1360
|
+
});
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
next(error);
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
router2.get("/:id", async (req, res, next) => {
|
|
1366
|
+
try {
|
|
1367
|
+
const workspace = await workspaceService.getWorkspaceById(req.params.id);
|
|
1368
|
+
res.json({
|
|
1369
|
+
success: true,
|
|
1370
|
+
data: workspace
|
|
1371
|
+
});
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
next(error);
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
router2.post("/", async (req, res, next) => {
|
|
1377
|
+
try {
|
|
1378
|
+
const { name, projectPath, type, scope } = req.body;
|
|
1379
|
+
if (!name || !type || !scope) {
|
|
1380
|
+
return res.status(400).json({
|
|
1381
|
+
success: false,
|
|
1382
|
+
error: "name, type, and scope are required"
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
if (scope === "project" && !projectPath) {
|
|
1386
|
+
return res.status(400).json({
|
|
1387
|
+
success: false,
|
|
1388
|
+
error: "projectPath is required for project scope"
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const validTypes = ["vscode", "cursor", "claude-desktop", "codex", "antigravity", "custom"];
|
|
1392
|
+
if (!validTypes.includes(type)) {
|
|
1393
|
+
return res.status(400).json({
|
|
1394
|
+
success: false,
|
|
1395
|
+
error: `type must be one of: ${validTypes.join(", ")}`
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
const validScopes = ["project", "global"];
|
|
1399
|
+
if (!validScopes.includes(scope)) {
|
|
1400
|
+
return res.status(400).json({
|
|
1401
|
+
success: false,
|
|
1402
|
+
error: `scope must be one of: ${validScopes.join(", ")}`
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const workspace = await workspaceService.createWorkspace(name, projectPath || "", type, scope);
|
|
1406
|
+
res.status(201).json({
|
|
1407
|
+
success: true,
|
|
1408
|
+
data: workspace,
|
|
1409
|
+
message: "Workspace created successfully"
|
|
1410
|
+
});
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
next(error);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
router2.put("/:id", async (req, res, next) => {
|
|
1416
|
+
try {
|
|
1417
|
+
const { name, path, type, scope } = req.body;
|
|
1418
|
+
if (type) {
|
|
1419
|
+
const validTypes = ["vscode", "cursor", "claude-desktop", "codex", "antigravity", "custom"];
|
|
1420
|
+
if (!validTypes.includes(type)) {
|
|
1421
|
+
return res.status(400).json({
|
|
1422
|
+
success: false,
|
|
1423
|
+
error: `type must be one of: ${validTypes.join(", ")}`
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (scope) {
|
|
1428
|
+
const validScopes = ["project", "global"];
|
|
1429
|
+
if (!validScopes.includes(scope)) {
|
|
1430
|
+
return res.status(400).json({
|
|
1431
|
+
success: false,
|
|
1432
|
+
error: `scope must be one of: ${validScopes.join(", ")}`
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
const workspace = await workspaceService.updateWorkspace(req.params.id, {
|
|
1437
|
+
name,
|
|
1438
|
+
path,
|
|
1439
|
+
type,
|
|
1440
|
+
scope
|
|
1441
|
+
});
|
|
1442
|
+
res.json({
|
|
1443
|
+
success: true,
|
|
1444
|
+
data: workspace,
|
|
1445
|
+
message: "Workspace updated successfully"
|
|
1446
|
+
});
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
next(error);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
router2.delete("/:id", async (req, res, next) => {
|
|
1452
|
+
try {
|
|
1453
|
+
const result = await workspaceService.deleteWorkspace(req.params.id);
|
|
1454
|
+
res.json(result);
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
next(error);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
router2.post("/:id/detect-skills", async (req, res, next) => {
|
|
1460
|
+
try {
|
|
1461
|
+
const workspace = await workspaceService.getWorkspaceById(req.params.id);
|
|
1462
|
+
const existingSkills = await workspaceService.detectExistingSkills(workspace.path);
|
|
1463
|
+
res.json({
|
|
1464
|
+
success: true,
|
|
1465
|
+
data: existingSkills
|
|
1466
|
+
});
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
next(error);
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
router2.post("/:id/migrate-skills", async (req, res, next) => {
|
|
1472
|
+
try {
|
|
1473
|
+
const { skillNames } = req.body;
|
|
1474
|
+
if (!skillNames || !Array.isArray(skillNames) || skillNames.length === 0) {
|
|
1475
|
+
return res.status(400).json({
|
|
1476
|
+
success: false,
|
|
1477
|
+
error: "skillNames array is required"
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
const result = await workspaceService.migrateExistingSkills(req.params.id, skillNames);
|
|
1481
|
+
res.json({
|
|
1482
|
+
success: true,
|
|
1483
|
+
data: result
|
|
1484
|
+
});
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
next(error);
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
workspaces_default = router2;
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
// src/services/bundleService.ts
|
|
1494
|
+
var bundleService_exports = {};
|
|
1495
|
+
__export(bundleService_exports, {
|
|
1496
|
+
bundleService: () => bundleService,
|
|
1497
|
+
createBundle: () => createBundle,
|
|
1498
|
+
deleteBundle: () => deleteBundle,
|
|
1499
|
+
extractBundle: () => extractBundle,
|
|
1500
|
+
getBundleSize: () => getBundleSize,
|
|
1501
|
+
listBundles: () => listBundles
|
|
1502
|
+
});
|
|
1503
|
+
import { createWriteStream, existsSync as existsSync6, createReadStream } from "fs";
|
|
1504
|
+
import { mkdir as mkdir4, rm as rm4, readdir, stat } from "fs/promises";
|
|
1505
|
+
import { join as join6 } from "path";
|
|
1506
|
+
import { pipeline } from "stream/promises";
|
|
1507
|
+
import archiver from "archiver";
|
|
1508
|
+
import { createGunzip } from "zlib";
|
|
1509
|
+
import { extract } from "tar";
|
|
1510
|
+
async function ensureBundlesDir() {
|
|
1511
|
+
if (!existsSync6(BUNDLES_DIR)) {
|
|
1512
|
+
await mkdir4(BUNDLES_DIR, { recursive: true });
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async function createBundle(skillPath, skillName) {
|
|
1516
|
+
await ensureBundlesDir();
|
|
1517
|
+
const bundleName = `${skillName}-${Date.now()}.tar.gz`;
|
|
1518
|
+
const bundlePath = join6(BUNDLES_DIR, bundleName);
|
|
1519
|
+
return new Promise((resolve, reject) => {
|
|
1520
|
+
const output = createWriteStream(bundlePath);
|
|
1521
|
+
const archive = archiver("tar", {
|
|
1522
|
+
gzip: true,
|
|
1523
|
+
gzipOptions: { level: 9 }
|
|
1524
|
+
});
|
|
1525
|
+
output.on("close", () => {
|
|
1526
|
+
console.log(`\u{1F4E6} Bundle created: ${bundlePath} (${archive.pointer()} bytes)`);
|
|
1527
|
+
resolve(bundlePath);
|
|
1528
|
+
});
|
|
1529
|
+
archive.on("error", (err) => {
|
|
1530
|
+
reject(err);
|
|
1531
|
+
});
|
|
1532
|
+
archive.pipe(output);
|
|
1533
|
+
archive.directory(skillPath, false);
|
|
1534
|
+
archive.finalize();
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
async function extractBundle(bundlePath, targetDir) {
|
|
1538
|
+
if (!existsSync6(bundlePath)) {
|
|
1539
|
+
throw new Error(`Bundle not found: ${bundlePath}`);
|
|
1540
|
+
}
|
|
1541
|
+
if (!existsSync6(targetDir)) {
|
|
1542
|
+
await mkdir4(targetDir, { recursive: true });
|
|
1543
|
+
}
|
|
1544
|
+
await pipeline(
|
|
1545
|
+
createReadStream(bundlePath),
|
|
1546
|
+
createGunzip(),
|
|
1547
|
+
extract({ cwd: targetDir })
|
|
1548
|
+
);
|
|
1549
|
+
console.log(`\u{1F4C2} Bundle extracted to: ${targetDir}`);
|
|
1550
|
+
}
|
|
1551
|
+
async function deleteBundle(bundlePath) {
|
|
1552
|
+
if (existsSync6(bundlePath)) {
|
|
1553
|
+
await rm4(bundlePath);
|
|
1554
|
+
console.log(`\u{1F5D1}\uFE0F Bundle deleted: ${bundlePath}`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async function getBundleSize(bundlePath) {
|
|
1558
|
+
if (!existsSync6(bundlePath)) {
|
|
1559
|
+
return 0;
|
|
1560
|
+
}
|
|
1561
|
+
const stats = await stat(bundlePath);
|
|
1562
|
+
return stats.size;
|
|
1563
|
+
}
|
|
1564
|
+
async function listBundles() {
|
|
1565
|
+
await ensureBundlesDir();
|
|
1566
|
+
const files = await readdir(BUNDLES_DIR);
|
|
1567
|
+
return files.filter((f) => f.endsWith(".tar.gz")).map((f) => join6(BUNDLES_DIR, f));
|
|
1568
|
+
}
|
|
1569
|
+
var BUNDLES_DIR, bundleService;
|
|
1570
|
+
var init_bundleService = __esm({
|
|
1571
|
+
"src/services/bundleService.ts"() {
|
|
1572
|
+
"use strict";
|
|
1573
|
+
BUNDLES_DIR = join6(
|
|
1574
|
+
process.env.SKILLVERSE_HOME || join6(process.env.HOME || "", ".skillverse"),
|
|
1575
|
+
"bundles"
|
|
1576
|
+
);
|
|
1577
|
+
bundleService = {
|
|
1578
|
+
createBundle,
|
|
1579
|
+
extractBundle,
|
|
1580
|
+
deleteBundle,
|
|
1581
|
+
getBundleSize,
|
|
1582
|
+
listBundles,
|
|
1583
|
+
BUNDLES_DIR
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// src/routes/marketplace.ts
|
|
1589
|
+
import { Router as Router3 } from "express";
|
|
1590
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1591
|
+
var router3, marketplace_default;
|
|
1592
|
+
var init_marketplace = __esm({
|
|
1593
|
+
"src/routes/marketplace.ts"() {
|
|
1594
|
+
"use strict";
|
|
1595
|
+
init_db();
|
|
1596
|
+
init_skillService();
|
|
1597
|
+
init_bundleService();
|
|
1598
|
+
init_errorHandler();
|
|
1599
|
+
init_dist();
|
|
1600
|
+
router3 = Router3();
|
|
1601
|
+
router3.get("/skills", async (req, res, next) => {
|
|
1602
|
+
try {
|
|
1603
|
+
const { search, page = "1", pageSize = "20" } = req.query;
|
|
1604
|
+
const where = {};
|
|
1605
|
+
if (search) {
|
|
1606
|
+
where.skill = {
|
|
1607
|
+
OR: [
|
|
1608
|
+
{ name: { contains: search } },
|
|
1609
|
+
{ description: { contains: search } }
|
|
1610
|
+
]
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
const [items, total] = await Promise.all([
|
|
1614
|
+
prisma.marketplaceSkill.findMany({
|
|
1615
|
+
where,
|
|
1616
|
+
include: {
|
|
1617
|
+
skill: true
|
|
1618
|
+
},
|
|
1619
|
+
orderBy: {
|
|
1620
|
+
downloads: "desc"
|
|
1621
|
+
},
|
|
1622
|
+
skip: (parseInt(page) - 1) * parseInt(pageSize),
|
|
1623
|
+
take: parseInt(pageSize)
|
|
1624
|
+
}),
|
|
1625
|
+
prisma.marketplaceSkill.count({ where })
|
|
1626
|
+
]);
|
|
1627
|
+
res.json({
|
|
1628
|
+
success: true,
|
|
1629
|
+
data: {
|
|
1630
|
+
items,
|
|
1631
|
+
total,
|
|
1632
|
+
page: parseInt(page),
|
|
1633
|
+
pageSize: parseInt(pageSize)
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
next(error);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
router3.get("/skills/:id", async (req, res, next) => {
|
|
1641
|
+
try {
|
|
1642
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
1643
|
+
where: { id: req.params.id },
|
|
1644
|
+
include: {
|
|
1645
|
+
skill: true
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
if (!marketplaceSkill) {
|
|
1649
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
|
|
1650
|
+
}
|
|
1651
|
+
res.json({
|
|
1652
|
+
success: true,
|
|
1653
|
+
data: marketplaceSkill
|
|
1654
|
+
});
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
next(error);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
router3.get("/download/:id", async (req, res, next) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
1662
|
+
where: { id: req.params.id },
|
|
1663
|
+
include: {
|
|
1664
|
+
skill: true
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
if (!marketplaceSkill) {
|
|
1668
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
|
|
1669
|
+
}
|
|
1670
|
+
if (marketplaceSkill.skill.source === "git" && marketplaceSkill.skill.sourceUrl) {
|
|
1671
|
+
return res.json({
|
|
1672
|
+
success: true,
|
|
1673
|
+
data: {
|
|
1674
|
+
type: "git",
|
|
1675
|
+
sourceUrl: marketplaceSkill.skill.sourceUrl
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (marketplaceSkill.bundlePath && existsSync7(marketplaceSkill.bundlePath)) {
|
|
1680
|
+
res.setHeader("Content-Type", "application/gzip");
|
|
1681
|
+
res.setHeader(
|
|
1682
|
+
"Content-Disposition",
|
|
1683
|
+
`attachment; filename="${marketplaceSkill.skill.name}.tar.gz"`
|
|
1684
|
+
);
|
|
1685
|
+
return res.sendFile(marketplaceSkill.bundlePath);
|
|
1686
|
+
}
|
|
1687
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Bundle not available for this skill", 404);
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
next(error);
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
router3.post("/publish", async (req, res, next) => {
|
|
1693
|
+
try {
|
|
1694
|
+
const { skillId, publisherName } = req.body;
|
|
1695
|
+
if (!skillId) {
|
|
1696
|
+
return res.status(400).json({
|
|
1697
|
+
success: false,
|
|
1698
|
+
error: "skillId is required"
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
const skill = await prisma.skill.findUnique({
|
|
1702
|
+
where: { id: skillId }
|
|
1703
|
+
});
|
|
1704
|
+
if (!skill) {
|
|
1705
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
|
|
1706
|
+
}
|
|
1707
|
+
const existingEntry = await prisma.marketplaceSkill.findUnique({
|
|
1708
|
+
where: { skillId }
|
|
1709
|
+
});
|
|
1710
|
+
if (existingEntry) {
|
|
1711
|
+
throw new AppError(ErrorCode.ALREADY_EXISTS, "Skill is already published to marketplace", 409);
|
|
1712
|
+
}
|
|
1713
|
+
let bundlePath = null;
|
|
1714
|
+
let bundleSize = null;
|
|
1715
|
+
if (skill.source === "local" && existsSync7(skill.storagePath)) {
|
|
1716
|
+
bundlePath = await bundleService.createBundle(skill.storagePath, skill.name);
|
|
1717
|
+
bundleSize = await bundleService.getBundleSize(bundlePath);
|
|
1718
|
+
}
|
|
1719
|
+
const marketplaceSkill = await prisma.marketplaceSkill.create({
|
|
1720
|
+
data: {
|
|
1721
|
+
skillId,
|
|
1722
|
+
publisherName: publisherName || "Anonymous",
|
|
1723
|
+
bundlePath,
|
|
1724
|
+
bundleSize
|
|
1725
|
+
},
|
|
1726
|
+
include: {
|
|
1727
|
+
skill: true
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
res.status(201).json({
|
|
1731
|
+
success: true,
|
|
1732
|
+
data: marketplaceSkill,
|
|
1733
|
+
message: "Skill published to marketplace successfully"
|
|
1734
|
+
});
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
next(error);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
router3.post("/install/:id", async (req, res, next) => {
|
|
1740
|
+
try {
|
|
1741
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
1742
|
+
where: { id: req.params.id },
|
|
1743
|
+
include: {
|
|
1744
|
+
skill: true
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
if (!marketplaceSkill) {
|
|
1748
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
|
|
1749
|
+
}
|
|
1750
|
+
const sourceSkill = marketplaceSkill.skill;
|
|
1751
|
+
if (sourceSkill.source === "git" && sourceSkill.sourceUrl) {
|
|
1752
|
+
const newSkill = await skillService.createSkillFromGit(
|
|
1753
|
+
sourceSkill.sourceUrl,
|
|
1754
|
+
sourceSkill.description || void 0
|
|
1755
|
+
);
|
|
1756
|
+
await prisma.marketplaceSkill.update({
|
|
1757
|
+
where: { id: req.params.id },
|
|
1758
|
+
data: { downloads: { increment: 1 } }
|
|
1759
|
+
});
|
|
1760
|
+
return res.status(201).json({
|
|
1761
|
+
success: true,
|
|
1762
|
+
data: newSkill,
|
|
1763
|
+
message: "Skill installed from marketplace successfully"
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
if (sourceSkill.source === "local" && marketplaceSkill.bundlePath && existsSync7(marketplaceSkill.bundlePath)) {
|
|
1767
|
+
const newSkill = await skillService.createSkillFromBundle(
|
|
1768
|
+
marketplaceSkill.bundlePath,
|
|
1769
|
+
sourceSkill.name,
|
|
1770
|
+
sourceSkill.description || void 0
|
|
1771
|
+
);
|
|
1772
|
+
await prisma.marketplaceSkill.update({
|
|
1773
|
+
where: { id: req.params.id },
|
|
1774
|
+
data: { downloads: { increment: 1 } }
|
|
1775
|
+
});
|
|
1776
|
+
return res.status(201).json({
|
|
1777
|
+
success: true,
|
|
1778
|
+
data: newSkill,
|
|
1779
|
+
message: "Skill installed from marketplace successfully"
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
return res.status(400).json({
|
|
1783
|
+
success: false,
|
|
1784
|
+
error: "Cannot install this skill. Bundle not available."
|
|
1785
|
+
});
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
next(error);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
router3.delete("/unpublish/:skillId", async (req, res, next) => {
|
|
1791
|
+
try {
|
|
1792
|
+
const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
|
|
1793
|
+
where: { skillId: req.params.skillId }
|
|
1794
|
+
});
|
|
1795
|
+
if (!marketplaceSkill) {
|
|
1796
|
+
throw new AppError(ErrorCode.NOT_FOUND, "Skill is not published to marketplace", 404);
|
|
1797
|
+
}
|
|
1798
|
+
await prisma.marketplaceSkill.delete({
|
|
1799
|
+
where: { id: marketplaceSkill.id }
|
|
1800
|
+
});
|
|
1801
|
+
res.json({
|
|
1802
|
+
success: true,
|
|
1803
|
+
message: "Skill unpublished from marketplace successfully"
|
|
1804
|
+
});
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
next(error);
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
marketplace_default = router3;
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// src/routes/dashboard.ts
|
|
1814
|
+
import { Router as Router4 } from "express";
|
|
1815
|
+
var router4, dashboard_default;
|
|
1816
|
+
var init_dashboard = __esm({
|
|
1817
|
+
"src/routes/dashboard.ts"() {
|
|
1818
|
+
"use strict";
|
|
1819
|
+
init_db();
|
|
1820
|
+
router4 = Router4();
|
|
1821
|
+
router4.get("/stats", async (req, res, next) => {
|
|
1822
|
+
try {
|
|
1823
|
+
const [totalSkills, totalWorkspaces, totalLinks, marketplaceSkills, recentSkills] = await Promise.all([
|
|
1824
|
+
prisma.skill.count(),
|
|
1825
|
+
prisma.workspace.count(),
|
|
1826
|
+
prisma.skillWorkspace.count(),
|
|
1827
|
+
prisma.marketplaceSkill.count(),
|
|
1828
|
+
prisma.skill.findMany({
|
|
1829
|
+
orderBy: { installDate: "desc" },
|
|
1830
|
+
take: 5,
|
|
1831
|
+
include: {
|
|
1832
|
+
linkedWorkspaces: {
|
|
1833
|
+
include: {
|
|
1834
|
+
workspace: true
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
})
|
|
1839
|
+
]);
|
|
1840
|
+
res.json({
|
|
1841
|
+
success: true,
|
|
1842
|
+
data: {
|
|
1843
|
+
totalSkills,
|
|
1844
|
+
totalWorkspaces,
|
|
1845
|
+
totalLinks,
|
|
1846
|
+
marketplaceSkills,
|
|
1847
|
+
recentSkills
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
next(error);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
router4.get("/activity", async (req, res, next) => {
|
|
1855
|
+
try {
|
|
1856
|
+
const [recentSkills, recentLinks, recentPublished] = await Promise.all([
|
|
1857
|
+
prisma.skill.findMany({
|
|
1858
|
+
orderBy: { createdAt: "desc" },
|
|
1859
|
+
take: 10,
|
|
1860
|
+
select: {
|
|
1861
|
+
id: true,
|
|
1862
|
+
name: true,
|
|
1863
|
+
source: true,
|
|
1864
|
+
createdAt: true
|
|
1865
|
+
}
|
|
1866
|
+
}),
|
|
1867
|
+
prisma.skillWorkspace.findMany({
|
|
1868
|
+
orderBy: { linkedAt: "desc" },
|
|
1869
|
+
take: 10,
|
|
1870
|
+
include: {
|
|
1871
|
+
skill: { select: { name: true } },
|
|
1872
|
+
workspace: { select: { name: true } }
|
|
1873
|
+
}
|
|
1874
|
+
}),
|
|
1875
|
+
prisma.marketplaceSkill.findMany({
|
|
1876
|
+
orderBy: { publishDate: "desc" },
|
|
1877
|
+
take: 10,
|
|
1878
|
+
include: {
|
|
1879
|
+
skill: { select: { name: true } }
|
|
1880
|
+
}
|
|
1881
|
+
})
|
|
1882
|
+
]);
|
|
1883
|
+
const activities = [
|
|
1884
|
+
...recentSkills.map((s) => ({
|
|
1885
|
+
type: "skill_added",
|
|
1886
|
+
date: s.createdAt,
|
|
1887
|
+
data: { skillName: s.name, source: s.source }
|
|
1888
|
+
})),
|
|
1889
|
+
...recentLinks.map((l) => ({
|
|
1890
|
+
type: "skill_linked",
|
|
1891
|
+
date: l.linkedAt,
|
|
1892
|
+
data: { skillName: l.skill.name, workspaceName: l.workspace.name }
|
|
1893
|
+
})),
|
|
1894
|
+
...recentPublished.map((p) => ({
|
|
1895
|
+
type: "skill_published",
|
|
1896
|
+
date: p.publishDate,
|
|
1897
|
+
data: { skillName: p.skill.name }
|
|
1898
|
+
}))
|
|
1899
|
+
].sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
1900
|
+
res.json({
|
|
1901
|
+
success: true,
|
|
1902
|
+
data: activities.slice(0, 20)
|
|
1903
|
+
});
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
next(error);
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
dashboard_default = router4;
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
// src/middleware/logger.ts
|
|
1913
|
+
function requestLogger(req, res, next) {
|
|
1914
|
+
const start = Date.now();
|
|
1915
|
+
res.on("finish", () => {
|
|
1916
|
+
const duration = Date.now() - start;
|
|
1917
|
+
console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
|
|
1918
|
+
});
|
|
1919
|
+
next();
|
|
1920
|
+
}
|
|
1921
|
+
var init_logger = __esm({
|
|
1922
|
+
"src/middleware/logger.ts"() {
|
|
1923
|
+
"use strict";
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// src/index.ts
|
|
1928
|
+
var index_exports = {};
|
|
1929
|
+
__export(index_exports, {
|
|
1930
|
+
startServer: () => startServer
|
|
1931
|
+
});
|
|
1932
|
+
import express from "express";
|
|
1933
|
+
import cors from "cors";
|
|
1934
|
+
import dotenv2 from "dotenv";
|
|
1935
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1936
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
1937
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
1938
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1939
|
+
async function initializeStorage() {
|
|
1940
|
+
const skillverseHome = process.env.SKILLVERSE_HOME || join7(process.env.HOME || "", ".skillverse");
|
|
1941
|
+
const skillsDir = process.env.SKILLS_DIR || join7(skillverseHome, "skills");
|
|
1942
|
+
const marketplaceDir = process.env.MARKETPLACE_DIR || join7(skillverseHome, "marketplace");
|
|
1943
|
+
const dirs = [skillverseHome, skillsDir, marketplaceDir];
|
|
1944
|
+
for (const dir of dirs) {
|
|
1945
|
+
if (!existsSync8(dir)) {
|
|
1946
|
+
await mkdir5(dir, { recursive: true });
|
|
1947
|
+
console.log(`Created directory: ${dir}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
async function startServer(port = parseInt(process.env.PORT || "3001")) {
|
|
1952
|
+
try {
|
|
1953
|
+
await initializeStorage();
|
|
1954
|
+
return new Promise((resolve) => {
|
|
1955
|
+
app.listen(port, () => {
|
|
1956
|
+
console.log(`\u{1F680} SkillVerse server running on http://localhost:${port}`);
|
|
1957
|
+
console.log(`\u{1F4C1} Storage: ${process.env.SKILLVERSE_HOME || join7(process.env.HOME || "", ".skillverse")}`);
|
|
1958
|
+
resolve();
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
console.error("Failed to start server:", error);
|
|
1963
|
+
process.exit(1);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
var __filename3, __dirname3, app, PORT, possiblePublicDirs, publicDir;
|
|
1967
|
+
var init_index = __esm({
|
|
1968
|
+
"src/index.ts"() {
|
|
1969
|
+
"use strict";
|
|
1970
|
+
init_skills();
|
|
1971
|
+
init_workspaces();
|
|
1972
|
+
init_marketplace();
|
|
1973
|
+
init_dashboard();
|
|
1974
|
+
init_errorHandler();
|
|
1975
|
+
init_logger();
|
|
1976
|
+
dotenv2.config();
|
|
1977
|
+
__filename3 = fileURLToPath3(import.meta.url);
|
|
1978
|
+
__dirname3 = dirname3(__filename3);
|
|
1979
|
+
app = express();
|
|
1980
|
+
PORT = process.env.PORT || 3001;
|
|
1981
|
+
app.use(cors());
|
|
1982
|
+
app.use(express.json());
|
|
1983
|
+
app.use(express.urlencoded({ extended: true }));
|
|
1984
|
+
app.use(requestLogger);
|
|
1985
|
+
possiblePublicDirs = [
|
|
1986
|
+
join7(__dirname3, "../public"),
|
|
1987
|
+
// Production: dist/index.js -> public
|
|
1988
|
+
join7(__dirname3, "../../public"),
|
|
1989
|
+
// Dev tsx might resolve here
|
|
1990
|
+
join7(process.cwd(), "public"),
|
|
1991
|
+
// Fallback: relative to cwd
|
|
1992
|
+
join7(process.cwd(), "server/public")
|
|
1993
|
+
// Fallback: from root
|
|
1994
|
+
];
|
|
1995
|
+
publicDir = possiblePublicDirs.find((dir) => existsSync8(dir));
|
|
1996
|
+
if (publicDir) {
|
|
1997
|
+
app.use(express.static(publicDir));
|
|
1998
|
+
console.log(`\u{1F4C2} Serving static files from: ${publicDir}`);
|
|
1999
|
+
} else {
|
|
2000
|
+
console.warn('\u26A0\uFE0F No public directory found. Run "npm run build" in client first.');
|
|
2001
|
+
}
|
|
2002
|
+
app.use("/api/skills", skills_default);
|
|
2003
|
+
app.use("/api/workspaces", workspaces_default);
|
|
2004
|
+
app.use("/api/marketplace", marketplace_default);
|
|
2005
|
+
app.use("/api/dashboard", dashboard_default);
|
|
2006
|
+
app.get("/health", (req, res) => {
|
|
2007
|
+
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2008
|
+
});
|
|
2009
|
+
if (publicDir && existsSync8(publicDir)) {
|
|
2010
|
+
app.get("*", (req, res) => {
|
|
2011
|
+
res.sendFile(join7(publicDir, "index.html"));
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
app.use(errorHandler);
|
|
2015
|
+
if (process.argv[1] === fileURLToPath3(import.meta.url)) {
|
|
2016
|
+
startServer();
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// src/bin.ts
|
|
2022
|
+
init_config();
|
|
2023
|
+
import { program } from "commander";
|
|
2024
|
+
import open from "open";
|
|
2025
|
+
import { existsSync as existsSync9, readFileSync, writeFileSync } from "fs";
|
|
2026
|
+
import { join as join8 } from "path";
|
|
2027
|
+
import readline from "readline";
|
|
2028
|
+
|
|
2029
|
+
// src/lib/initDb.ts
|
|
2030
|
+
init_db();
|
|
2031
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2032
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
2033
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2034
|
+
import { spawnSync } from "child_process";
|
|
2035
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2036
|
+
var __dirname2 = dirname2(__filename2);
|
|
2037
|
+
function getSchemaPath() {
|
|
2038
|
+
const possiblePaths = [
|
|
2039
|
+
join2(__dirname2, "../../prisma/schema.prisma"),
|
|
2040
|
+
// From dist/lib or src/lib
|
|
2041
|
+
join2(__dirname2, "../../../prisma/schema.prisma"),
|
|
2042
|
+
join2(process.cwd(), "prisma/schema.prisma"),
|
|
2043
|
+
// Fallback
|
|
2044
|
+
join2(process.cwd(), "server/prisma/schema.prisma")
|
|
2045
|
+
];
|
|
2046
|
+
return possiblePaths.find((p) => existsSync2(p)) || null;
|
|
2047
|
+
}
|
|
2048
|
+
async function ensureDatabaseInitialized() {
|
|
2049
|
+
try {
|
|
2050
|
+
try {
|
|
2051
|
+
await prisma.skill.count();
|
|
2052
|
+
return;
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
if (error.code === "P2021" || error.message.includes("table") || error.message.includes("does not exist")) {
|
|
2055
|
+
console.log("\u{1F4E6} Initializing database...");
|
|
2056
|
+
await initializeDatabase();
|
|
2057
|
+
} else {
|
|
2058
|
+
throw error;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
throw error;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
async function initializeDatabase() {
|
|
2066
|
+
const schemaPath = getSchemaPath();
|
|
2067
|
+
if (!schemaPath) {
|
|
2068
|
+
throw new Error("Could not find schema.prisma");
|
|
2069
|
+
}
|
|
2070
|
+
const possiblePrismaBins = [
|
|
2071
|
+
join2(__dirname2, "../../node_modules/.bin/prisma"),
|
|
2072
|
+
join2(__dirname2, "../../../node_modules/.bin/prisma"),
|
|
2073
|
+
join2(process.cwd(), "node_modules/.bin/prisma")
|
|
2074
|
+
];
|
|
2075
|
+
const prismaBin = possiblePrismaBins.find((p) => existsSync2(p)) || "prisma";
|
|
2076
|
+
console.log(` Using schema: ${schemaPath}`);
|
|
2077
|
+
const result = spawnSync(prismaBin, ["db", "push", "--schema", schemaPath], {
|
|
2078
|
+
stdio: "inherit",
|
|
2079
|
+
env: { ...process.env }
|
|
2080
|
+
// Ensure env vars (DATABASE_URL) are passed
|
|
2081
|
+
});
|
|
2082
|
+
if (result.error) {
|
|
2083
|
+
throw result.error;
|
|
2084
|
+
}
|
|
2085
|
+
if (result.status !== 0) {
|
|
2086
|
+
throw new Error(`Database initialization failed with status ${result.status}`);
|
|
2087
|
+
}
|
|
2088
|
+
console.log("\u2705 Database initialized successfully");
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// src/bin.ts
|
|
2092
|
+
init_skillService();
|
|
2093
|
+
init_workspaceService();
|
|
2094
|
+
var { skillverseHome: SKILLVERSE_HOME2 } = config;
|
|
2095
|
+
var AUTH_FILE = join8(SKILLVERSE_HOME2, "auth.json");
|
|
2096
|
+
var CONFIG_FILE = join8(SKILLVERSE_HOME2, "config.json");
|
|
2097
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
2098
|
+
if (["list", "start", "install", "add", "remove", "import", "search"].includes(actionCommand.name())) {
|
|
2099
|
+
await ensureDatabaseInitialized();
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
function getRegistryUrl() {
|
|
2103
|
+
if (existsSync9(CONFIG_FILE)) {
|
|
2104
|
+
const config2 = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
2105
|
+
return config2.registryUrl || "http://localhost:4000";
|
|
2106
|
+
}
|
|
2107
|
+
return process.env.SKILLVERSE_REGISTRY || "http://localhost:4000";
|
|
2108
|
+
}
|
|
2109
|
+
function getAuthToken() {
|
|
2110
|
+
if (existsSync9(AUTH_FILE)) {
|
|
2111
|
+
const auth = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
|
2112
|
+
return auth.token || null;
|
|
2113
|
+
}
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
function saveAuth(token, user) {
|
|
2117
|
+
writeFileSync(AUTH_FILE, JSON.stringify({ token, user }, null, 2));
|
|
2118
|
+
}
|
|
2119
|
+
async function prompt(question, hidden = false) {
|
|
2120
|
+
const rl = readline.createInterface({
|
|
2121
|
+
input: process.stdin,
|
|
2122
|
+
output: process.stdout
|
|
2123
|
+
});
|
|
2124
|
+
return new Promise((resolve) => {
|
|
2125
|
+
if (hidden) {
|
|
2126
|
+
process.stdout.write(question);
|
|
2127
|
+
let input = "";
|
|
2128
|
+
process.stdin.setRawMode(true);
|
|
2129
|
+
process.stdin.resume();
|
|
2130
|
+
process.stdin.on("data", (char) => {
|
|
2131
|
+
const c = char.toString();
|
|
2132
|
+
if (c === "\n" || c === "\r") {
|
|
2133
|
+
process.stdin.setRawMode(false);
|
|
2134
|
+
process.stdin.pause();
|
|
2135
|
+
console.log();
|
|
2136
|
+
rl.close();
|
|
2137
|
+
resolve(input);
|
|
2138
|
+
} else if (c === "") {
|
|
2139
|
+
process.exit();
|
|
2140
|
+
} else if (c === "\x7F") {
|
|
2141
|
+
input = input.slice(0, -1);
|
|
2142
|
+
} else {
|
|
2143
|
+
input += c;
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
} else {
|
|
2147
|
+
rl.question(question, (answer) => {
|
|
2148
|
+
rl.close();
|
|
2149
|
+
resolve(answer);
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
program.name("skillverse").description("SkillVerse - Local-first skill management platform for AI coding assistants").version("0.1.0");
|
|
2155
|
+
program.command("start").description("Start the SkillVerse local server").option("-p, --port <number>", "Port to run server on", "3001").option("--no-open", "Do not open browser on start").action(async (options) => {
|
|
2156
|
+
const port = parseInt(options.port, 10);
|
|
2157
|
+
console.log("");
|
|
2158
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
2159
|
+
console.log(" \u2551 \u{1F680} SkillVerse CLI \u2551");
|
|
2160
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
2161
|
+
console.log("");
|
|
2162
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
2163
|
+
await startServer2(port);
|
|
2164
|
+
if (options.open) {
|
|
2165
|
+
const url = `http://localhost:${port}`;
|
|
2166
|
+
console.log(`
|
|
2167
|
+
\u{1F310} Opening ${url} in your browser...`);
|
|
2168
|
+
open(url).catch((err) => console.error("Failed to open browser:", err));
|
|
2169
|
+
}
|
|
2170
|
+
console.log("\n\u{1F4A1} Press Ctrl+C to stop the server\n");
|
|
2171
|
+
});
|
|
2172
|
+
program.command("login").description("Login to SkillVerse Registry").option("-r, --registry <url>", "Registry URL").action(async (options) => {
|
|
2173
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
2174
|
+
console.log(`
|
|
2175
|
+
\u{1F510} Logging in to ${registryUrl}...
|
|
2176
|
+
`);
|
|
2177
|
+
const username = await prompt("Username: ");
|
|
2178
|
+
const password = await prompt("Password: ", true);
|
|
2179
|
+
try {
|
|
2180
|
+
const response = await fetch(`${registryUrl}/api/auth/login`, {
|
|
2181
|
+
method: "POST",
|
|
2182
|
+
headers: { "Content-Type": "application/json" },
|
|
2183
|
+
body: JSON.stringify({ username, password })
|
|
2184
|
+
});
|
|
2185
|
+
const data = await response.json();
|
|
2186
|
+
if (!response.ok) {
|
|
2187
|
+
console.error(`
|
|
2188
|
+
\u274C Login failed: ${data.error || "Unknown error"}`);
|
|
2189
|
+
process.exit(1);
|
|
2190
|
+
}
|
|
2191
|
+
saveAuth(data.data.token, data.data.user);
|
|
2192
|
+
console.log(`
|
|
2193
|
+
\u2705 Logged in as ${data.data.user.username}`);
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
console.error(`
|
|
2196
|
+
\u274C Failed to connect to registry: ${error.message}`);
|
|
2197
|
+
process.exit(1);
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
program.command("publish [path]").description("Publish a skill to the Registry").option("-n, --name <name>", "Skill name (defaults to directory name)").option("-d, --description <desc>", "Skill description").option("-r, --registry <url>", "Registry URL").action(async (skillPath, options) => {
|
|
2201
|
+
const token = getAuthToken();
|
|
2202
|
+
if (!token) {
|
|
2203
|
+
console.error("\n\u274C Not logged in. Run `skillverse login` first.");
|
|
2204
|
+
process.exit(1);
|
|
2205
|
+
}
|
|
2206
|
+
const targetPath = skillPath || process.cwd();
|
|
2207
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
2208
|
+
if (!existsSync9(targetPath)) {
|
|
2209
|
+
console.error(`
|
|
2210
|
+
\u274C Path not found: ${targetPath}`);
|
|
2211
|
+
process.exit(1);
|
|
2212
|
+
}
|
|
2213
|
+
const skillName = options.name || targetPath.split("/").pop();
|
|
2214
|
+
console.log(`
|
|
2215
|
+
\u{1F4E6} Publishing skill "${skillName}" to ${registryUrl}...`);
|
|
2216
|
+
try {
|
|
2217
|
+
const { bundleService: bundleService2 } = await Promise.resolve().then(() => (init_bundleService(), bundleService_exports));
|
|
2218
|
+
const bundlePath = await bundleService2.createBundle(targetPath, skillName);
|
|
2219
|
+
const FormData = (await import("form-data")).default;
|
|
2220
|
+
const form = new FormData();
|
|
2221
|
+
form.append("name", skillName);
|
|
2222
|
+
if (options.description) form.append("description", options.description);
|
|
2223
|
+
form.append("bundle", readFileSync(bundlePath), {
|
|
2224
|
+
filename: `${skillName}.tar.gz`,
|
|
2225
|
+
contentType: "application/gzip"
|
|
2226
|
+
});
|
|
2227
|
+
const response = await fetch(`${registryUrl}/api/skills/publish`, {
|
|
2228
|
+
method: "POST",
|
|
2229
|
+
headers: {
|
|
2230
|
+
"Authorization": `Bearer ${token}`,
|
|
2231
|
+
...form.getHeaders()
|
|
2232
|
+
},
|
|
2233
|
+
body: form
|
|
2234
|
+
});
|
|
2235
|
+
const data = await response.json();
|
|
2236
|
+
if (!response.ok) {
|
|
2237
|
+
console.error(`
|
|
2238
|
+
\u274C Publish failed: ${data.error || "Unknown error"}`);
|
|
2239
|
+
process.exit(1);
|
|
2240
|
+
}
|
|
2241
|
+
console.log(`
|
|
2242
|
+
\u2705 Published ${skillName}@${data.data.version}`);
|
|
2243
|
+
console.log(` \u{1F4E5} Install: skillverse install ${skillName}`);
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
console.error(`
|
|
2246
|
+
\u274C Publish failed: ${error.message}`);
|
|
2247
|
+
process.exit(1);
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
program.command("search <query>").description("Search for skills in the Registry").option("-r, --registry <url>", "Registry URL").action(async (query, options) => {
|
|
2251
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
2252
|
+
console.log(`
|
|
2253
|
+
\u{1F50D} Searching for "${query}"...
|
|
2254
|
+
`);
|
|
2255
|
+
try {
|
|
2256
|
+
const response = await fetch(`${registryUrl}/api/skills?search=${encodeURIComponent(query)}`);
|
|
2257
|
+
const data = await response.json();
|
|
2258
|
+
if (!response.ok) {
|
|
2259
|
+
console.error(`\u274C Search failed: ${data.error || "Unknown error"}`);
|
|
2260
|
+
process.exit(1);
|
|
2261
|
+
}
|
|
2262
|
+
if (data.data.items.length === 0) {
|
|
2263
|
+
console.log("No skills found.");
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
console.log(`Found ${data.data.total} skill(s):
|
|
2267
|
+
`);
|
|
2268
|
+
for (const skill of data.data.items) {
|
|
2269
|
+
console.log(` \u{1F4E6} ${skill.name}@${skill.version}`);
|
|
2270
|
+
console.log(` ${skill.description || "No description"}`);
|
|
2271
|
+
console.log(` by ${skill.publisher?.displayName || skill.publisher?.username}`);
|
|
2272
|
+
console.log(` \u{1F4E5} ${skill.downloads} downloads
|
|
2273
|
+
`);
|
|
2274
|
+
}
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
console.error(`
|
|
2277
|
+
\u274C Failed to search: ${error.message}`);
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
program.command("config").description("Configure SkillVerse settings").option("-r, --registry <url>", "Set default registry URL").action((options) => {
|
|
2282
|
+
const config2 = existsSync9(CONFIG_FILE) ? JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) : {};
|
|
2283
|
+
if (options.registry) {
|
|
2284
|
+
config2.registryUrl = options.registry;
|
|
2285
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2));
|
|
2286
|
+
console.log(`\u2705 Registry URL set to: ${options.registry}`);
|
|
2287
|
+
} else {
|
|
2288
|
+
console.log("\nCurrent Configuration:");
|
|
2289
|
+
console.log(` Registry: ${config2.registryUrl || getRegistryUrl()}`);
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2292
|
+
program.command("install [gitUrl]").description("Install a skill from Git URL").option("-a, --agent <agent>", "Link to agent workspace (vscode, cursor, etc.)").action(async (gitUrl, options) => {
|
|
2293
|
+
if (!gitUrl) {
|
|
2294
|
+
console.error("\u274C Git URL is required (Registry install not yet supported via CLI)");
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
console.log(`
|
|
2299
|
+
\u2B07\uFE0F Installing skill from ${gitUrl}...`);
|
|
2300
|
+
const skill = await skillService.createSkillFromGit(gitUrl);
|
|
2301
|
+
console.log(`\u2705 Installed skill "${skill.name}"`);
|
|
2302
|
+
if (options.agent) {
|
|
2303
|
+
const projectPath = process.cwd();
|
|
2304
|
+
const type = options.agent;
|
|
2305
|
+
const validAgents = ["vscode", "cursor", "claude-desktop", "codex", "antigravity", "custom"];
|
|
2306
|
+
if (!validAgents.includes(type)) {
|
|
2307
|
+
console.error(`\u274C Invalid agent type. Valid types: ${validAgents.join(", ")}`);
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
console.log(`
|
|
2311
|
+
\u{1F517} Linking to ${type} workspace...`);
|
|
2312
|
+
const skillsPath = workspaceService.getSkillsPath(projectPath, type, "project");
|
|
2313
|
+
const existingWorkspace = await workspaceService.findWorkspaceByPath(skillsPath);
|
|
2314
|
+
let workspaceId = existingWorkspace?.id;
|
|
2315
|
+
if (!workspaceId) {
|
|
2316
|
+
console.log(` Creating new workspace for ${type}...`);
|
|
2317
|
+
const newWorkspace = await workspaceService.createWorkspace(
|
|
2318
|
+
`${type}-workspace`,
|
|
2319
|
+
projectPath,
|
|
2320
|
+
type,
|
|
2321
|
+
"project"
|
|
2322
|
+
);
|
|
2323
|
+
workspaceId = newWorkspace.id;
|
|
2324
|
+
}
|
|
2325
|
+
await workspaceService.linkSkillToWorkspace(skill.id, workspaceId);
|
|
2326
|
+
console.log(`\u2705 Linked "${skill.name}" to ${type} workspace at ${skillsPath}`);
|
|
2327
|
+
}
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
console.error(`
|
|
2330
|
+
\u274C Install failed: ${error.message}`);
|
|
2331
|
+
process.exit(1);
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
program.command("add").description("Add a local skill").requiredOption("-p, --path <path>", "Path to skill directory").option("-a, --agent <agent>", "Link to agent workspace").action(async (options) => {
|
|
2335
|
+
const sourcePath = options.path.startsWith("/") ? options.path : join8(process.cwd(), options.path);
|
|
2336
|
+
if (!existsSync9(sourcePath)) {
|
|
2337
|
+
console.error(`\u274C Source path not found: ${sourcePath}`);
|
|
2338
|
+
process.exit(1);
|
|
2339
|
+
}
|
|
2340
|
+
try {
|
|
2341
|
+
console.log(`
|
|
2342
|
+
\u{1F4E6} Adding skill from ${sourcePath}...`);
|
|
2343
|
+
const skill = await skillService.createSkillFromDirectory(sourcePath);
|
|
2344
|
+
console.log(`\u2705 Added skill "${skill.name}"`);
|
|
2345
|
+
if (options.agent) {
|
|
2346
|
+
const projectPath = process.cwd();
|
|
2347
|
+
const type = options.agent;
|
|
2348
|
+
const validAgents = ["vscode", "cursor", "claude-desktop", "codex", "antigravity", "custom"];
|
|
2349
|
+
if (!validAgents.includes(type)) {
|
|
2350
|
+
console.error(`\u274C Invalid agent type. Valid types: ${validAgents.join(", ")}`);
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
console.log(`
|
|
2354
|
+
\u{1F517} Linking to ${type} workspace...`);
|
|
2355
|
+
const skillsPath = workspaceService.getSkillsPath(projectPath, type, "project");
|
|
2356
|
+
const existingWorkspace = await workspaceService.findWorkspaceByPath(skillsPath);
|
|
2357
|
+
let workspaceId = existingWorkspace?.id;
|
|
2358
|
+
if (!workspaceId) {
|
|
2359
|
+
console.log(` Creating new workspace for ${type}...`);
|
|
2360
|
+
const newWorkspace = await workspaceService.createWorkspace(
|
|
2361
|
+
`${type}-workspace`,
|
|
2362
|
+
projectPath,
|
|
2363
|
+
type,
|
|
2364
|
+
"project"
|
|
2365
|
+
);
|
|
2366
|
+
workspaceId = newWorkspace.id;
|
|
2367
|
+
}
|
|
2368
|
+
await workspaceService.linkSkillToWorkspace(skill.id, workspaceId);
|
|
2369
|
+
console.log(`\u2705 Linked "${skill.name}" to ${type} workspace at ${skillsPath}`);
|
|
2370
|
+
}
|
|
2371
|
+
} catch (error) {
|
|
2372
|
+
console.error(`
|
|
2373
|
+
\u274C Add failed: ${error.message}`);
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
program.command("list").description("List installed skills").action(async () => {
|
|
2378
|
+
try {
|
|
2379
|
+
const skills = await skillService.getAllSkills();
|
|
2380
|
+
if (skills.length === 0) {
|
|
2381
|
+
console.log("\nNo skills installed.");
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
console.log(`
|
|
2385
|
+
\u{1F4E6} Installed Skills (${skills.length}):
|
|
2386
|
+
`);
|
|
2387
|
+
console.log("Name".padEnd(50) + "Source".padEnd(10) + "Linked");
|
|
2388
|
+
console.log("-".repeat(80));
|
|
2389
|
+
for (const skill of skills) {
|
|
2390
|
+
const source = skill.source;
|
|
2391
|
+
const linkedCount = skill.linkedWorkspaces?.length || 0;
|
|
2392
|
+
const updateStatus = skill.updateAvailable ? "*" : "";
|
|
2393
|
+
console.log(
|
|
2394
|
+
`${skill.name}${updateStatus}`.padEnd(50) + source.padEnd(10) + `${linkedCount} workspace(s)`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
console.log("\n(* indicates update available)");
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
console.error(`
|
|
2400
|
+
\u274C List failed: ${error.message}`);
|
|
2401
|
+
process.exit(1);
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
program.command("remove <name>").description("Remove a skill").action(async (name) => {
|
|
2405
|
+
try {
|
|
2406
|
+
const skill = await skillService.getSkillByName(name);
|
|
2407
|
+
console.log(`
|
|
2408
|
+
\u{1F5D1}\uFE0F Removing skill "${name}"...`);
|
|
2409
|
+
await skillService.deleteSkill(skill.id);
|
|
2410
|
+
console.log(`\u2705 Skill "${name}" removed successfully`);
|
|
2411
|
+
} catch (error) {
|
|
2412
|
+
console.error(`
|
|
2413
|
+
\u274C Remove failed: ${error.message}`);
|
|
2414
|
+
process.exit(1);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
2417
|
+
program.parse();
|
|
2418
|
+
if (!process.argv.slice(2).length) {
|
|
2419
|
+
program.parse(["node", "skillverse", "start"]);
|
|
2420
|
+
}
|
|
2421
|
+
//# sourceMappingURL=bin.js.map
|