skillverse 0.1.4 → 0.1.5

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/index.js CHANGED
@@ -1,38 +1,35 @@
1
- // src/index.ts
2
- import express from "express";
3
- import cors from "cors";
4
- import dotenv2 from "dotenv";
5
- import { fileURLToPath as fileURLToPath3 } from "url";
6
- import { dirname as dirname3, join as join7 } from "path";
7
- import { mkdir as mkdir5 } from "fs/promises";
8
- import { existsSync as existsSync8 } from "fs";
9
-
10
- // src/routes/skills.ts
11
- import { Router } from "express";
12
- import multer from "multer";
13
- import { join as join4 } from "path";
14
- import { existsSync as existsSync4 } from "fs";
15
- import { mkdir as mkdir3, rm as rm3 } from "fs/promises";
16
-
17
- // src/services/skillService.ts
18
- import { join as join2, basename } from "path";
19
- import { existsSync as existsSync2 } from "fs";
20
- import { mkdir, rm, cp, readFile, unlink } from "fs/promises";
21
- import matter from "gray-matter";
22
- import simpleGit from "simple-git";
23
- import AdmZip from "adm-zip";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
24
10
 
25
11
  // src/config.ts
26
12
  import { join, dirname } from "path";
27
- import { existsSync, mkdirSync } from "fs";
13
+ import { existsSync, mkdirSync, readFileSync } from "fs";
28
14
  import { fileURLToPath } from "url";
29
15
  import dotenv from "dotenv";
