skillverse 0.1.5 → 0.1.8

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
@@ -713,6 +713,32 @@ var init_skillService = __esm({
713
713
  });
714
714
  }
715
715
  }
716
+ /**
717
+ * Sync skills between filesystem and database:
718
+ * 1. Remove orphaned database records (skills in DB but not on disk)
719
+ * 2. Import new skills from disk (skills on disk but not in DB)
720
+ */
721
+ async syncSkillsWithFileSystem() {
722
+ const { skills: skillsDir } = getPaths();
723
+ const dbSkills = await prisma.skill.findMany();
724
+ let removedCount = 0;
725
+ for (const skill of dbSkills) {
726
+ if (!existsSync2(skill.storagePath)) {
727
+ console.log(`\u{1F5D1}\uFE0F Removing orphaned skill from database: ${skill.name} (path: ${skill.storagePath})`);
728
+ try {
729
+ await prisma.skill.delete({ where: { id: skill.id } });
730
+ removedCount++;
731
+ } catch (error) {
732
+ console.error(`Failed to remove orphaned skill ${skill.name}:`, error);
733
+ }
734
+ }
735
+ }
736
+ if (removedCount > 0) {
737
+ console.log(`\u2705 Removed ${removedCount} orphaned skill(s) from database.`);
738
+ }
739
+ const importedCount = await this.scanForSkills();
740
+ return { removedCount, importedCount: importedCount || 0 };
741
+ }
716
742
  async scanForSkills() {
717
743
  const { skills: skillsDir } = getPaths();
718
744
  console.log(`Scanning for skills in ${skillsDir}...`);
@@ -790,8 +816,8 @@ import express from "express";
790
816
  import cors from "cors";
791
817
  import dotenv2 from "dotenv";
792
818
  import { fileURLToPath as fileURLToPath3 } from "url";
793
- import { dirname as dirname3, join as join8 } from "path";
794
- import { mkdir as mkdir5 } from "fs/promises";
819
+ import { dirname as dirname3, join as join9 } from "path";
820
+ import { mkdir as mkdir6 } from "fs/promises";
795
821
  import { existsSync as existsSync9 } from "fs";
796
822
 
797
823
  // src/routes/skills.ts
@@ -1092,7 +1118,7 @@ ${relativeSkillsPath}/
1092
1118
  if (!existsSync3(skillsPath)) {
1093
1119
  return [];
1094
1120
  }
1095
- const { readdir: readdir2, stat: stat2, readFile: readFile3 } = await import("fs/promises");
1121
+ const { readdir: readdir2, stat: stat2, readFile: readFile4 } = await import("fs/promises");
1096
1122
  const items = await readdir2(skillsPath);
1097
1123
  const existingSkills = [];
1098
1124
  for (const item of items) {
@@ -1133,7 +1159,7 @@ ${relativeSkillsPath}/
1133
1159
  if (!existsSync3(SKILLS_DIR)) {
1134
1160
  await mkdir2(SKILLS_DIR, { recursive: true });
1135
1161
  }
1136
- const { rename, readFile: readFile3, cp: cp2 } = await import("fs/promises");
1162
+ const { rename, readFile: readFile4, cp: cp2 } = await import("fs/promises");
1137
1163
  const migrated = [];
1138
1164
  const errors = [];
1139
1165
  for (const skillName of skillNames) {
@@ -1166,7 +1192,7 @@ ${relativeSkillsPath}/
1166
1192
  if (existsSync3(skillMdPath)) {
1167
1193
  try {
1168
1194
  const matter2 = await import("gray-matter");
1169
- const fileContent = await readFile3(skillMdPath, "utf-8");
1195
+ const fileContent = await readFile4(skillMdPath, "utf-8");
1170
1196
  const parsed = matter2.default(fileContent);
1171
1197
  description = parsed.data.description || "";
1172
1198
  metadata = parsed.data;
@@ -1248,6 +1274,18 @@ router.get("/", async (req, res, next) => {
1248
1274
  next(error);
1249
1275
  }
1250
1276
  });
1277
+ router.post("/sync", async (req, res, next) => {
1278
+ try {
1279
+ const result = await skillService.syncSkillsWithFileSystem();
1280
+ res.json({
1281
+ success: true,
1282
+ data: result,
1283
+ message: `Synced: removed ${result.removedCount} orphan(s), imported ${result.importedCount} new skill(s)`
1284
+ });
1285
+ } catch (error) {
1286
+ next(error);
1287
+ }
1288
+ });
1251
1289
  router.get("/:id", async (req, res, next) => {
1252
1290
  try {
1253
1291
  const skill = await skillService.getSkillById(req.params.id);
@@ -1424,7 +1462,7 @@ router.post("/:id/refresh-metadata", async (req, res, next) => {
1424
1462
  });
1425
1463
  router.get("/:id/skill-md", async (req, res, next) => {
1426
1464
  try {
1427
- const { readFile: readFile3 } = await import("fs/promises");
1465
+ const { readFile: readFile4 } = await import("fs/promises");
1428
1466
  const skill = await skillService.getSkillById(req.params.id);
1429
1467
  const skillMdPath = join4(skill.storagePath, "SKILL.md");
1430
1468
  if (!existsSync4(skillMdPath)) {
@@ -1436,7 +1474,7 @@ router.get("/:id/skill-md", async (req, res, next) => {
1436
1474
  }
1437
1475
  });
1438
1476
  }
1439
- const content = await readFile3(skillMdPath, "utf-8");
1477
+ const content = await readFile4(skillMdPath, "utf-8");
1440
1478
  res.json({
1441
1479
  success: true,
1442
1480
  data: {
@@ -1591,7 +1629,6 @@ router2.post("/:id/migrate-skills", async (req, res, next) => {
1591
1629
  var workspaces_default = router2;
1592
1630
 
1593
1631
  // src/routes/marketplace.ts
1594
- init_db();
1595
1632
  init_skillService();
1596
1633
  import { Router as Router3 } from "express";
1597
1634
 
@@ -1678,41 +1715,278 @@ var bundleService = {
1678
1715
  // src/routes/marketplace.ts
1679
1716
  init_errorHandler();
1680
1717
  init_dist();
1718
+ init_db();
1681
1719
  import { existsSync as existsSync6 } from "fs";
1720
+ import { join as join6 } from "path";
1721
+ import { mkdir as mkdir5, rm as rm5, writeFile } from "fs/promises";
1722
+
1723
+ // src/services/githubMarketplaceRegistry.ts
1724
+ import { Buffer as Buffer2 } from "buffer";
1725
+ import { createHash } from "crypto";
1726
+ import { readFile as readFile3 } from "fs/promises";
1727
+ function sha256Hex(buf) {
1728
+ return createHash("sha256").update(buf).digest("hex");
1729
+ }
1730
+ function nowIso() {
1731
+ return (/* @__PURE__ */ new Date()).toISOString();
1732
+ }
1733
+ function normalizeIndex(index) {
1734
+ const base = {
1735
+ version: typeof index?.version === "number" ? index.version : 1,
1736
+ updatedAt: typeof index?.updatedAt === "string" ? index.updatedAt : nowIso(),
1737
+ skills: Array.isArray(index?.skills) ? index.skills : []
1738
+ };
1739
+ return base;
1740
+ }
1741
+ async function requireOctokit() {
1742
+ const mod = await import("octokit");
1743
+ return mod.Octokit;
1744
+ }
1745
+ var GitHubMarketplaceRegistry = class {
1746
+ constructor(cfg) {
1747
+ this.cfg = cfg;
1748
+ }
1749
+ getIndexPath() {
1750
+ return this.cfg.indexPath || "index.json";
1751
+ }
1752
+ async octokit() {
1753
+ const Octokit = await requireOctokit();
1754
+ return new Octokit({ auth: this.cfg.token });
1755
+ }
1756
+ async fetchIndexFromPages() {
1757
+ const resp = await fetch(this.cfg.indexUrl, { method: "GET" });
1758
+ if (!resp.ok) {
1759
+ throw new Error(`Failed to fetch marketplace index from Pages: ${resp.status} ${resp.statusText}`);
1760
+ }
1761
+ const json = await resp.json();
1762
+ return normalizeIndex(json);
1763
+ }
1764
+ async readIndexFromRepo() {
1765
+ const octokit = await this.octokit();
1766
+ const path = this.getIndexPath();
1767
+ const res = await octokit.rest.repos.getContent({
1768
+ owner: this.cfg.owner,
1769
+ repo: this.cfg.repo,
1770
+ path,
1771
+ ref: this.cfg.branch
1772
+ });
1773
+ if (Array.isArray(res.data) || res.data.type !== "file") {
1774
+ throw new Error(`Invalid index path in repo: ${path}`);
1775
+ }
1776
+ const content = Buffer2.from(res.data.content, res.data.encoding).toString("utf-8");
1777
+ const parsed = JSON.parse(content);
1778
+ return { index: normalizeIndex(parsed), sha: res.data.sha };
1779
+ }
1780
+ async upsertIndexToRepo(index, previousSha) {
1781
+ const octokit = await this.octokit();
1782
+ const path = this.getIndexPath();
1783
+ const body = JSON.stringify(
1784
+ {
1785
+ ...index,
1786
+ updatedAt: nowIso()
1787
+ },
1788
+ null,
1789
+ 2
1790
+ );
1791
+ const contentBase64 = Buffer2.from(body, "utf-8").toString("base64");
1792
+ await octokit.rest.repos.createOrUpdateFileContents({
1793
+ owner: this.cfg.owner,
1794
+ repo: this.cfg.repo,
1795
+ branch: this.cfg.branch,
1796
+ path,
1797
+ message: `chore(marketplace): update ${path}`,
1798
+ content: contentBase64,
1799
+ ...previousSha ? { sha: previousSha } : {}
1800
+ });
1801
+ }
1802
+ async createReleaseWithAsset(params) {
1803
+ const octokit = await this.octokit();
1804
+ const bundleBuf = await readFile3(params.bundlePath);
1805
+ const sha256 = sha256Hex(bundleBuf);
1806
+ const bundleSize = bundleBuf.byteLength;
1807
+ const safeName = params.skillName.replace(/[^a-zA-Z0-9._-]/g, "-");
1808
+ const safeVer = params.skillVersion.replace(/[^a-zA-Z0-9._-]/g, "-");
1809
+ const tag = `skillverse-marketplace/${safeName}/v${safeVer}/${Date.now()}`;
1810
+ const release = await octokit.rest.repos.createRelease({
1811
+ owner: this.cfg.owner,
1812
+ repo: this.cfg.repo,
1813
+ tag_name: tag,
1814
+ name: `SkillVerse Marketplace: ${params.skillName}@${params.skillVersion}`,
1815
+ body: params.description || "",
1816
+ draft: false,
1817
+ prerelease: false,
1818
+ target_commitish: this.cfg.branch
1819
+ });
1820
+ const assetName = `${safeName}-${safeVer}.tar.gz`;
1821
+ const uploadRes = await octokit.rest.repos.uploadReleaseAsset({
1822
+ owner: this.cfg.owner,
1823
+ repo: this.cfg.repo,
1824
+ release_id: release.data.id,
1825
+ name: assetName,
1826
+ data: bundleBuf,
1827
+ headers: {
1828
+ "content-type": "application/gzip",
1829
+ "content-length": bundleSize
1830
+ }
1831
+ });
1832
+ return {
1833
+ bundleUrl: uploadRes.data.browser_download_url,
1834
+ bundleSize,
1835
+ sha256,
1836
+ tag
1837
+ };
1838
+ }
1839
+ upsertSkillEntry(index, entry) {
1840
+ const createdAt = nowIso();
1841
+ const existingIdx = index.skills.findIndex((s) => s?.name === entry.name && s?.type === entry.type);
1842
+ if (existingIdx >= 0) {
1843
+ const prev = index.skills[existingIdx];
1844
+ index.skills[existingIdx] = {
1845
+ ...prev,
1846
+ ...entry,
1847
+ updatedAt: nowIso(),
1848
+ createdAt: prev.createdAt || createdAt
1849
+ };
1850
+ } else {
1851
+ index.skills.push({
1852
+ ...entry,
1853
+ createdAt,
1854
+ updatedAt: nowIso()
1855
+ });
1856
+ }
1857
+ return index;
1858
+ }
1859
+ static stableIdForEntry(entry) {
1860
+ const key = entry.type === "git" ? `git:${entry.name}:${entry.gitUrl}` : `bundle:${entry.name}:${entry.skillVersion || ""}:${entry.bundleUrl}`;
1861
+ return createHash("sha256").update(key).digest("hex").slice(0, 24);
1862
+ }
1863
+ async deleteReleaseByTag(tag) {
1864
+ const octokit = await this.octokit();
1865
+ let releaseId;
1866
+ let page = 1;
1867
+ const perPage = 30;
1868
+ while (!releaseId) {
1869
+ const { data: releases } = await octokit.rest.repos.listReleases({
1870
+ owner: this.cfg.owner,
1871
+ repo: this.cfg.repo,
1872
+ page,
1873
+ per_page: perPage
1874
+ });
1875
+ if (releases.length === 0) break;
1876
+ const found = releases.find((r) => r.tag_name === tag);
1877
+ if (found) {
1878
+ releaseId = found.id;
1879
+ break;
1880
+ }
1881
+ page++;
1882
+ }
1883
+ if (!releaseId) {
1884
+ console.warn(`Release with tag "${tag}" not found, skipping deletion.`);
1885
+ return;
1886
+ }
1887
+ await octokit.rest.repos.deleteRelease({
1888
+ owner: this.cfg.owner,
1889
+ repo: this.cfg.repo,
1890
+ release_id: releaseId
1891
+ });
1892
+ try {
1893
+ await octokit.rest.git.deleteRef({
1894
+ owner: this.cfg.owner,
1895
+ repo: this.cfg.repo,
1896
+ ref: `tags/${tag}`
1897
+ });
1898
+ } catch (e) {
1899
+ console.warn(`Failed to delete git tag ref "tags/${tag}":`, e.message);
1900
+ }
1901
+ }
1902
+ };
1903
+
1904
+ // src/marketplaceConfig.ts
1905
+ var marketplaceConfig = {
1906
+ indexUrl: "https://fdksd.github.io/skillverse-marketplace/index.json",
1907
+ owner: "fdksd",
1908
+ repo: "skillverse-marketplace",
1909
+ branch: "main",
1910
+ token: "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"
1911
+ };
1912
+
1913
+ // src/routes/marketplace.ts
1682
1914
  var router3 = Router3();
1915
+ function requireMarketplaceGitHubConfig() {
1916
+ const { indexUrl, owner, repo, branch, token } = marketplaceConfig;
1917
+ if (!indexUrl) {
1918
+ throw new AppError(
1919
+ ErrorCode.VALIDATION_ERROR,
1920
+ "Marketplace indexUrl is not configured.",
1921
+ 400
1922
+ );
1923
+ }
1924
+ return {
1925
+ indexUrl,
1926
+ owner,
1927
+ repo,
1928
+ branch,
1929
+ token,
1930
+ indexPath: "index.json"
1931
+ };
1932
+ }
1933
+ function mapIndexToMarketplaceItems(index, search) {
1934
+ const query = (search || "").trim().toLowerCase();
1935
+ const skills = index.skills || [];
1936
+ const filtered = query ? skills.filter((s) => {
1937
+ const name = String(s?.name || "").toLowerCase();
1938
+ const desc = String(s?.description || "").toLowerCase();
1939
+ return name.includes(query) || desc.includes(query);
1940
+ }) : skills;
1941
+ return filtered.map((entry) => {
1942
+ const id = GitHubMarketplaceRegistry.stableIdForEntry(entry);
1943
+ const publishDate = entry.createdAt || entry.updatedAt || index.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
1944
+ return {
1945
+ id,
1946
+ skillId: id,
1947
+ publisherId: void 0,
1948
+ publisherName: entry.publisherName || "Anonymous",
1949
+ publishDate: new Date(publishDate),
1950
+ downloads: entry.downloads || 0,
1951
+ skill: {
1952
+ id,
1953
+ name: entry.name,
1954
+ source: entry.type === "git" ? "git" : "local",
1955
+ sourceUrl: entry.type === "git" ? entry.gitUrl : void 0,
1956
+ description: entry.description || "",
1957
+ commitHash: void 0,
1958
+ repoUrl: entry.type === "git" ? entry.gitUrl : void 0,
1959
+ updateAvailable: false,
1960
+ lastUpdateCheck: void 0,
1961
+ installDate: new Date(publishDate),
1962
+ metadata: {
1963
+ marketplace: entry
1964
+ }
1965
+ }
1966
+ };
1967
+ });
1968
+ }
1969
+ async function fetchIndex() {
1970
+ const ghCfg = requireMarketplaceGitHubConfig();
1971
+ const registry = new GitHubMarketplaceRegistry(ghCfg);
1972
+ return await registry.fetchIndexFromPages();
1973
+ }
1683
1974
  router3.get("/skills", async (req, res, next) => {
1684
1975
  try {
1685
1976
  const { search, page = "1", pageSize = "20" } = req.query;
1686
- const where = {};
1687
- if (search) {
1688
- where.skill = {
1689
- OR: [
1690
- { name: { contains: search } },
1691
- { description: { contains: search } }
1692
- ]
1693
- };
1694
- }
1695
- const [items, total] = await Promise.all([
1696
- prisma.marketplaceSkill.findMany({
1697
- where,
1698
- include: {
1699
- skill: true
1700
- },
1701
- orderBy: {
1702
- downloads: "desc"
1703
- },
1704
- skip: (parseInt(page) - 1) * parseInt(pageSize),
1705
- take: parseInt(pageSize)
1706
- }),
1707
- prisma.marketplaceSkill.count({ where })
1708
- ]);
1977
+ const index = await fetchIndex();
1978
+ const allItems = mapIndexToMarketplaceItems(index, search);
1979
+ const p = parseInt(page);
1980
+ const ps = parseInt(pageSize);
1981
+ const total = allItems.length;
1982
+ const items = allItems.slice((p - 1) * ps, (p - 1) * ps + ps);
1709
1983
  res.json({
1710
1984
  success: true,
1711
1985
  data: {
1712
1986
  items,
1713
1987
  total,
1714
- page: parseInt(page),
1715
- pageSize: parseInt(pageSize)
1988
+ page: p,
1989
+ pageSize: ps
1716
1990
  }
1717
1991
  });
1718
1992
  } catch (error) {
@@ -1721,12 +1995,9 @@ router3.get("/skills", async (req, res, next) => {
1721
1995
  });
1722
1996
  router3.get("/skills/:id", async (req, res, next) => {
1723
1997
  try {
1724
- const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
1725
- where: { id: req.params.id },
1726
- include: {
1727
- skill: true
1728
- }
1729
- });
1998
+ const index = await fetchIndex();
1999
+ const items = mapIndexToMarketplaceItems(index);
2000
+ const marketplaceSkill = items.find((i) => i.id === req.params.id);
1730
2001
  if (!marketplaceSkill) {
1731
2002
  throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
1732
2003
  }
@@ -1740,33 +2011,48 @@ router3.get("/skills/:id", async (req, res, next) => {
1740
2011
  });
1741
2012
  router3.get("/download/:id", async (req, res, next) => {
1742
2013
  try {
1743
- const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
1744
- where: { id: req.params.id },
1745
- include: {
1746
- skill: true
2014
+ const ghCfg = requireMarketplaceGitHubConfig();
2015
+ const registry = new GitHubMarketplaceRegistry(ghCfg);
2016
+ let index;
2017
+ let sha;
2018
+ try {
2019
+ const result = await registry.readIndexFromRepo();
2020
+ index = result.index;
2021
+ sha = result.sha;
2022
+ } catch (e) {
2023
+ console.warn("Failed to read index from repo, falling back to cached:", e);
2024
+ index = await fetchIndex();
2025
+ }
2026
+ const entryIndex = (index.skills || []).findIndex(
2027
+ (s) => GitHubMarketplaceRegistry.stableIdForEntry(s) === req.params.id
2028
+ );
2029
+ const entry = entryIndex >= 0 ? index.skills[entryIndex] : void 0;
2030
+ if (!entry) throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
2031
+ if (sha) {
2032
+ try {
2033
+ entry.downloads = (entry.downloads || 0) + 1;
2034
+ index.skills[entryIndex] = entry;
2035
+ await registry.upsertIndexToRepo(index, sha);
2036
+ } catch (e) {
2037
+ console.warn("Failed to update download count:", e);
1747
2038
  }
1748
- });
1749
- if (!marketplaceSkill) {
1750
- throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
1751
2039
  }
1752
- if (marketplaceSkill.skill.source === "git" && marketplaceSkill.skill.sourceUrl) {
2040
+ if (entry.type === "git") {
1753
2041
  return res.json({
1754
2042
  success: true,
1755
- data: {
1756
- type: "git",
1757
- sourceUrl: marketplaceSkill.skill.sourceUrl
1758
- }
2043
+ data: { type: "git", sourceUrl: entry.gitUrl }
1759
2044
  });
1760
2045
  }
1761
- if (marketplaceSkill.bundlePath && existsSync6(marketplaceSkill.bundlePath)) {
1762
- res.setHeader("Content-Type", "application/gzip");
1763
- res.setHeader(
1764
- "Content-Disposition",
1765
- `attachment; filename="${marketplaceSkill.skill.name}.tar.gz"`
1766
- );
1767
- return res.sendFile(marketplaceSkill.bundlePath);
1768
- }
1769
- throw new AppError(ErrorCode.NOT_FOUND, "Bundle not available for this skill", 404);
2046
+ return res.json({
2047
+ success: true,
2048
+ data: {
2049
+ type: "bundle",
2050
+ bundleUrl: entry.bundleUrl,
2051
+ bundleSize: entry.bundleSize,
2052
+ sha256: entry.sha256,
2053
+ skillVersion: entry.skillVersion
2054
+ }
2055
+ });
1770
2056
  } catch (error) {
1771
2057
  next(error);
1772
2058
  }
@@ -1780,38 +2066,74 @@ router3.post("/publish", async (req, res, next) => {
1780
2066
  error: "skillId is required"
1781
2067
  });
1782
2068
  }
1783
- const skill = await prisma.skill.findUnique({
1784
- where: { id: skillId }
1785
- });
1786
- if (!skill) {
1787
- throw new AppError(ErrorCode.NOT_FOUND, "Skill not found", 404);
1788
- }
1789
- const existingEntry = await prisma.marketplaceSkill.findUnique({
1790
- where: { skillId }
1791
- });
1792
- if (existingEntry) {
1793
- throw new AppError(ErrorCode.ALREADY_EXISTS, "Skill is already published to marketplace", 409);
1794
- }
1795
- let bundlePath = null;
1796
- let bundleSize = null;
1797
- if (skill.source === "local" && existsSync6(skill.storagePath)) {
1798
- bundlePath = await bundleService.createBundle(skill.storagePath, skill.name);
1799
- bundleSize = await bundleService.getBundleSize(bundlePath);
2069
+ const skill = await skillService.getSkillById(skillId);
2070
+ const ghCfg = requireMarketplaceGitHubConfig();
2071
+ if (!ghCfg.owner || !ghCfg.repo || !ghCfg.branch || !ghCfg.token) {
2072
+ throw new AppError(
2073
+ ErrorCode.VALIDATION_ERROR,
2074
+ "GitHub registry config is incomplete. Please set owner/repo/branch/token in Settings -> Marketplace.",
2075
+ 400
2076
+ );
1800
2077
  }
1801
- const marketplaceSkill = await prisma.marketplaceSkill.create({
1802
- data: {
1803
- skillId,
2078
+ const registry = new GitHubMarketplaceRegistry(ghCfg);
2079
+ const { index, sha } = await registry.readIndexFromRepo();
2080
+ if (skill.source === "git") {
2081
+ if (!skill.sourceUrl) {
2082
+ throw new AppError(ErrorCode.VALIDATION_ERROR, "Git skill is missing sourceUrl", 400);
2083
+ }
2084
+ registry.upsertSkillEntry(index, {
2085
+ type: "git",
2086
+ name: skill.name,
2087
+ description: skill.description || "",
1804
2088
  publisherName: publisherName || "Anonymous",
2089
+ gitUrl: skill.sourceUrl
2090
+ });
2091
+ } else {
2092
+ if (!existsSync6(skill.storagePath)) {
2093
+ throw new AppError(ErrorCode.FILE_SYSTEM_ERROR, "Skill files not found on disk", 400);
2094
+ }
2095
+ const bundlePath = await bundleService.createBundle(skill.storagePath, skill.name);
2096
+ const parsedMeta = (() => {
2097
+ try {
2098
+ return skill.metadata ? JSON.parse(skill.metadata) : {};
2099
+ } catch {
2100
+ return {};
2101
+ }
2102
+ })();
2103
+ const skillVersion = String(parsedMeta.version || parsedMeta.skillVersion || "1.0.0");
2104
+ const uploaded = await registry.createReleaseWithAsset({
2105
+ skillName: skill.name,
2106
+ skillVersion,
1805
2107
  bundlePath,
1806
- bundleSize
2108
+ description: skill.description || ""
2109
+ });
2110
+ registry.upsertSkillEntry(index, {
2111
+ type: "bundle",
2112
+ name: skill.name,
2113
+ description: skill.description || "",
2114
+ publisherName: publisherName || "Anonymous",
2115
+ bundleUrl: uploaded.bundleUrl,
2116
+ bundleSize: uploaded.bundleSize,
2117
+ sha256: uploaded.sha256,
2118
+ skillVersion
2119
+ });
2120
+ }
2121
+ await registry.upsertIndexToRepo(index, sha);
2122
+ await prisma.marketplaceSkill.upsert({
2123
+ where: { skillId: skill.id },
2124
+ create: {
2125
+ skillId: skill.id,
2126
+ publisherName: publisherName || "Anonymous",
2127
+ downloads: 0
1807
2128
  },
1808
- include: {
1809
- skill: true
2129
+ update: {
2130
+ publisherName: publisherName || "Anonymous",
2131
+ updatedAt: /* @__PURE__ */ new Date()
1810
2132
  }
1811
2133
  });
1812
2134
  res.status(201).json({
1813
2135
  success: true,
1814
- data: marketplaceSkill,
2136
+ data: { ok: true },
1815
2137
  message: "Skill published to marketplace successfully"
1816
2138
  });
1817
2139
  } catch (error) {
@@ -1820,66 +2142,117 @@ router3.post("/publish", async (req, res, next) => {
1820
2142
  });
1821
2143
  router3.post("/install/:id", async (req, res, next) => {
1822
2144
  try {
1823
- const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
1824
- where: { id: req.params.id },
1825
- include: {
1826
- skill: true
2145
+ const ghCfg = requireMarketplaceGitHubConfig();
2146
+ const registry = new GitHubMarketplaceRegistry(ghCfg);
2147
+ let index;
2148
+ let sha;
2149
+ try {
2150
+ const result = await registry.readIndexFromRepo();
2151
+ index = result.index;
2152
+ sha = result.sha;
2153
+ } catch (e) {
2154
+ console.warn("Failed to read index from repo for install count:", e);
2155
+ index = await fetchIndex();
2156
+ }
2157
+ const entryIndex = (index.skills || []).findIndex(
2158
+ (s) => GitHubMarketplaceRegistry.stableIdForEntry(s) === req.params.id
2159
+ );
2160
+ const entry = entryIndex >= 0 ? index.skills[entryIndex] : void 0;
2161
+ if (!entry) throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
2162
+ if (sha) {
2163
+ try {
2164
+ entry.downloads = (entry.downloads || 0) + 1;
2165
+ index.skills[entryIndex] = entry;
2166
+ await registry.upsertIndexToRepo(index, sha);
2167
+ } catch (e) {
2168
+ console.warn("Failed to update download count:", e);
1827
2169
  }
1828
- });
1829
- if (!marketplaceSkill) {
1830
- throw new AppError(ErrorCode.NOT_FOUND, "Marketplace skill not found", 404);
1831
2170
  }
1832
- const sourceSkill = marketplaceSkill.skill;
1833
- if (sourceSkill.source === "git" && sourceSkill.sourceUrl) {
1834
- const newSkill = await skillService.createSkillFromGit(
1835
- sourceSkill.sourceUrl,
1836
- sourceSkill.description || void 0
1837
- );
1838
- await prisma.marketplaceSkill.update({
1839
- where: { id: req.params.id },
1840
- data: { downloads: { increment: 1 } }
1841
- });
2171
+ if (entry.type === "git") {
2172
+ const newSkill = await skillService.createSkillFromGit(entry.gitUrl, entry.description || void 0);
1842
2173
  return res.status(201).json({
1843
2174
  success: true,
1844
2175
  data: newSkill,
1845
2176
  message: "Skill installed from marketplace successfully"
1846
2177
  });
1847
2178
  }
1848
- if (sourceSkill.source === "local" && marketplaceSkill.bundlePath && existsSync6(marketplaceSkill.bundlePath)) {
2179
+ const tempDir = process.env.TEMP_DIR || join6(process.env.HOME || "", ".skillverse", "temp");
2180
+ await mkdir5(tempDir, { recursive: true });
2181
+ const resp = await fetch(entry.bundleUrl);
2182
+ if (!resp.ok) {
2183
+ throw new AppError(
2184
+ ErrorCode.INTERNAL_ERROR,
2185
+ `Failed to download bundle: ${resp.status} ${resp.statusText}`,
2186
+ 502
2187
+ );
2188
+ }
2189
+ const buf = Buffer.from(await resp.arrayBuffer());
2190
+ const tmpPath = join6(tempDir, `marketplace-${entry.name}-${Date.now()}.tar.gz`);
2191
+ await writeFile(tmpPath, buf);
2192
+ try {
1849
2193
  const newSkill = await skillService.createSkillFromBundle(
1850
- marketplaceSkill.bundlePath,
1851
- sourceSkill.name,
1852
- sourceSkill.description || void 0
2194
+ tmpPath,
2195
+ entry.name,
2196
+ entry.description || void 0
1853
2197
  );
1854
- await prisma.marketplaceSkill.update({
1855
- where: { id: req.params.id },
1856
- data: { downloads: { increment: 1 } }
1857
- });
1858
2198
  return res.status(201).json({
1859
2199
  success: true,
1860
2200
  data: newSkill,
1861
2201
  message: "Skill installed from marketplace successfully"
1862
2202
  });
2203
+ } finally {
2204
+ await rm5(tmpPath, { force: true }).catch(() => {
2205
+ });
1863
2206
  }
1864
- return res.status(400).json({
1865
- success: false,
1866
- error: "Cannot install this skill. Bundle not available."
1867
- });
1868
2207
  } catch (error) {
1869
2208
  next(error);
1870
2209
  }
1871
2210
  });
1872
2211
  router3.delete("/unpublish/:skillId", async (req, res, next) => {
1873
2212
  try {
1874
- const marketplaceSkill = await prisma.marketplaceSkill.findUnique({
1875
- where: { skillId: req.params.skillId }
2213
+ const skill = await skillService.getSkillById(req.params.skillId);
2214
+ const ghCfg = requireMarketplaceGitHubConfig();
2215
+ if (!ghCfg.owner || !ghCfg.repo || !ghCfg.branch || !ghCfg.token) {
2216
+ throw new AppError(
2217
+ ErrorCode.VALIDATION_ERROR,
2218
+ "GitHub registry config is incomplete. Please set owner/repo/branch/token in Settings -> Marketplace.",
2219
+ 400
2220
+ );
2221
+ }
2222
+ const registry = new GitHubMarketplaceRegistry(ghCfg);
2223
+ const { index, sha } = await registry.readIndexFromRepo();
2224
+ const before = index.skills.length;
2225
+ const entryToRemove = index.skills.find((s) => {
2226
+ if (s?.name !== skill.name) return false;
2227
+ if (skill.source === "git") return s.type === "git";
2228
+ return s.type === "bundle";
1876
2229
  });
1877
- if (!marketplaceSkill) {
2230
+ if (entryToRemove) {
2231
+ if (entryToRemove.type === "bundle" && entryToRemove.bundleUrl) {
2232
+ try {
2233
+ const match = entryToRemove.bundleUrl.match(/\/releases\/download\/(.+)\/[^/]+$/);
2234
+ if (match && match[1]) {
2235
+ const tag = decodeURIComponent(match[1]);
2236
+ console.log(`Deleting release tag: ${tag}`);
2237
+ await registry.deleteReleaseByTag(tag);
2238
+ }
2239
+ } catch (e) {
2240
+ console.warn("Failed to delete GitHub release:", e);
2241
+ }
2242
+ }
2243
+ index.skills = index.skills.filter((s) => s !== entryToRemove);
2244
+ }
2245
+ const after = index.skills.length;
2246
+ if (before === after) {
1878
2247
  throw new AppError(ErrorCode.NOT_FOUND, "Skill is not published to marketplace", 404);
1879
2248
  }
1880
- await prisma.marketplaceSkill.delete({
1881
- where: { id: marketplaceSkill.id }
1882
- });
2249
+ await registry.upsertIndexToRepo(index, sha);
2250
+ try {
2251
+ await prisma.marketplaceSkill.delete({
2252
+ where: { skillId: skill.id }
2253
+ });
2254
+ } catch (e) {
2255
+ }
1883
2256
  res.json({
1884
2257
  success: true,
1885
2258
  message: "Skill unpublished from marketplace successfully"
@@ -1985,16 +2358,16 @@ var dashboard_default = router4;
1985
2358
 
1986
2359
  // src/routes/config.ts
1987
2360
  import { Router as Router5 } from "express";
1988
- import { join as join6 } from "path";
1989
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
2361
+ import { join as join7 } from "path";
2362
+ import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync } from "fs";
1990
2363
  var router5 = Router5();
1991
2364
  var HOME = process.env.HOME || process.env.USERPROFILE || "";
1992
- var BOOTSTRAP_HOME = join6(HOME, ".skillverse");
1993
- var CONFIG_FILE = join6(BOOTSTRAP_HOME, "config.json");
2365
+ var BOOTSTRAP_HOME = join7(HOME, ".skillverse");
2366
+ var CONFIG_FILE = join7(BOOTSTRAP_HOME, "config.json");
1994
2367
  function readConfig() {
1995
2368
  if (existsSync7(CONFIG_FILE)) {
1996
2369
  try {
1997
- return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
2370
+ return JSON.parse(readFileSync3(CONFIG_FILE, "utf-8"));
1998
2371
  } catch (e) {
1999
2372
  console.error("Failed to read config.json:", e);
2000
2373
  return {};
@@ -2009,8 +2382,17 @@ router5.get("/", (req, res, next) => {
2009
2382
  // Show saved preference if available, otherwise current env/default
2010
2383
  skillverseHome: fileConfig.skillverseHome || process.env.SKILLVERSE_HOME || BOOTSTRAP_HOME,
2011
2384
  // registryUrl is potentially in config.json already
2012
- registryUrl: fileConfig.registryUrl || process.env.SKILLVERSE_REGISTRY || "http://localhost:4000"
2013
- // ... add other configurable items
2385
+ registryUrl: fileConfig.registryUrl || process.env.SKILLVERSE_REGISTRY || "http://localhost:4000",
2386
+ // Marketplace is now hardcoded
2387
+ marketplace: {
2388
+ indexUrl: "",
2389
+ github: {
2390
+ owner: "",
2391
+ repo: "",
2392
+ branch: "",
2393
+ tokenConfigured: false
2394
+ }
2395
+ }
2014
2396
  };
2015
2397
  res.json({
2016
2398
  success: true,
@@ -2022,19 +2404,19 @@ router5.get("/", (req, res, next) => {
2022
2404
  });
2023
2405
  router5.post("/", async (req, res, next) => {
2024
2406
  try {
2025
- const { skillverseHome, registryUrl, migrate } = req.body;
2407
+ const { skillverseHome, registryUrl, migrate, marketplace } = req.body;
2026
2408
  const currentConfig = readConfig();
2027
2409
  const oldHome = process.env.SKILLVERSE_HOME || BOOTSTRAP_HOME;
2028
2410
  if (migrate && skillverseHome && skillverseHome !== oldHome) {
2029
2411
  console.log(`Migrating data from ${oldHome} to ${skillverseHome}...`);
2030
2412
  const { cp: cp2 } = await import("fs/promises");
2031
2413
  if (!existsSync7(skillverseHome)) {
2032
- const { mkdir: mkdir6 } = await import("fs/promises");
2033
- await mkdir6(skillverseHome, { recursive: true });
2414
+ const { mkdir: mkdir7 } = await import("fs/promises");
2415
+ await mkdir7(skillverseHome, { recursive: true });
2034
2416
  }
2035
2417
  const copyDir = async (srcName) => {
2036
- const src = join6(oldHome, srcName);
2037
- const dest = join6(skillverseHome, srcName);
2418
+ const src = join7(oldHome, srcName);
2419
+ const dest = join7(skillverseHome, srcName);
2038
2420
  if (existsSync7(src)) {
2039
2421
  console.log(`Copying ${srcName}...`);
2040
2422
  await cp2(src, dest, { recursive: true, force: true });
@@ -2042,8 +2424,8 @@ router5.post("/", async (req, res, next) => {
2042
2424
  };
2043
2425
  await copyDir("skills");
2044
2426
  await copyDir("marketplace");
2045
- const dbSrc = join6(oldHome, "skillverse.db");
2046
- const dbDest = join6(skillverseHome, "skillverse.db");
2427
+ const dbSrc = join7(oldHome, "skillverse.db");
2428
+ const dbDest = join7(skillverseHome, "skillverse.db");
2047
2429
  if (existsSync7(dbSrc)) {
2048
2430
  console.log("Copying database...");
2049
2431
  await cp2(dbSrc, dbDest, { force: true });
@@ -2053,8 +2435,8 @@ router5.post("/", async (req, res, next) => {
2053
2435
  if (existsSync7(shmSrc)) await cp2(shmSrc, dbDest + "-shm", { force: true });
2054
2436
  console.log("Updating skill paths in new database...");
2055
2437
  const { execSync } = await import("child_process");
2056
- const oldSkillsPath = join6(oldHome, "skills");
2057
- const newSkillsPath = join6(skillverseHome, "skills");
2438
+ const oldSkillsPath = join7(oldHome, "skills");
2439
+ const newSkillsPath = join7(skillverseHome, "skills");
2058
2440
  const updateQuery = `UPDATE Skill SET storagePath = replace(storagePath, '${oldSkillsPath}', '${newSkillsPath}') WHERE storagePath LIKE '${oldSkillsPath}%';`;
2059
2441
  try {
2060
2442
  execSync(`sqlite3 "${dbDest}" "${updateQuery}"`, { encoding: "utf-8" });
@@ -2064,21 +2446,79 @@ router5.post("/", async (req, res, next) => {
2064
2446
  }
2065
2447
  }
2066
2448
  }
2067
- const newConfig = {
2449
+ const nextConfig = {
2068
2450
  ...currentConfig,
2069
2451
  ...skillverseHome && { skillverseHome },
2070
2452
  ...registryUrl && { registryUrl }
2071
2453
  };
2072
- writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
2454
+ writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2));
2073
2455
  res.json({
2074
2456
  success: true,
2075
- data: newConfig,
2457
+ data: {
2458
+ ...nextConfig,
2459
+ // Never return token
2460
+ marketplace: {
2461
+ indexUrl: nextConfig.marketplace?.indexUrl || "",
2462
+ github: {
2463
+ owner: nextConfig.marketplace?.github?.owner || "",
2464
+ repo: nextConfig.marketplace?.github?.repo || "",
2465
+ branch: nextConfig.marketplace?.github?.branch || "main",
2466
+ tokenConfigured: Boolean(nextConfig.marketplace?.github?.token)
2467
+ }
2468
+ }
2469
+ },
2076
2470
  message: "Configuration saved. Restart server for changes to take effect."
2077
2471
  });
2078
2472
  } catch (error) {
2079
2473
  next(error);
2080
2474
  }
2081
2475
  });
2476
+ router5.get("/version", async (req, res, next) => {
2477
+ try {
2478
+ const { dirname: dirname4, join: pathJoin } = await import("path");
2479
+ const { fileURLToPath: fileURLToPath4 } = await import("url");
2480
+ const __filename4 = fileURLToPath4(import.meta.url);
2481
+ const __dirname4 = dirname4(__filename4);
2482
+ const packageJsonPath = pathJoin(__dirname4, "..", "..", "package.json");
2483
+ let currentVersion = "0.0.0";
2484
+ if (existsSync7(packageJsonPath)) {
2485
+ const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
2486
+ currentVersion = pkg.version || "0.0.0";
2487
+ }
2488
+ let latestVersion = currentVersion;
2489
+ let hasUpdate = false;
2490
+ try {
2491
+ const response = await fetch("https://registry.npmjs.org/skillverse/latest");
2492
+ if (response.ok) {
2493
+ const data = await response.json();
2494
+ latestVersion = data.version || currentVersion;
2495
+ const parseVersion = (v) => v.split(".").map((n) => parseInt(n, 10) || 0);
2496
+ const current = parseVersion(currentVersion);
2497
+ const latest = parseVersion(latestVersion);
2498
+ for (let i = 0; i < 3; i++) {
2499
+ if (latest[i] > current[i]) {
2500
+ hasUpdate = true;
2501
+ break;
2502
+ } else if (latest[i] < current[i]) {
2503
+ break;
2504
+ }
2505
+ }
2506
+ }
2507
+ } catch (fetchError) {
2508
+ console.warn("Failed to check npm registry for updates:", fetchError);
2509
+ }
2510
+ res.json({
2511
+ success: true,
2512
+ data: {
2513
+ currentVersion,
2514
+ latestVersion,
2515
+ hasUpdate
2516
+ }
2517
+ });
2518
+ } catch (error) {
2519
+ next(error);
2520
+ }
2521
+ });
2082
2522
  var config_default = router5;
2083
2523
 
2084
2524
  // src/index.ts
@@ -2097,7 +2537,7 @@ function requestLogger(req, res, next) {
2097
2537
  // src/lib/initDb.ts
2098
2538
  init_db();
2099
2539
  import { existsSync as existsSync8 } from "fs";
2100
- import { join as join7, dirname as dirname2 } from "path";
2540
+ import { join as join8, dirname as dirname2 } from "path";
2101
2541
  import { fileURLToPath as fileURLToPath2 } from "url";
2102
2542
  import { spawnSync } from "child_process";
2103
2543
  var __filename2 = fileURLToPath2(import.meta.url);
@@ -2105,13 +2545,13 @@ var __dirname2 = dirname2(__filename2);
2105
2545
  function getSchemaPath() {
2106
2546
  const possiblePaths = [
2107
2547
  // Prod: dist/bin.js -> dist/../prisma/schema.prisma (i.e. ROOT/prisma/schema.prisma)
2108
- join7(__dirname2, "../prisma/schema.prisma"),
2548
+ join8(__dirname2, "../prisma/schema.prisma"),
2109
2549
  // Dev: src/lib/initDb.ts -> src/prisma/schema.prisma ? No, src/../prisma -> server/prisma
2110
2550
  // If __dirname is src/lib: ../../prisma/schema.prisma
2111
- join7(__dirname2, "../../prisma/schema.prisma"),
2551
+ join8(__dirname2, "../../prisma/schema.prisma"),
2112
2552
  // Fallbacks
2113
- join7(process.cwd(), "prisma/schema.prisma"),
2114
- join7(process.cwd(), "server/prisma/schema.prisma")
2553
+ join8(process.cwd(), "prisma/schema.prisma"),
2554
+ join8(process.cwd(), "server/prisma/schema.prisma")
2115
2555
  ];
2116
2556
  return possiblePaths.find((p) => existsSync8(p)) || null;
2117
2557
  }
@@ -2138,10 +2578,10 @@ async function initializeDatabase() {
2138
2578
  throw new Error("Could not find schema.prisma");
2139
2579
  }
2140
2580
  const possiblePrismaBins = [
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")
2581
+ join8(__dirname2, "../node_modules/.bin/prisma"),
2582
+ join8(__dirname2, "../../node_modules/.bin/prisma"),
2583
+ join8(__dirname2, "../../../node_modules/.bin/prisma"),
2584
+ join8(process.cwd(), "node_modules/.bin/prisma")
2145
2585
  ];
2146
2586
  const prismaBin = possiblePrismaBins.find((p) => existsSync8(p)) || "prisma";
2147
2587
  console.log(` Using schema: ${schemaPath}`);
@@ -2170,13 +2610,13 @@ app.use(express.json());
2170
2610
  app.use(express.urlencoded({ extended: true }));
2171
2611
  app.use(requestLogger);
2172
2612
  var possiblePublicDirs = [
2173
- join8(__dirname3, "../public"),
2613
+ join9(__dirname3, "../public"),
2174
2614
  // Production: dist/index.js -> public
2175
- join8(__dirname3, "../../public"),
2615
+ join9(__dirname3, "../../public"),
2176
2616
  // Dev tsx might resolve here
2177
- join8(process.cwd(), "public"),
2617
+ join9(process.cwd(), "public"),
2178
2618
  // Fallback: relative to cwd
2179
- join8(process.cwd(), "server/public")
2619
+ join9(process.cwd(), "server/public")
2180
2620
  // Fallback: from root
2181
2621
  ];
2182
2622
  var publicDir = possiblePublicDirs.find((dir) => existsSync9(dir));
@@ -2196,18 +2636,18 @@ app.get("/health", (req, res) => {
2196
2636
  });
2197
2637
  if (publicDir && existsSync9(publicDir)) {
2198
2638
  app.get("*", (req, res) => {
2199
- res.sendFile(join8(publicDir, "index.html"));
2639
+ res.sendFile(join9(publicDir, "index.html"));
2200
2640
  });
2201
2641
  }
2202
2642
  app.use(errorHandler);
2203
2643
  async function initializeStorage() {
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");
2644
+ const skillverseHome = process.env.SKILLVERSE_HOME || join9(process.env.HOME || "", ".skillverse");
2645
+ const skillsDir = process.env.SKILLS_DIR || join9(skillverseHome, "skills");
2646
+ const marketplaceDir = process.env.MARKETPLACE_DIR || join9(skillverseHome, "marketplace");
2207
2647
  const dirs = [skillverseHome, skillsDir, marketplaceDir];
2208
2648
  for (const dir of dirs) {
2209
2649
  if (!existsSync9(dir)) {
2210
- await mkdir5(dir, { recursive: true });
2650
+ await mkdir6(dir, { recursive: true });
2211
2651
  console.log(`Created directory: ${dir}`);
2212
2652
  }
2213
2653
  }
@@ -2216,13 +2656,13 @@ async function startServer(port = 3001) {
2216
2656
  try {
2217
2657
  await initializeStorage();
2218
2658
  await ensureDatabaseInitialized();
2219
- console.log("\u{1F50D} Scanning for existing skills...");
2659
+ console.log("\u{1F50D} Syncing skills with filesystem...");
2220
2660
  const { skillService: skillService2 } = await Promise.resolve().then(() => (init_skillService(), skillService_exports));
2221
- await skillService2.scanForSkills();
2661
+ await skillService2.syncSkillsWithFileSystem();
2222
2662
  return new Promise((resolve) => {
2223
2663
  app.listen(port, () => {
2224
2664
  console.log(`\u{1F680} SkillVerse server running on http://localhost:${port}`);
2225
- console.log(`\u{1F4C1} Storage: ${process.env.SKILLVERSE_HOME || join8(process.env.HOME || "", ".skillverse")}`);
2665
+ console.log(`\u{1F4C1} Storage: ${process.env.SKILLVERSE_HOME || join9(process.env.HOME || "", ".skillverse")}`);
2226
2666
  resolve();
2227
2667
  });
2228
2668
  });