30
- dotenv.config();
31
- var __filename = fileURLToPath(import.meta.url);
32
- var __dirname = dirname(__filename);
33
16
  function setupEnvironment() {
34
17
  const home = process.env.HOME || process.env.USERPROFILE || "";
35
- const skillverseHome = process.env.SKILLVERSE_HOME || join(home, ".skillverse");
18
+ const defaultHome = join(home, ".skillverse");
19
+ const bootstrapConfigPath = join(defaultHome, "config.json");
20
+ let skillverseHome = process.env.SKILLVERSE_HOME || defaultHome;
21
+ if (existsSync(bootstrapConfigPath)) {
22
+ try {
23
+ const config3 = JSON.parse(readFileSync(bootstrapConfigPath, "utf-8"));
24
+ if (config3.skillverseHome) {
25
+ skillverseHome = config3.skillverseHome;
26
+ process.env.SKILLVERSE_HOME = skillverseHome;
27
+ console.log(`Common config found. Using custom home: ${skillverseHome}`);
28
+ }
29
+ } catch (e) {
30
+ console.warn("Failed to read bootstrap config:", e);
31
+ }
32
+ }
36
33
  const dbUrl = process.env.DATABASE_URL;
37
34
  if (!existsSync(skillverseHome)) {
38
35
  try {
@@ -41,73 +38,99 @@ function setupEnvironment() {
41
38
  console.error("Failed to create .skillverse directory:", error);
42
39
  }
43
40
  }
44
- if (!dbUrl) {
41
+ if (!dbUrl || skillverseHome !== defaultHome) {
45
42
  const dbPath = join(skillverseHome, "skillverse.db");
46
43
  process.env.DATABASE_URL = `file:${dbPath}`;
44
+ console.log(`Using database at: ${dbPath}`);
47
45
  }
46
+ process.env.SKILLS_DIR = join(skillverseHome, "skills");
47
+ process.env.MARKETPLACE_DIR = join(skillverseHome, "marketplace");
48
+ process.env.TEMP_DIR = join(skillverseHome, "temp");
48
49
  return {
49
50
  skillverseHome,
50
51
  databaseUrl: process.env.DATABASE_URL
51
52
  };
52
53
  }
53
- var config = setupEnvironment();
54
+ var __filename, __dirname, config;
55
+ var init_config = __esm({
56
+ "src/config.ts"() {
57
+ "use strict";
58
+ dotenv.config();
59
+ __filename = fileURLToPath(import.meta.url);
60
+ __dirname = dirname(__filename);
61
+ config = setupEnvironment();
62
+ }
63
+ });
54
64
 
55
65
  // src/lib/db.ts
56
66
  import { PrismaClient } from "@prisma/client";
57
- var globalForPrisma = globalThis;
58
- var prisma = globalForPrisma.prisma ?? new PrismaClient({
59
- log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"]
67
+ var config2, globalForPrisma, currentDbUrl, prisma;
68
+ var init_db = __esm({
69
+ "src/lib/db.ts"() {
70
+ "use strict";
71
+ init_config();
72
+ config2 = setupEnvironment();
73
+ globalForPrisma = globalThis;
74
+ currentDbUrl = process.env.DATABASE_URL;
75
+ console.log(`\u{1F4CA} DB connection target: ${currentDbUrl}`);
76
+ if (globalForPrisma.prisma && globalForPrisma.lastDbUrl !== currentDbUrl) {
77
+ console.log(`\u{1F504} DATABASE_URL changed from ${globalForPrisma.lastDbUrl} to ${currentDbUrl}, reconnecting...`);
78
+ globalForPrisma.prisma.$disconnect().catch(console.error);
79
+ globalForPrisma.prisma = void 0;
80
+ }
81
+ prisma = globalForPrisma.prisma ?? new PrismaClient({
82
+ log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"]
83
+ });
84
+ globalForPrisma.prisma = prisma;
85
+ globalForPrisma.lastDbUrl = currentDbUrl;
86
+ }
60
87
  });
61
- if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
62
88
 
63
89
  // ../shared/dist/index.js
64
- var WORKSPACE_SKILLS_PATHS = {
65
- vscode: {
66
- project: ".github/skills",
67
- global: "~/.copilot/skills"
68
- },
69
- cursor: {
70
- project: ".cursor/skills",
71
- global: "~/.cursor/skills"
72
- },
73
- "claude-desktop": {
74
- project: ".claude/skills",
75
- global: "~/.claude/skills"
76
- },
77
- codex: {
78
- project: ".codex/skills",
79
- global: "~/.codex/skills"
80
- },
81
- antigravity: {
82
- project: ".agent/skills",
83
- global: "~/.agent/skills"
84
- },
85
- custom: {
86
- project: "skills",
87
- global: ""
90
+ var WORKSPACE_SKILLS_PATHS, ErrorCode;
91
+ var init_dist = __esm({
92
+ "../shared/dist/index.js"() {
93
+ "use strict";
94
+ WORKSPACE_SKILLS_PATHS = {
95
+ vscode: {
96
+ project: ".github/skills",
97
+ global: "~/.copilot/skills"
98
+ },
99
+ cursor: {
100
+ project: ".cursor/skills",
101
+ global: "~/.cursor/skills"
102
+ },
103
+ "claude-desktop": {
104
+ project: ".claude/skills",
105
+ global: "~/.claude/skills"
106
+ },
107
+ codex: {
108
+ project: ".codex/skills",
109
+ global: "~/.codex/skills"
110
+ },
111
+ antigravity: {
112
+ project: ".agent/skills",
113
+ global: "~/.agent/skills"
114
+ },
115
+ custom: {
116
+ project: "skills",
117
+ global: ""
118
+ }
119
+ };
120
+ ErrorCode = {
121
+ VALIDATION_ERROR: "VALIDATION_ERROR",
122
+ NOT_FOUND: "NOT_FOUND",
123
+ ALREADY_EXISTS: "ALREADY_EXISTS",
124
+ INTERNAL_ERROR: "INTERNAL_ERROR",
125
+ GIT_ERROR: "GIT_ERROR",
126
+ FILE_SYSTEM_ERROR: "FILE_SYSTEM_ERROR",
127
+ PERMISSION_ERROR: "PERMISSION_ERROR",
128
+ SYMLINK_ERROR: "SYMLINK_ERROR"
129
+ };
88
130
  }
89
- };
90
- var ErrorCode = {
91
- VALIDATION_ERROR: "VALIDATION_ERROR",
92
- NOT_FOUND: "NOT_FOUND",
93
- ALREADY_EXISTS: "ALREADY_EXISTS",
94
- INTERNAL_ERROR: "INTERNAL_ERROR",
95
- GIT_ERROR: "GIT_ERROR",
96
- FILE_SYSTEM_ERROR: "FILE_SYSTEM_ERROR",
97
- PERMISSION_ERROR: "PERMISSION_ERROR",
98
- SYMLINK_ERROR: "SYMLINK_ERROR"
99
- };
131
+ });
100
132
 
101
133
  // src/middleware/errorHandler.ts
102
- var AppError = class extends Error {
103
- constructor(code, message, statusCode = 500, details) {
104
- super(message);
105
- this.code = code;
106
- this.statusCode = statusCode;
107
- this.details = details;
108
- this.name = "AppError";
109
- }
110
- };
111
134
  function errorHandler(err, req, res, next) {
112
135
  console.error("Error:", err);
113
136
  if (err instanceof AppError) {
@@ -125,11 +148,35 @@ function errorHandler(err, req, res, next) {
125
148
  message: process.env.NODE_ENV === "development" ? err.message : void 0
126
149
  });
127
150
  }
151
+ var AppError;
152
+ var init_errorHandler = __esm({
153
+ "src/middleware/errorHandler.ts"() {
154
+ "use strict";
155
+ init_dist();
156
+ AppError = class extends Error {
157
+ constructor(code, message, statusCode = 500, details) {
158
+ super(message);
159
+ this.code = code;
160
+ this.statusCode = statusCode;
161
+ this.details = details;
162
+ this.name = "AppError";
163
+ }
164
+ };
165
+ }
166
+ });
128
167
 
129
168
  // src/services/skillService.ts
130
- var SKILLVERSE_HOME = process.env.SKILLVERSE_HOME || join2(process.env.HOME || "", ".skillverse");
131
- var SKILLS_DIR = process.env.SKILLS_DIR || join2(SKILLVERSE_HOME, "skills");
132
- var TEMP_DIR = process.env.TEMP_DIR || join2(SKILLVERSE_HOME, "temp");
169
+ var skillService_exports = {};
170
+ __export(skillService_exports, {
171
+ SkillService: () => SkillService,
172
+ skillService: () => skillService
173
+ });
174
+ import { join as join2, basename } from "path";
175
+ import { existsSync as existsSync2 } from "fs";
176
+ import { mkdir, rm, cp, readFile, unlink } from "fs/promises";
177
+ import matter from "gray-matter";
178
+ import simpleGit from "simple-git";
179
+ import AdmZip from "adm-zip";
133
180
  function parseGitUrl(url) {
134
181
  if (!url.includes("/tree/")) {
135
182
  const urlParts = url.split("/");
@@ -144,511 +191,621 @@ function parseGitUrl(url) {
144
191
  const skillName = pathParts[pathParts.length - 1];
145
192
  return { repoUrl, subdir, skillName };
146
193
  }
147
- var SkillService = class {
148
- async getAllSkills() {
149
- const skills = await prisma.skill.findMany({
150
- include: {
151
- linkedWorkspaces: {
194
+ var getPaths, SkillService, skillService;
195
+ var init_skillService = __esm({
196
+ "src/services/skillService.ts"() {
197
+ "use strict";
198
+ init_db();
199
+ init_errorHandler();
200
+ init_dist();
201
+ getPaths = () => {
202
+ const home = process.env.SKILLVERSE_HOME || join2(process.env.HOME || "", ".skillverse");
203
+ return {
204
+ home,
205
+ skills: process.env.SKILLS_DIR || join2(home, "skills"),
206
+ temp: process.env.TEMP_DIR || join2(home, "temp")
207
+ };
208
+ };
209
+ SkillService = class {
210
+ async getAllSkills() {
211
+ const skills = await prisma.skill.findMany({
152
212
  include: {
153
- workspace: true
213
+ linkedWorkspaces: {
214
+ include: {
215
+ workspace: true
216
+ }
217
+ },
218
+ marketplaceEntry: true
219
+ },
220
+ orderBy: {
221
+ installDate: "desc"
154
222
  }
155
- },
156
- marketplaceEntry: true
157
- },
158
- orderBy: {
159
- installDate: "desc"
223
+ });
224
+ return skills;
160
225
  }
161
- });
162
- return skills;
163
- }
164
- async getSkillById(id) {
165
- const skill = await prisma.skill.findUnique({
166
- where: { id },
167
- include: {
168
- linkedWorkspaces: {
226
+ async getSkillById(id) {
227
+ const skill = await prisma.skill.findUnique({
228
+ where: { id },
169
229
  include: {
170
- workspace: true
230
+ linkedWorkspaces: {
231
+ include: {
232
+ workspace: true
233
+ }
234
+ },
235
+ marketplaceEntry: true
171
236
  }
172
- },
173
- marketplaceEntry: true
237
+ });
238
+ if (!skill) {
239
+ throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
240
+ }
241
+ return skill;
174
242
  }
175
- });
176
- if (!skill) {
177
- throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
178
- }
179
- return skill;
180
- }
181
- async getSkillByName(name) {
182
- const skill = await prisma.skill.findUnique({
183
- where: { name },
184
- include: {
185
- linkedWorkspaces: {
243
+ async getSkillByName(name) {
244
+ const skill = await prisma.skill.findUnique({
245
+ where: { name },
186
246
  include: {
187
- workspace: true
247
+ linkedWorkspaces: {
248
+ include: {
249
+ workspace: true
250
+ }
251
+ },
252
+ marketplaceEntry: true
188
253
  }
189
- },
190
- marketplaceEntry: true
191
- }
192
- });
193
- if (!skill) {
194
- throw new AppError(ErrorCode.NOT_FOUND, `Skill "${name}" not found`, 404);
195
- }
196
- return skill;
197
- }
198
- async parseSkillMetadata(skillPath) {
199
- try {
200
- const skillMdPath = join2(skillPath, "SKILL.md");
201
- let description = "";
202
- let metadata = {};
203
- if (existsSync2(skillMdPath)) {
204
- const fileContent = await readFile(skillMdPath, "utf-8");
205
- const parsed = matter(fileContent);
206
- description = parsed.data.description || "";
207
- metadata = parsed.data;
208
- }
209
- if (!description) {
210
- const packageJsonPath = join2(skillPath, "package.json");
211
- const skillJsonPath = join2(skillPath, "skill.json");
212
- if (existsSync2(skillJsonPath)) {
213
- const content = await readFile(skillJsonPath, "utf-8");
214
- const json = JSON.parse(content);
215
- description = json.description || "";
216
- metadata = { ...metadata, ...json };
217
- } else if (existsSync2(packageJsonPath)) {
218
- const content = await readFile(packageJsonPath, "utf-8");
219
- const pkg = JSON.parse(content);
220
- description = pkg.description || "";
221
- metadata = { ...metadata, name: pkg.name, version: pkg.version };
254
+ });
255
+ if (!skill) {
256
+ throw new AppError(ErrorCode.NOT_FOUND, `Skill "${name}" not found`, 404);
222
257
  }
258
+ return skill;
223
259
  }
224
- return { description, metadata };
225
- } catch (error) {
226
- console.warn("Failed to parse skill metadata:", error);
227
- return { description: "", metadata: {} };
228
- }
229
- }
230
- async createSkillFromGit(gitUrl, description) {
231
- let tempPath = null;
232
- try {
233
- const { repoUrl, subdir, skillName } = parseGitUrl(gitUrl);
234
- const existingSkill = await prisma.skill.findUnique({
235
- where: { name: skillName }
236
- });
237
- if (existingSkill) {
238
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
239
- }
240
- const skillPath = join2(SKILLS_DIR, skillName);
241
- if (existsSync2(skillPath)) {
242
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
260
+ async parseSkillMetadata(skillPath) {
261
+ try {
262
+ const skillMdPath = join2(skillPath, "SKILL.md");
263
+ let description = "";
264
+ let metadata = {};
265
+ if (existsSync2(skillMdPath)) {
266
+ const fileContent = await readFile(skillMdPath, "utf-8");
267
+ const parsed = matter(fileContent);
268
+ description = parsed.data.description || "";
269
+ metadata = parsed.data;
270
+ }
271
+ if (!description) {
272
+ const packageJsonPath = join2(skillPath, "package.json");
273
+ const skillJsonPath = join2(skillPath, "skill.json");
274
+ if (existsSync2(skillJsonPath)) {
275
+ const content = await readFile(skillJsonPath, "utf-8");
276
+ const json = JSON.parse(content);
277
+ description = json.description || "";
278
+ metadata = { ...metadata, ...json };
279
+ } else if (existsSync2(packageJsonPath)) {
280
+ const content = await readFile(packageJsonPath, "utf-8");
281
+ const pkg = JSON.parse(content);
282
+ description = pkg.description || "";
283
+ metadata = { ...metadata, name: pkg.name, version: pkg.version };
284
+ }
285
+ }
286
+ return { description, metadata };
287
+ } catch (error) {
288
+ console.warn("Failed to parse skill metadata:", error);
289
+ return { description: "", metadata: {} };
290
+ }
243
291
  }
244
- let commitHash = "";
245
- if (subdir) {
246
- tempPath = join2(TEMP_DIR, `git-clone-${Date.now()}`);
247
- await mkdir(tempPath, { recursive: true });
248
- console.log(`Cloning ${repoUrl} to temp path for extraction...`);
249
- const git = simpleGit();
250
- await git.clone(repoUrl, tempPath);
292
+ async createSkillFromGit(gitUrl, description) {
293
+ let tempPath = null;
251
294
  try {
252
- commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
253
- console.log(`Captured commit hash: ${commitHash}`);
254
- } catch (e) {
255
- console.warn("Failed to capture commit hash:", e);
295
+ const { repoUrl, subdir, skillName } = parseGitUrl(gitUrl);
296
+ const existingSkill = await prisma.skill.findUnique({
297
+ where: { name: skillName }
298
+ });
299
+ if (existingSkill) {
300
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
301
+ }
302
+ const { skills: skillsDir, temp: tempDir } = getPaths();
303
+ const skillPath = join2(skillsDir, skillName);
304
+ if (existsSync2(skillPath)) {
305
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
306
+ }
307
+ let commitHash = "";
308
+ if (subdir) {
309
+ tempPath = join2(tempDir, `git-clone-${Date.now()}`);
310
+ await mkdir(tempPath, { recursive: true });
311
+ console.log(`Cloning ${repoUrl} to temp path for extraction...`);
312
+ const git = simpleGit();
313
+ await git.clone(repoUrl, tempPath);
314
+ try {
315
+ commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
316
+ console.log(`Captured commit hash: ${commitHash}`);
317
+ } catch (e) {
318
+ console.warn("Failed to capture commit hash:", e);
319
+ }
320
+ const sourcePath = join2(tempPath, subdir);
321
+ if (!existsSync2(sourcePath)) {
322
+ throw new AppError(
323
+ ErrorCode.GIT_ERROR,
324
+ `Subdirectory "${subdir}" not found in repository`,
325
+ 400
326
+ );
327
+ }
328
+ const hasSkillMd = existsSync2(join2(sourcePath, "SKILL.md"));
329
+ const hasSkillJson = existsSync2(join2(sourcePath, "skill.json"));
330
+ const hasPackageJson = existsSync2(join2(sourcePath, "package.json"));
331
+ if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
332
+ throw new AppError(
333
+ ErrorCode.VALIDATION_ERROR,
334
+ `Invalid skill structure: "${subdir}" does not contain SKILL.md, skill.json, or package.json`,
335
+ 400
336
+ );
337
+ }
338
+ await mkdir(skillPath, { recursive: true });
339
+ await cp(sourcePath, skillPath, { recursive: true });
340
+ } else {
341
+ if (!existsSync2(skillPath)) {
342
+ await mkdir(skillPath, { recursive: true });
343
+ }
344
+ const git = simpleGit();
345
+ console.log(`Cloning ${gitUrl} to ${skillPath}...`);
346
+ await git.clone(gitUrl, skillPath);
347
+ try {
348
+ commitHash = await git.cwd(skillPath).revparse(["HEAD"]);
349
+ console.log(`Captured commit hash: ${commitHash}`);
350
+ } catch (e) {
351
+ console.warn("Failed to capture commit hash:", e);
352
+ }
353
+ }
354
+ const parsed = await this.parseSkillMetadata(skillPath);
355
+ const skill = await prisma.skill.create({
356
+ data: {
357
+ name: skillName,
358
+ source: "git",
359
+ sourceUrl: gitUrl,
360
+ repoUrl,
361
+ commitHash,
362
+ description: description || parsed.description || "",
363
+ storagePath: skillPath,
364
+ metadata: JSON.stringify(parsed.metadata)
365
+ }
366
+ });
367
+ return skill;
368
+ } catch (error) {
369
+ throw error;
370
+ } finally {
371
+ if (tempPath && existsSync2(tempPath)) {
372
+ await rm(tempPath, { recursive: true, force: true }).catch(console.error);
373
+ }
256
374
  }
257
- const sourcePath = join2(tempPath, subdir);
258
- if (!existsSync2(sourcePath)) {
259
- throw new AppError(
260
- ErrorCode.GIT_ERROR,
261
- `Subdirectory "${subdir}" not found in repository`,
262
- 400
263
- );
375
+ }
376
+ async createSkillFromDirectory(path, name, description) {
377
+ if (!existsSync2(path)) {
378
+ throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, `Source path not found: ${path}`, 400);
264
379
  }
265
- const hasSkillMd = existsSync2(join2(sourcePath, "SKILL.md"));
266
- const hasSkillJson = existsSync2(join2(sourcePath, "skill.json"));
267
- const hasPackageJson = existsSync2(join2(sourcePath, "package.json"));
268
- if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
269
- throw new AppError(
270
- ErrorCode.VALIDATION_ERROR,
271
- `Invalid skill structure: "${subdir}" does not contain SKILL.md, skill.json, or package.json`,
272
- 400
273
- );
380
+ const { skills: skillsDir } = getPaths();
381
+ const skillName = name || basename(path);
382
+ const skillPath = join2(skillsDir, skillName);
383
+ const existingSkill = await prisma.skill.findUnique({
384
+ where: { name: skillName }
385
+ });
386
+ if (existingSkill) {
387
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
274
388
  }
275
- await mkdir(skillPath, { recursive: true });
276
- await cp(sourcePath, skillPath, { recursive: true });
277
- } else {
278
- if (!existsSync2(skillPath)) {
279
- await mkdir(skillPath, { recursive: true });
389
+ if (existsSync2(skillPath)) {
390
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
280
391
  }
281
- const git = simpleGit();
282
- console.log(`Cloning ${gitUrl} to ${skillPath}...`);
283
- await git.clone(gitUrl, skillPath);
284
392
  try {
285
- commitHash = await git.cwd(skillPath).revparse(["HEAD"]);
286
- console.log(`Captured commit hash: ${commitHash}`);
287
- } catch (e) {
288
- console.warn("Failed to capture commit hash:", e);
393
+ await mkdir(skillPath, { recursive: true });
394
+ await cp(path, skillPath, { recursive: true });
395
+ const parsed = await this.parseSkillMetadata(skillPath);
396
+ const skill = await prisma.skill.create({
397
+ data: {
398
+ name: skillName,
399
+ source: "local",
400
+ description: description || parsed.description || "",
401
+ storagePath: skillPath,
402
+ metadata: JSON.stringify(parsed.metadata)
403
+ }
404
+ });
405
+ return skill;
406
+ } catch (error) {
407
+ if (existsSync2(skillPath)) {
408
+ await rm(skillPath, { recursive: true, force: true }).catch(() => {
409
+ });
410
+ }
411
+ throw error;
289
412
  }
290
413
  }
291
- const parsed = await this.parseSkillMetadata(skillPath);
292
- const skill = await prisma.skill.create({
293
- data: {
294
- name: skillName,
295
- source: "git",
296
- sourceUrl: gitUrl,
297
- repoUrl,
298
- commitHash,
299
- description: description || parsed.description || "",
300
- storagePath: skillPath,
301
- metadata: JSON.stringify(parsed.metadata)
414
+ /**
415
+ * Helper to handle cases where the archive contains a single top-level directory.
416
+ * Moves contents up one level if that's the case.
417
+ */
418
+ async stripTopLevelDirectory(targetPath) {
419
+ const { readdir: readdir2, rename, rmdir, stat: stat2 } = await import("fs/promises");
420
+ const items = await readdir2(targetPath);
421
+ const validItems = items.filter((i) => i !== ".DS_Store" && i !== "__MACOSX");
422
+ if (validItems.length === 1) {
423
+ const topLevelItem = validItems[0];
424
+ const topLevelPath = join2(targetPath, topLevelItem);
425
+ const stats = await stat2(topLevelPath);
426
+ if (stats.isDirectory()) {
427
+ console.log(`Striping top-level directory: ${topLevelItem}`);
428
+ const subItems = await readdir2(topLevelPath);
429
+ for (const item of subItems) {
430
+ await rename(join2(topLevelPath, item), join2(targetPath, item));
431
+ }
432
+ await rmdir(topLevelPath);
433
+ }
302
434
  }
303
- });
304
- return skill;
305
- } catch (error) {
306
- throw error;
307
- } finally {
308
- if (tempPath && existsSync2(tempPath)) {
309
- await rm(tempPath, { recursive: true, force: true }).catch(console.error);
310
435
  }
311
- }
312
- }
313
- async createSkillFromDirectory(path, name, description) {
314
- if (!existsSync2(path)) {
315
- throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, `Source path not found: ${path}`, 400);
316
- }
317
- const skillName = name || basename(path);
318
- const skillPath = join2(SKILLS_DIR, skillName);
319
- const existingSkill = await prisma.skill.findUnique({
320
- where: { name: skillName }
321
- });
322
- if (existingSkill) {
323
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${skillName}" already exists`, 409);
324
- }
325
- if (existsSync2(skillPath)) {
326
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${skillName}" already exists`, 409);
327
- }
328
- try {
329
- await mkdir(skillPath, { recursive: true });
330
- await cp(path, skillPath, { recursive: true });
331
- const parsed = await this.parseSkillMetadata(skillPath);
332
- const skill = await prisma.skill.create({
333
- data: {
334
- name: skillName,
335
- source: "local",
336
- description: description || parsed.description || "",
337
- storagePath: skillPath,
338
- metadata: JSON.stringify(parsed.metadata)
339
- }
340
- });
341
- return skill;
342
- } catch (error) {
343
- if (existsSync2(skillPath)) {
344
- await rm(skillPath, { recursive: true, force: true }).catch(() => {
436
+ async createSkillFromLocal(name, zipPath, description) {
437
+ const { skills: skillsDir, temp: tempDir } = getPaths();
438
+ const skillPath = join2(skillsDir, name);
439
+ const existingSkill = await prisma.skill.findUnique({
440
+ where: { name }
345
441
  });
346
- }
347
- throw error;
348
- }
349
- }
350
- /**
351
- * Helper to handle cases where the archive contains a single top-level directory.
352
- * Moves contents up one level if that's the case.
353
- */
354
- async stripTopLevelDirectory(targetPath) {
355
- const { readdir: readdir2, rename, rmdir, stat: stat2 } = await import("fs/promises");
356
- const items = await readdir2(targetPath);
357
- const validItems = items.filter((i) => i !== ".DS_Store" && i !== "__MACOSX");
358
- if (validItems.length === 1) {
359
- const topLevelItem = validItems[0];
360
- const topLevelPath = join2(targetPath, topLevelItem);
361
- const stats = await stat2(topLevelPath);
362
- if (stats.isDirectory()) {
363
- console.log(`Striping top-level directory: ${topLevelItem}`);
364
- const subItems = await readdir2(topLevelPath);
365
- for (const item of subItems) {
366
- await rename(join2(topLevelPath, item), join2(targetPath, item));
442
+ if (existingSkill) {
443
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${name}" already exists`, 409);
444
+ }
445
+ if (existsSync2(skillPath)) {
446
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
447
+ }
448
+ const extractPath = join2(tempDir, `extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
449
+ await mkdir(extractPath, { recursive: true });
450
+ try {
451
+ const zip = new AdmZip(zipPath);
452
+ zip.extractAllTo(extractPath, true);
453
+ await this.stripTopLevelDirectory(extractPath);
454
+ const parsed = await this.parseSkillMetadata(extractPath);
455
+ if (existsSync2(skillPath)) {
456
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
457
+ }
458
+ await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
459
+ const skill = await prisma.skill.create({
460
+ data: {
461
+ name,
462
+ source: "local",
463
+ description: description || parsed.description || "",
464
+ storagePath: skillPath,
465
+ metadata: JSON.stringify(parsed.metadata)
466
+ }
467
+ });
468
+ return skill;
469
+ } catch (error) {
470
+ if (existsSync2(skillPath)) {
471
+ await rm(skillPath, { recursive: true, force: true }).catch(() => {
472
+ });
473
+ }
474
+ if (existsSync2(extractPath)) {
475
+ await rm(extractPath, { recursive: true, force: true }).catch(() => {
476
+ });
477
+ }
478
+ throw error;
367
479
  }
368
- await rmdir(topLevelPath);
369
- }
370
- }
371
- }
372
- async createSkillFromLocal(name, zipPath, description) {
373
- const skillPath = join2(SKILLS_DIR, name);
374
- const existingSkill = await prisma.skill.findUnique({
375
- where: { name }
376
- });
377
- if (existingSkill) {
378
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill "${name}" already exists`, 409);
379
- }
380
- if (existsSync2(skillPath)) {
381
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
382
- }
383
- const extractPath = join2(TEMP_DIR, `extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
384
- await mkdir(extractPath, { recursive: true });
385
- try {
386
- const zip = new AdmZip(zipPath);
387
- zip.extractAllTo(extractPath, true);
388
- await this.stripTopLevelDirectory(extractPath);
389
- const parsed = await this.parseSkillMetadata(extractPath);
390
- if (existsSync2(skillPath)) {
391
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
392
480
  }
393
- await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
394
- const skill = await prisma.skill.create({
395
- data: {
396
- name,
397
- source: "local",
398
- description: description || parsed.description || "",
399
- storagePath: skillPath,
400
- metadata: JSON.stringify(parsed.metadata)
481
+ /**
482
+ * Create a skill from a tar.gz bundle (marketplace install)
483
+ * @param bundlePath - Path to the .tar.gz bundle
484
+ * @param originalName - Original skill name
485
+ * @param description - Optional skill description
486
+ */
487
+ async createSkillFromBundle(bundlePath, originalName, description) {
488
+ let name = originalName;
489
+ let counter = 1;
490
+ while (await prisma.skill.findUnique({ where: { name } })) {
491
+ name = `${originalName}-${counter}`;
492
+ counter++;
401
493
  }
402
- });
403
- return skill;
404
- } catch (error) {
405
- if (existsSync2(skillPath)) {
406
- await rm(skillPath, { recursive: true, force: true }).catch(() => {
494
+ const { skills: skillsDir, temp: tempDir } = getPaths();
495
+ const skillPath = join2(skillsDir, name);
496
+ const extractPath = join2(tempDir, `extract-bundle-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
497
+ await mkdir(extractPath, { recursive: true });
498
+ try {
499
+ const { createReadStream: createReadStream2 } = await import("fs");
500
+ const { createGunzip: createGunzip2 } = await import("zlib");
501
+ const tar = await import("tar");
502
+ const { pipeline: pipeline2 } = await import("stream/promises");
503
+ await pipeline2(
504
+ createReadStream2(bundlePath),
505
+ createGunzip2(),
506
+ tar.extract({ cwd: extractPath })
507
+ );
508
+ await this.stripTopLevelDirectory(extractPath);
509
+ const parsed = await this.parseSkillMetadata(extractPath);
510
+ if (existsSync2(skillPath)) {
511
+ throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
512
+ }
513
+ await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
514
+ const skill = await prisma.skill.create({
515
+ data: {
516
+ name,
517
+ source: "local",
518
+ description: description || parsed.description || "",
519
+ storagePath: skillPath,
520
+ metadata: JSON.stringify(parsed.metadata)
521
+ }
522
+ });
523
+ console.log(`\u{1F4E6} Installed skill "${name}" from bundle`);
524
+ return skill;
525
+ } catch (error) {
526
+ if (existsSync2(skillPath)) {
527
+ await rm(skillPath, { recursive: true, force: true }).catch(() => {
528
+ });
529
+ }
530
+ if (existsSync2(extractPath)) {
531
+ await rm(extractPath, { recursive: true, force: true }).catch(() => {
532
+ });
533
+ }
534
+ throw error;
535
+ }
536
+ }
537
+ async updateSkill(id, data) {
538
+ const skill = await this.getSkillById(id);
539
+ const updatedSkill = await prisma.skill.update({
540
+ where: { id },
541
+ data: {
542
+ ...data.name && { name: data.name },
543
+ ...data.description !== void 0 && { description: data.description },
544
+ ...data.metadata && { metadata: JSON.stringify(data.metadata) }
545
+ }
407
546
  });
547
+ return updatedSkill;
408
548
  }
409
- if (existsSync2(extractPath)) {
410
- await rm(extractPath, { recursive: true, force: true }).catch(() => {
549
+ async deleteSkill(id, removeFiles = true) {
550
+ const skill = await this.getSkillById(id);
551
+ if (skill.linkedWorkspaces && skill.linkedWorkspaces.length > 0) {
552
+ for (const link of skill.linkedWorkspaces) {
553
+ try {
554
+ const workspacePath = link.workspace.path;
555
+ const symlinkPath = join2(workspacePath, skill.name);
556
+ if (existsSync2(symlinkPath)) {
557
+ const stats = await import("fs/promises").then((fs) => fs.lstat(symlinkPath));
558
+ if (stats.isSymbolicLink()) {
559
+ await unlink(symlinkPath);
560
+ console.log(`Removed symlink for ${skill.name} in workspace ${workspacePath}`);
561
+ }
562
+ }
563
+ } catch (error) {
564
+ console.warn(`Failed to remove symlink for ${skill.name} in workspace ${link.workspace.path}:`, error);
565
+ }
566
+ }
567
+ }
568
+ await prisma.skill.delete({
569
+ where: { id }
411
570
  });
571
+ if (removeFiles && skill.storagePath) {
572
+ await rm(skill.storagePath, { recursive: true, force: true }).catch((err) => {
573
+ console.warn(`Failed to delete skill directory ${skill.storagePath}:`, err);
574
+ });
575
+ }
576
+ return { success: true, message: "Skill deleted successfully" };
412
577
  }
413
- throw error;
414
- }
415
- }
416
- /**
417
- * Create a skill from a tar.gz bundle (marketplace install)
418
- * @param bundlePath - Path to the .tar.gz bundle
419
- * @param originalName - Original skill name
420
- * @param description - Optional skill description
421
- */
422
- async createSkillFromBundle(bundlePath, originalName, description) {
423
- let name = originalName;
424
- let counter = 1;
425
- while (await prisma.skill.findUnique({ where: { name } })) {
426
- name = `${originalName}-${counter}`;
427
- counter++;
428
- }
429
- const skillPath = join2(SKILLS_DIR, name);
430
- const extractPath = join2(TEMP_DIR, `extract-bundle-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
431
- await mkdir(extractPath, { recursive: true });
432
- try {
433
- const { createReadStream: createReadStream2 } = await import("fs");
434
- const { createGunzip: createGunzip2 } = await import("zlib");
435
- const tar = await import("tar");
436
- const { pipeline: pipeline2 } = await import("stream/promises");
437
- await pipeline2(
438
- createReadStream2(bundlePath),
439
- createGunzip2(),
440
- tar.extract({ cwd: extractPath })
441
- );
442
- await this.stripTopLevelDirectory(extractPath);
443
- const parsed = await this.parseSkillMetadata(extractPath);
444
- if (existsSync2(skillPath)) {
445
- throw new AppError(ErrorCode.ALREADY_EXISTS, `Skill directory "${name}" already exists`, 409);
578
+ // Check for updates
579
+ async checkForUpdate(id) {
580
+ const skill = await this.getSkillById(id);
581
+ if (skill.source !== "git" || !skill.repoUrl) {
582
+ return {
583
+ hasUpdate: false,
584
+ currentHash: skill.commitHash,
585
+ remoteHash: null,
586
+ message: "Not a git skill or missing repo URL"
587
+ };
588
+ }
589
+ const { temp: tempDir } = getPaths();
590
+ const tempPath = join2(tempDir, `check-update-${Date.now()}`);
591
+ await mkdir(tempPath, { recursive: true });
592
+ try {
593
+ const git = simpleGit();
594
+ console.log(`Checking updates for ${skill.name} from ${skill.repoUrl}...`);
595
+ const remote = await git.listRemote([skill.repoUrl, "HEAD"]);
596
+ if (!remote) {
597
+ throw new Error("Failed to get remote HEAD");
598
+ }
599
+ const remoteHash = remote.split(" ")[0];
600
+ const hasUpdate = remoteHash !== skill.commitHash;
601
+ await prisma.skill.update({
602
+ where: { id },
603
+ data: {
604
+ lastUpdateCheck: /* @__PURE__ */ new Date(),
605
+ updateAvailable: hasUpdate
606
+ }
607
+ });
608
+ return {
609
+ hasUpdate,
610
+ currentHash: skill.commitHash,
611
+ remoteHash
612
+ };
613
+ } catch (error) {
614
+ console.error("Update check error:", error);
615
+ throw new AppError(ErrorCode.GIT_ERROR, `Failed to check for updates: ${error.message}`, 500);
616
+ } finally {
617
+ await rm(tempPath, { recursive: true, force: true }).catch(() => {
618
+ });
619
+ }
446
620
  }
447
- await import("fs/promises").then((fs) => fs.rename(extractPath, skillPath));
448
- const skill = await prisma.skill.create({
449
- data: {
450
- name,
451
- source: "local",
452
- description: description || parsed.description || "",
453
- storagePath: skillPath,
454
- metadata: JSON.stringify(parsed.metadata)
621
+ async checkUpdates(ids) {
622
+ const whereClause = {
623
+ source: "git",
624
+ repoUrl: { not: null }
625
+ };
626
+ if (ids && ids.length > 0) {
627
+ whereClause.id = { in: ids };
455
628
  }
456
- });
457
- console.log(`\u{1F4E6} Installed skill "${name}" from bundle`);
458
- return skill;
459
- } catch (error) {
460
- if (existsSync2(skillPath)) {
461
- await rm(skillPath, { recursive: true, force: true }).catch(() => {
462
- });
629
+ const skills = await prisma.skill.findMany({ where: whereClause });
630
+ const results = {};
631
+ console.log(`Checking updates for ${skills.length} skills...`);
632
+ await Promise.all(skills.map(async (skill) => {
633
+ try {
634
+ const result = await this.checkForUpdate(skill.id);
635
+ results[skill.id] = result;
636
+ } catch (e) {
637
+ results[skill.id] = { error: e.message };
638
+ }
639
+ }));
640
+ return results;
463
641
  }
464
- if (existsSync2(extractPath)) {
465
- await rm(extractPath, { recursive: true, force: true }).catch(() => {
642
+ async refreshMetadata(id) {
643
+ const skill = await this.getSkillById(id);
644
+ const parsed = await this.parseSkillMetadata(skill.storagePath);
645
+ const updated = await prisma.skill.update({
646
+ where: { id },
647
+ data: {
648
+ description: parsed.description,
649
+ metadata: JSON.stringify(parsed.metadata)
650
+ }
466
651
  });
652
+ return updated;
467
653
  }
468
- throw error;
469
- }
470
- }
471
- async updateSkill(id, data) {
472
- const skill = await this.getSkillById(id);
473
- const updatedSkill = await prisma.skill.update({
474
- where: { id },
475
- data: {
476
- ...data.name && { name: data.name },
477
- ...data.description !== void 0 && { description: data.description },
478
- ...data.metadata && { metadata: JSON.stringify(data.metadata) }
479
- }
480
- });
481
- return updatedSkill;
482
- }
483
- async deleteSkill(id, removeFiles = true) {
484
- const skill = await this.getSkillById(id);
485
- if (skill.linkedWorkspaces && skill.linkedWorkspaces.length > 0) {
486
- for (const link of skill.linkedWorkspaces) {
654
+ async upgradeSkill(id) {
655
+ const skill = await this.getSkillById(id);
656
+ if (skill.source !== "git" || !skill.sourceUrl) {
657
+ throw new AppError(ErrorCode.VALIDATION_ERROR, "Cannot upgrade non-git skill", 400);
658
+ }
659
+ let tempPath = null;
660
+ let tempSkillPath = null;
487
661
  try {
488
- const workspacePath = link.workspace.path;
489
- const symlinkPath = join2(workspacePath, skill.name);
490
- if (existsSync2(symlinkPath)) {
491
- const stats = await import("fs/promises").then((fs) => fs.lstat(symlinkPath));
492
- if (stats.isSymbolicLink()) {
493
- await unlink(symlinkPath);
494
- console.log(`Removed symlink for ${skill.name} in workspace ${workspacePath}`);
662
+ const { repoUrl, subdir } = parseGitUrl(skill.sourceUrl);
663
+ const { temp: tempDir } = getPaths();
664
+ tempPath = join2(tempDir, `upgrade-${Date.now()}`);
665
+ await mkdir(tempPath, { recursive: true });
666
+ const git = simpleGit();
667
+ console.log(`Cloning ${repoUrl} to temp for upgrade...`);
668
+ await git.clone(repoUrl, tempPath);
669
+ let commitHash = "";
670
+ try {
671
+ commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
672
+ } catch (e) {
673
+ console.warn("Failed to capture commit hash during upgrade:", e);
674
+ }
675
+ let sourcePath = tempPath;
676
+ if (subdir) {
677
+ sourcePath = join2(tempPath, subdir);
678
+ if (!existsSync2(sourcePath)) {
679
+ throw new AppError(ErrorCode.GIT_ERROR, `Subdirectory "${subdir}" not found`, 400);
495
680
  }
496
681
  }
682
+ const hasSkillMd = existsSync2(join2(sourcePath, "SKILL.md"));
683
+ const hasSkillJson = existsSync2(join2(sourcePath, "skill.json"));
684
+ const hasPackageJson = existsSync2(join2(sourcePath, "package.json"));
685
+ if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
686
+ throw new AppError(ErrorCode.VALIDATION_ERROR, "Invalid skill structure in new version", 400);
687
+ }
688
+ const files = await import("fs/promises").then((fs) => fs.readdir(skill.storagePath));
689
+ for (const file of files) {
690
+ await rm(join2(skill.storagePath, file), { recursive: true, force: true });
691
+ }
692
+ await cp(sourcePath, skill.storagePath, { recursive: true });
693
+ const parsed = await this.parseSkillMetadata(skill.storagePath);
694
+ const updatedSkill = await prisma.skill.update({
695
+ where: { id },
696
+ data: {
697
+ commitHash,
698
+ updateAvailable: false,
699
+ lastUpdateCheck: /* @__PURE__ */ new Date(),
700
+ installDate: /* @__PURE__ */ new Date(),
701
+ // Considering upgrade as reinstall? Or user prefer generic updatedAt
702
+ // Keeping installDate as "last installed/upgraded"
703
+ description: parsed.description,
704
+ metadata: JSON.stringify(parsed.metadata)
705
+ }
706
+ });
707
+ return updatedSkill;
497
708
  } catch (error) {
498
- console.warn(`Failed to remove symlink for ${skill.name} in workspace ${link.workspace.path}:`, error);
709
+ console.error("Upgrade error:", error);
710
+ throw new AppError(ErrorCode.GIT_ERROR, `Failed to upgrade skill: ${error.message}`, 500);
711
+ } finally {
712
+ if (tempPath) await rm(tempPath, { recursive: true, force: true }).catch(() => {
713
+ });
499
714
  }
500
715
  }
501
- }
502
- await prisma.skill.delete({
503
- where: { id }
504
- });
505
- if (removeFiles && skill.storagePath) {
506
- await rm(skill.storagePath, { recursive: true, force: true }).catch((err) => {
507
- console.warn(`Failed to delete skill directory ${skill.storagePath}:`, err);
508
- });
509
- }
510
- return { success: true, message: "Skill deleted successfully" };
511
- }
512
- // Check for updates
513
- async checkForUpdate(id) {
514
- const skill = await this.getSkillById(id);
515
- if (skill.source !== "git" || !skill.repoUrl) {
516
- return {
517
- hasUpdate: false,
518
- currentHash: skill.commitHash,
519
- remoteHash: null,
520
- message: "Not a git skill or missing repo URL"
521
- };
522
- }
523
- const tempPath = join2(TEMP_DIR, `check-update-${Date.now()}`);
524
- await mkdir(tempPath, { recursive: true });
525
- try {
526
- const git = simpleGit();
527
- console.log(`Checking updates for ${skill.name} from ${skill.repoUrl}...`);
528
- const remote = await git.listRemote([skill.repoUrl, "HEAD"]);
529
- if (!remote) {
530
- throw new Error("Failed to get remote HEAD");
531
- }
532
- const remoteHash = remote.split(" ")[0];
533
- const hasUpdate = remoteHash !== skill.commitHash;
534
- await prisma.skill.update({
535
- where: { id },
536
- data: {
537
- lastUpdateCheck: /* @__PURE__ */ new Date(),
538
- updateAvailable: hasUpdate
716
+ async scanForSkills() {
717
+ const { skills: skillsDir } = getPaths();
718
+ console.log(`Scanning for skills in ${skillsDir}...`);
719
+ if (!existsSync2(skillsDir)) {
720
+ console.log("Skills directory does not exist, skipping scan.");
721
+ return;
539
722
  }
540
- });
541
- return {
542
- hasUpdate,
543
- currentHash: skill.commitHash,
544
- remoteHash
545
- };
546
- } catch (error) {
547
- console.error("Update check error:", error);
548
- throw new AppError(ErrorCode.GIT_ERROR, `Failed to check for updates: ${error.message}`, 500);
549
- } finally {
550
- await rm(tempPath, { recursive: true, force: true }).catch(() => {
551
- });
552
- }
553
- }
554
- async checkUpdates(ids) {
555
- const whereClause = {
556
- source: "git",
557
- repoUrl: { not: null }
558
- };
559
- if (ids && ids.length > 0) {
560
- whereClause.id = { in: ids };
561
- }
562
- const skills = await prisma.skill.findMany({ where: whereClause });
563
- const results = {};
564
- console.log(`Checking updates for ${skills.length} skills...`);
565
- await Promise.all(skills.map(async (skill) => {
566
- try {
567
- const result = await this.checkForUpdate(skill.id);
568
- results[skill.id] = result;
569
- } catch (e) {
570
- results[skill.id] = { error: e.message };
571
- }
572
- }));
573
- return results;
574
- }
575
- async refreshMetadata(id) {
576
- const skill = await this.getSkillById(id);
577
- const parsed = await this.parseSkillMetadata(skill.storagePath);
578
- const updated = await prisma.skill.update({
579
- where: { id },
580
- data: {
581
- description: parsed.description,
582
- metadata: JSON.stringify(parsed.metadata)
583
- }
584
- });
585
- return updated;
586
- }
587
- async upgradeSkill(id) {
588
- const skill = await this.getSkillById(id);
589
- if (skill.source !== "git" || !skill.sourceUrl) {
590
- throw new AppError(ErrorCode.VALIDATION_ERROR, "Cannot upgrade non-git skill", 400);
591
- }
592
- let tempPath = null;
593
- let tempSkillPath = null;
594
- try {
595
- const { repoUrl, subdir } = parseGitUrl(skill.sourceUrl);
596
- tempPath = join2(TEMP_DIR, `upgrade-${Date.now()}`);
597
- await mkdir(tempPath, { recursive: true });
598
- const git = simpleGit();
599
- console.log(`Cloning ${repoUrl} to temp for upgrade...`);
600
- await git.clone(repoUrl, tempPath);
601
- let commitHash = "";
602
- try {
603
- commitHash = await simpleGit(tempPath).revparse(["HEAD"]);
604
- } catch (e) {
605
- console.warn("Failed to capture commit hash during upgrade:", e);
606
- }
607
- let sourcePath = tempPath;
608
- if (subdir) {
609
- sourcePath = join2(tempPath, subdir);
610
- if (!existsSync2(sourcePath)) {
611
- throw new AppError(ErrorCode.GIT_ERROR, `Subdirectory "${subdir}" not found`, 400);
723
+ const entries = await import("fs/promises").then((fs) => fs.readdir(skillsDir, { withFileTypes: true }));
724
+ const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
725
+ console.log(`Found ${directories.length} directories in skills folder.`);
726
+ let importedCount = 0;
727
+ for (const dirName of directories) {
728
+ const skillPath = join2(skillsDir, dirName);
729
+ const existingSkill = await prisma.skill.findUnique({
730
+ where: { name: dirName }
731
+ });
732
+ if (existingSkill) {
733
+ continue;
734
+ }
735
+ const hasSkillMd = existsSync2(join2(skillPath, "SKILL.md"));
736
+ const hasPackageJson = existsSync2(join2(skillPath, "package.json"));
737
+ const hasSkillJson = existsSync2(join2(skillPath, "skill.json"));
738
+ if (!hasSkillMd && !hasPackageJson && !hasSkillJson) {
739
+ console.log(`Skipping ${dirName}: No skill metadata found.`);
740
+ continue;
741
+ }
742
+ try {
743
+ console.log(`Importing skill: ${dirName}`);
744
+ const parsed = await this.parseSkillMetadata(skillPath);
745
+ let source = "local";
746
+ let sourceUrl = void 0;
747
+ let repoUrl = void 0;
748
+ let commitHash = void 0;
749
+ if (existsSync2(join2(skillPath, ".git"))) {
750
+ source = "git";
751
+ try {
752
+ const git = simpleGit(skillPath);
753
+ const remotes = await git.getRemotes(true);
754
+ if (remotes.length > 0) {
755
+ sourceUrl = remotes[0].refs.fetch;
756
+ repoUrl = sourceUrl;
757
+ }
758
+ commitHash = await git.revparse(["HEAD"]);
759
+ } catch (e) {
760
+ console.warn(`Failed to read git info for ${dirName}:`, e);
761
+ }
762
+ }
763
+ await prisma.skill.create({
764
+ data: {
765
+ name: dirName,
766
+ source,
767
+ sourceUrl,
768
+ repoUrl,
769
+ commitHash,
770
+ description: parsed.description,
771
+ storagePath: skillPath,
772
+ metadata: JSON.stringify(parsed.metadata)
773
+ }
774
+ });
775
+ importedCount++;
776
+ } catch (error) {
777
+ console.error(`Failed to import skill ${dirName}:`, error);
778
+ }
612
779
  }
780
+ console.log(`Scan complete. Imported ${importedCount} new skills.`);
781
+ return importedCount;
613
782
  }
614
- const hasSkillMd = existsSync2(join2(sourcePath, "SKILL.md"));
615
- const hasSkillJson = existsSync2(join2(sourcePath, "skill.json"));
616
- const hasPackageJson = existsSync2(join2(sourcePath, "package.json"));
617
- if (!hasSkillMd && !hasSkillJson && !hasPackageJson) {
618
- throw new AppError(ErrorCode.VALIDATION_ERROR, "Invalid skill structure in new version", 400);
619
- }
620
- const files = await import("fs/promises").then((fs) => fs.readdir(skill.storagePath));
621
- for (const file of files) {
622
- await rm(join2(skill.storagePath, file), { recursive: true, force: true });
623
- }
624
- await cp(sourcePath, skill.storagePath, { recursive: true });
625
- const parsed = await this.parseSkillMetadata(skill.storagePath);
626
- const updatedSkill = await prisma.skill.update({
627
- where: { id },
628
- data: {
629
- commitHash,
630
- updateAvailable: false,
631
- lastUpdateCheck: /* @__PURE__ */ new Date(),
632
- installDate: /* @__PURE__ */ new Date(),
633
- // Considering upgrade as reinstall? Or user prefer generic updatedAt
634
- // Keeping installDate as "last installed/upgraded"
635
- description: parsed.description,
636
- metadata: JSON.stringify(parsed.metadata)
637
- }
638
- });
639
- return updatedSkill;
640
- } catch (error) {
641
- console.error("Upgrade error:", error);
642
- throw new AppError(ErrorCode.GIT_ERROR, `Failed to upgrade skill: ${error.message}`, 500);
643
- } finally {
644
- if (tempPath) await rm(tempPath, { recursive: true, force: true }).catch(() => {
645
- });
646
- }
783
+ };
784
+ skillService = new SkillService();
647
785
  }
648
- };
649
- var skillService = new SkillService();
786
+ });
787
+
788
+ // src/index.ts
789
+ import express from "express";
790
+ import cors from "cors";
791
+ import dotenv2 from "dotenv";
792
+ import { fileURLToPath as fileURLToPath3 } from "url";
793
+ import { dirname as dirname3, join as join8 } from "path";
794
+ import { mkdir as mkdir5 } from "fs/promises";
795
+ import { existsSync as existsSync9 } from "fs";
796
+
797
+ // src/routes/skills.ts
798
+ init_skillService();
799
+ import { Router } from "express";
800
+ import multer from "multer";
801
+ import { join as join4 } from "path";
802
+ import { existsSync as existsSync4 } from "fs";
803
+ import { mkdir as mkdir3, rm as rm3 } from "fs/promises";
650
804
 
651
805
  // src/services/workspaceService.ts
806
+ init_db();
807
+ init_errorHandler();
808
+ init_dist();
652
809
  import { join as join3 } from "path";
653
810
  import { homedir } from "os";
654
811
  import { symlink, unlink as unlink2, rm as rm2, mkdir as mkdir2, appendFile, readFile as readFile2 } from "fs/promises";
@@ -971,17 +1128,17 @@ ${relativeSkillsPath}/
971
1128
  */
972
1129
  async migrateExistingSkills(workspaceId, skillNames) {
973
1130
  const workspace = await this.getWorkspaceById(workspaceId);
974
- const SKILLVERSE_HOME2 = process.env.SKILLVERSE_HOME || join3(homedir(), ".skillverse");
975
- const SKILLS_DIR2 = process.env.SKILLS_DIR || join3(SKILLVERSE_HOME2, "skills");
976
- if (!existsSync3(SKILLS_DIR2)) {
977
- await mkdir2(SKILLS_DIR2, { recursive: true });
1131
+ const SKILLVERSE_HOME = process.env.SKILLVERSE_HOME || join3(homedir(), ".skillverse");
1132
+ const SKILLS_DIR = process.env.SKILLS_DIR || join3(SKILLVERSE_HOME, "skills");
1133
+ if (!existsSync3(SKILLS_DIR)) {
1134
+ await mkdir2(SKILLS_DIR, { recursive: true });
978
1135
  }
979
1136
  const { rename, readFile: readFile3, cp: cp2 } = await import("fs/promises");
980
1137
  const migrated = [];
981
1138
  const errors = [];
982
1139
  for (const skillName of skillNames) {
983
1140
  const sourcePath = join3(workspace.path, skillName);
984
- const targetPath = join3(SKILLS_DIR2, skillName);
1141
+ const targetPath = join3(SKILLS_DIR, skillName);
985
1142
  try {
986
1143
  if (!existsSync3(sourcePath)) {
987
1144
  errors.push(`${skillName}: Source path not found`);
@@ -1053,13 +1210,13 @@ var workspaceService = new WorkspaceService();
1053
1210
 
1054
1211
  // src/routes/skills.ts
1055
1212
  var router = Router();
1056
- var TEMP_DIR2 = process.env.TEMP_DIR || join4(process.env.HOME || "", ".skillverse", "temp");
1057
- if (!existsSync4(TEMP_DIR2)) {
1058
- mkdir3(TEMP_DIR2, { recursive: true });
1213
+ var TEMP_DIR = process.env.TEMP_DIR || join4(process.env.HOME || "", ".skillverse", "temp");
1214
+ if (!existsSync4(TEMP_DIR)) {
1215
+ mkdir3(TEMP_DIR, { recursive: true });
1059
1216
  }
1060
1217
  var storage = multer.diskStorage({
1061
1218
  destination: (req, file, cb) => {
1062
- cb(null, TEMP_DIR2);
1219
+ cb(null, TEMP_DIR);
1063
1220
  },
1064
1221
  filename: (req, file, cb) => {
1065
1222
  const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
@@ -1434,6 +1591,8 @@ router2.post("/:id/migrate-skills", async (req, res, next) => {
1434
1591
  var workspaces_default = router2;
1435
1592
 
1436
1593
  // src/routes/marketplace.ts
1594
+ init_db();
1595
+ init_skillService();
1437
1596
  import { Router as Router3 } from "express";
1438
1597
 
1439
1598
  // src/services/bundleService.ts
@@ -1517,6 +1676,8 @@ var bundleService = {
1517
1676
  };
1518
1677
 
1519
1678
  // src/routes/marketplace.ts
1679
+ init_errorHandler();
1680
+ init_dist();
1520
1681
  import { existsSync as existsSync6 } from "fs";
1521
1682
  var router3 = Router3();
1522
1683
  router3.get("/skills", async (req, res, next) => {
@@ -1730,6 +1891,7 @@ router3.delete("/unpublish/:skillId", async (req, res, next) => {
1730
1891
  var marketplace_default = router3;
1731
1892
 
1732
1893
  // src/routes/dashboard.ts
1894
+ init_db();
1733
1895
  import { Router as Router4 } from "express";
1734
1896
  var router4 = Router4();
1735
1897
  router4.get("/stats", async (req, res, next) => {
@@ -1821,6 +1983,107 @@ router4.get("/activity", async (req, res, next) => {
1821
1983
  });
1822
1984
  var dashboard_default = router4;
1823
1985
 
1986
+ // src/routes/config.ts
1987
+ import { Router as Router5 } from "express";
1988
+ import { join as join6 } from "path";
1989
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
1990
+ var router5 = Router5();
1991
+ var HOME = process.env.HOME || process.env.USERPROFILE || "";
1992
+ var BOOTSTRAP_HOME = join6(HOME, ".skillverse");
1993
+ var CONFIG_FILE = join6(BOOTSTRAP_HOME, "config.json");
1994
+ function readConfig() {
1995
+ if (existsSync7(CONFIG_FILE)) {
1996
+ try {
1997
+ return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
1998
+ } catch (e) {
1999
+ console.error("Failed to read config.json:", e);
2000
+ return {};
2001
+ }
2002
+ }
2003
+ return {};
2004
+ }
2005
+ router5.get("/", (req, res, next) => {
2006
+ try {
2007
+ const fileConfig = readConfig();
2008
+ const config3 = {
2009
+ // Show saved preference if available, otherwise current env/default
2010
+ skillverseHome: fileConfig.skillverseHome || process.env.SKILLVERSE_HOME || BOOTSTRAP_HOME,
2011
+ // registryUrl is potentially in config.json already
2012
+ registryUrl: fileConfig.registryUrl || process.env.SKILLVERSE_REGISTRY || "http://localhost:4000"
2013
+ // ... add other configurable items
2014
+ };
2015
+ res.json({
2016
+ success: true,
2017
+ data: config3
2018
+ });
2019
+ } catch (error) {
2020
+ next(error);
2021
+ }
2022
+ });
2023
+ router5.post("/", async (req, res, next) => {
2024
+ try {
2025
+ const { skillverseHome, registryUrl, migrate } = req.body;
2026
+ const currentConfig = readConfig();
2027
+ const oldHome = process.env.SKILLVERSE_HOME || BOOTSTRAP_HOME;
2028
+ if (migrate && skillverseHome && skillverseHome !== oldHome) {
2029
+ console.log(`Migrating data from ${oldHome} to ${skillverseHome}...`);
2030
+ const { cp: cp2 } = await import("fs/promises");
2031
+ if (!existsSync7(skillverseHome)) {
2032
+ const { mkdir: mkdir6 } = await import("fs/promises");
2033
+ await mkdir6(skillverseHome, { recursive: true });
2034
+ }
2035
+ const copyDir = async (srcName) => {
2036
+ const src = join6(oldHome, srcName);
2037
+ const dest = join6(skillverseHome, srcName);
2038
+ if (existsSync7(src)) {
2039
+ console.log(`Copying ${srcName}...`);
2040
+ await cp2(src, dest, { recursive: true, force: true });
2041
+ }
2042
+ };
2043
+ await copyDir("skills");
2044
+ await copyDir("marketplace");
2045
+ const dbSrc = join6(oldHome, "skillverse.db");
2046
+ const dbDest = join6(skillverseHome, "skillverse.db");
2047
+ if (existsSync7(dbSrc)) {
2048
+ console.log("Copying database...");
2049
+ await cp2(dbSrc, dbDest, { force: true });
2050
+ const walSrc = dbSrc + "-wal";
2051
+ if (existsSync7(walSrc)) await cp2(walSrc, dbDest + "-wal", { force: true });
2052
+ const shmSrc = dbSrc + "-shm";
2053
+ if (existsSync7(shmSrc)) await cp2(shmSrc, dbDest + "-shm", { force: true });
2054
+ console.log("Updating skill paths in new database...");
2055
+ const { execSync } = await import("child_process");
2056
+ const oldSkillsPath = join6(oldHome, "skills");
2057
+ const newSkillsPath = join6(skillverseHome, "skills");
2058
+ const updateQuery = `UPDATE Skill SET storagePath = replace(storagePath, '${oldSkillsPath}', '${newSkillsPath}') WHERE storagePath LIKE '${oldSkillsPath}%';`;
2059
+ try {
2060
+ execSync(`sqlite3 "${dbDest}" "${updateQuery}"`, { encoding: "utf-8" });
2061
+ console.log("\u2705 Updated skill paths in new database");
2062
+ } catch (sqlErr) {
2063
+ console.error("Failed to update skill paths:", sqlErr);
2064
+ }
2065
+ }
2066
+ }
2067
+ const newConfig = {
2068
+ ...currentConfig,
2069
+ ...skillverseHome && { skillverseHome },
2070
+ ...registryUrl && { registryUrl }
2071
+ };
2072
+ writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
2073
+ res.json({
2074
+ success: true,
2075
+ data: newConfig,
2076
+ message: "Configuration saved. Restart server for changes to take effect."
2077
+ });
2078
+ } catch (error) {
2079
+ next(error);
2080
+ }
2081
+ });
2082
+ var config_default = router5;
2083
+
2084
+ // src/index.ts
2085
+ init_errorHandler();
2086
+
1824
2087
  // src/middleware/logger.ts
1825
2088
  function requestLogger(req, res, next) {
1826
2089
  const start = Date.now();
@@ -1832,8 +2095,9 @@ function requestLogger(req, res, next) {
1832
2095
  }
1833
2096
 
1834
2097
  // src/lib/initDb.ts
1835
- import { existsSync as existsSync7 } from "fs";
1836
- import { join as join6, dirname as dirname2 } from "path";
2098
+ init_db();
2099
+ import { existsSync as existsSync8 } from "fs";
2100
+ import { join as join7, dirname as dirname2 } from "path";
1837
2101
  import { fileURLToPath as fileURLToPath2 } from "url";
1838
2102
  import { spawnSync } from "child_process";
1839
2103
  var __filename2 = fileURLToPath2(import.meta.url);
@@ -1841,15 +2105,15 @@ var __dirname2 = dirname2(__filename2);
1841
2105
  function getSchemaPath() {
1842
2106
  const possiblePaths = [
1843
2107
  // Prod: dist/bin.js -> dist/../prisma/schema.prisma (i.e. ROOT/prisma/schema.prisma)
1844
- join6(__dirname2, "../prisma/schema.prisma"),
2108
+ join7(__dirname2, "../prisma/schema.prisma"),
1845
2109
  // Dev: src/lib/initDb.ts -> src/prisma/schema.prisma ? No, src/../prisma -> server/prisma
1846
2110
  // If __dirname is src/lib: ../../prisma/schema.prisma
1847
- join6(__dirname2, "../../prisma/schema.prisma"),
2111
+ join7(__dirname2, "../../prisma/schema.prisma"),
1848
2112
  // Fallbacks
1849
- join6(process.cwd(), "prisma/schema.prisma"),
1850
- join6(process.cwd(), "server/prisma/schema.prisma")
2113
+ join7(process.cwd(), "prisma/schema.prisma"),
2114
+ join7(process.cwd(), "server/prisma/schema.prisma")
1851
2115
  ];
1852
- return possiblePaths.find((p) => existsSync7(p)) || null;
2116
+ return possiblePaths.find((p) => existsSync8(p)) || null;
1853
2117
  }
1854
2118
  async function ensureDatabaseInitialized() {
1855
2119
  try {
@@ -1874,12 +2138,12 @@ async function initializeDatabase() {
1874
2138
  throw new Error("Could not find schema.prisma");
1875
2139
  }
1876
2140
  const possiblePrismaBins = [
1877
- join6(__dirname2, "../node_modules/.bin/prisma"),
1878
- join6(__dirname2, "../../node_modules/.bin/prisma"),
1879
- join6(__dirname2, "../../../node_modules/.bin/prisma"),
1880
- join6(process.cwd(), "node_modules/.bin/prisma")
2141
+ join7(__dirname2, "../node_modules/.bin/prisma"),
2142
+ join7(__dirname2, "../../node_modules/.bin/prisma"),
2143
+ join7(__dirname2, "../../../node_modules/.bin/prisma"),
2144
+ join7(process.cwd(), "node_modules/.bin/prisma")
1881
2145
  ];
1882
- const prismaBin = possiblePrismaBins.find((p) => existsSync7(p)) || "prisma";
2146
+ const prismaBin = possiblePrismaBins.find((p) => existsSync8(p)) || "prisma";
1883
2147
  console.log(` Using schema: ${schemaPath}`);
1884
2148
  const result = spawnSync(prismaBin, ["db", "push", "--schema", schemaPath], {
1885
2149
  stdio: "inherit",
@@ -1906,16 +2170,16 @@ app.use(express.json());
1906
2170
  app.use(express.urlencoded({ extended: true }));
1907
2171
  app.use(requestLogger);
1908
2172
  var possiblePublicDirs = [
1909
- join7(__dirname3, "../public"),
2173
+ join8(__dirname3, "../public"),
1910
2174
  // Production: dist/index.js -> public
1911
- join7(__dirname3, "../../public"),
2175
+ join8(__dirname3, "../../public"),
1912
2176
  // Dev tsx might resolve here
1913
- join7(process.cwd(), "public"),
2177
+ join8(process.cwd(), "public"),
1914
2178
  // Fallback: relative to cwd
1915
- join7(process.cwd(), "server/public")
2179
+ join8(process.cwd(), "server/public")
1916
2180
  // Fallback: from root
1917
2181
  ];
1918
- var publicDir = possiblePublicDirs.find((dir) => existsSync8(dir));
2182
+ var publicDir = possiblePublicDirs.find((dir) => existsSync9(dir));
1919
2183
  if (publicDir) {
1920
2184
  app.use(express.static(publicDir));
1921
2185
  console.log(`\u{1F4C2} Serving static files from: ${publicDir}`);
@@ -1926,22 +2190,23 @@ app.use("/api/skills", skills_default);
1926
2190
  app.use("/api/workspaces", workspaces_default);
1927
2191
  app.use("/api/marketplace", marketplace_default);
1928
2192
  app.use("/api/dashboard", dashboard_default);
2193
+ app.use("/api/config", config_default);
1929
2194
  app.get("/health", (req, res) => {
1930
2195
  res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1931
2196
  });
1932
- if (publicDir && existsSync8(publicDir)) {
2197
+ if (publicDir && existsSync9(publicDir)) {
1933
2198
  app.get("*", (req, res) => {
1934
- res.sendFile(join7(publicDir, "index.html"));
2199
+ res.sendFile(join8(publicDir, "index.html"));
1935
2200
  });
1936
2201
  }
1937
2202
  app.use(errorHandler);
1938
2203
  async function initializeStorage() {
1939
- const skillverseHome = process.env.SKILLVERSE_HOME || join7(process.env.HOME || "", ".skillverse");
1940
- const skillsDir = process.env.SKILLS_DIR || join7(skillverseHome, "skills");
1941
- const marketplaceDir = process.env.MARKETPLACE_DIR || join7(skillverseHome, "marketplace");
2204
+ const skillverseHome = process.env.SKILLVERSE_HOME || join8(process.env.HOME || "", ".skillverse");
2205
+ const skillsDir = process.env.SKILLS_DIR || join8(skillverseHome, "skills");
2206
+ const marketplaceDir = process.env.MARKETPLACE_DIR || join8(skillverseHome, "marketplace");
1942
2207
  const dirs = [skillverseHome, skillsDir, marketplaceDir];
1943
2208
  for (const dir of dirs) {
1944
- if (!existsSync8(dir)) {
2209
+ if (!existsSync9(dir)) {
1945
2210
  await mkdir5(dir, { recursive: true });
1946
2211
  console.log(`Created directory: ${dir}`);
1947
2212
  }
@@ -1951,10 +2216,13 @@ async function startServer(port = 3001) {
1951
2216
  try {
1952
2217
  await initializeStorage();
1953
2218
  await ensureDatabaseInitialized();
2219
+ console.log("\u{1F50D} Scanning for existing skills...");
2220
+ const { skillService: skillService2 } = await Promise.resolve().then(() => (init_skillService(), skillService_exports));
2221
+ await skillService2.scanForSkills();
1954
2222
  return new Promise((resolve) => {
1955
2223
  app.listen(port, () => {
1956
2224
  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")}`);
2225
+ console.log(`\u{1F4C1} Storage: ${process.env.SKILLVERSE_HOME || join8(process.env.HOME || "", ".skillverse")}`);
1958
2226
  resolve();
1959
2227
  });
1960
2228
  });