itismyskillmarket 1.3.37 → 1.3.38
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/README.md +4 -2
- package/dist/chunk-NWGRVBSX.js +2318 -0
- package/dist/electron-entry.d.ts +28 -0
- package/dist/electron-entry.js +7 -0
- package/dist/index.js +146 -2337
- package/electron/icon.ico +0 -0
- package/electron/icon.png +0 -0
- package/electron/main.mjs +214 -0
- package/electron/preload.mjs +18 -0
- package/electron/tray-icon.png +0 -0
- package/electron//345/220/257/345/212/250/346/241/214/351/235/242/345/272/224/347/224/250.bat +3 -0
- package/package.json +60 -3
|
@@ -0,0 +1,2318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/ui.ts
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, mkdirSync, rmSync, readdirSync, renameSync } from "fs";
|
|
6
|
+
import { join as join3, extname, dirname, basename } from "path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
import AdmZip from "adm-zip";
|
|
9
|
+
|
|
10
|
+
// src/commands/npm.ts
|
|
11
|
+
import https from "https";
|
|
12
|
+
import { URL as URL2 } from "url";
|
|
13
|
+
|
|
14
|
+
// src/config.ts
|
|
15
|
+
import { readFileSync, existsSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import os from "os";
|
|
18
|
+
var CONFIG_FILE_PATH = join(os.homedir(), ".skillmarket", "config.json");
|
|
19
|
+
function loadConfigFileSync() {
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(CONFIG_FILE_PATH)) {
|
|
22
|
+
const raw = readFileSync(CONFIG_FILE_PATH, "utf-8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
var fileConfig = loadConfigFileSync();
|
|
30
|
+
var NPM_SCOPE = process.env.SKM_NPM_SCOPE || fileConfig.npmScope || "@itismyskillmarket";
|
|
31
|
+
var NPM_SCOPE_FALLBACK = process.env.SKM_NPM_SCOPE_FALLBACK || fileConfig.npmScopeFallback || "@wanxuchen";
|
|
32
|
+
var NPM_REGISTRY = process.env.SKM_NPM_REGISTRY || fileConfig.npmRegistry || "https://registry.npmjs.org";
|
|
33
|
+
var DEFAULT_SCOPES = [
|
|
34
|
+
"@itismyskillmarket",
|
|
35
|
+
"@wanxuchen",
|
|
36
|
+
"@thisisskillmarket",
|
|
37
|
+
"@this-is-skillmarket",
|
|
38
|
+
"@skillmarket"
|
|
39
|
+
];
|
|
40
|
+
var fileScopes = fileConfig.npmScopes ? fileConfig.npmScopes.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
|
41
|
+
var SKILL_SCOPES = process.env.SKM_NPM_SCOPES ? process.env.SKM_NPM_SCOPES.split(",").map((s) => s.trim()).filter(Boolean) : fileScopes || DEFAULT_SCOPES;
|
|
42
|
+
var SKM_URL = process.env.SKM_URL || fileConfig.skmUrl || `https://www.npmjs.com/package/${NPM_SCOPE}`;
|
|
43
|
+
|
|
44
|
+
// src/commands/npm.ts
|
|
45
|
+
var npmCache = /* @__PURE__ */ new Map();
|
|
46
|
+
function getCached(key) {
|
|
47
|
+
const entry = npmCache.get(key);
|
|
48
|
+
if (!entry) return null;
|
|
49
|
+
if (Date.now() > entry.expiry) {
|
|
50
|
+
npmCache.delete(key);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return entry.data;
|
|
54
|
+
}
|
|
55
|
+
function setCache(key, data, ttlMs = 3e4) {
|
|
56
|
+
npmCache.set(key, { data, expiry: Date.now() + ttlMs });
|
|
57
|
+
}
|
|
58
|
+
async function fetchNpmPackage(packageName, retries = 1) {
|
|
59
|
+
const cacheKey = `pkg:${packageName}`;
|
|
60
|
+
const cached = getCached(cacheKey);
|
|
61
|
+
if (cached) return cached;
|
|
62
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await fetchNpmPackageOnce(packageName);
|
|
65
|
+
if (result !== null) {
|
|
66
|
+
setCache(cacheKey, result);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
if (attempt === retries) return null;
|
|
71
|
+
}
|
|
72
|
+
if (attempt < retries) {
|
|
73
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
async function fetchNpmPackageOnce(packageName) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const isScoped = packageName.startsWith("@");
|
|
81
|
+
let encodedName;
|
|
82
|
+
if (isScoped) {
|
|
83
|
+
const scopeAndName = packageName.substring(1);
|
|
84
|
+
const slashIndex = scopeAndName.indexOf("/");
|
|
85
|
+
if (slashIndex > 0) {
|
|
86
|
+
const scope = scopeAndName.substring(0, slashIndex);
|
|
87
|
+
const name = scopeAndName.substring(slashIndex + 1);
|
|
88
|
+
encodedName = `@${encodeURIComponent(scope)}%2F${encodeURIComponent(name)}`;
|
|
89
|
+
} else {
|
|
90
|
+
encodedName = packageName;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
encodedName = encodeURIComponent(packageName);
|
|
94
|
+
}
|
|
95
|
+
const url = new URL2(`https://registry.npmjs.org/${encodedName}`);
|
|
96
|
+
const req = https.get(url.toString(), { timeout: 1e4 }, (res) => {
|
|
97
|
+
let data = "";
|
|
98
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
99
|
+
res.resume();
|
|
100
|
+
resolve(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
res.on("data", (chunk) => {
|
|
104
|
+
data += chunk;
|
|
105
|
+
});
|
|
106
|
+
res.on("end", () => {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(data);
|
|
109
|
+
if (parsed.error) {
|
|
110
|
+
resolve(null);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve(parsed);
|
|
114
|
+
} catch {
|
|
115
|
+
resolve(null);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
req.on("error", reject);
|
|
120
|
+
req.on("timeout", () => {
|
|
121
|
+
req.destroy();
|
|
122
|
+
reject(new Error("Request timeout"));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function getPossiblePackageNames(skillId) {
|
|
127
|
+
if (skillId.startsWith("@")) {
|
|
128
|
+
return [skillId];
|
|
129
|
+
}
|
|
130
|
+
return SKILL_SCOPES.map((scope) => `${scope}/${skillId}`);
|
|
131
|
+
}
|
|
132
|
+
async function fetchSkillPackage(skillId) {
|
|
133
|
+
const packageNames = getPossiblePackageNames(skillId);
|
|
134
|
+
for (const packageName of packageNames) {
|
|
135
|
+
try {
|
|
136
|
+
const info = await fetchNpmPackage(packageName);
|
|
137
|
+
if (info) {
|
|
138
|
+
return info;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
async function searchSkillmarketPackages(options = {}) {
|
|
146
|
+
const { from = 0, size = 100, keyword } = options;
|
|
147
|
+
const packages = [];
|
|
148
|
+
let total = 0;
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const url = new URL2("https://registry.npmjs.org/-/v1/search");
|
|
151
|
+
url.searchParams.set("text", "keywords:skillmarket");
|
|
152
|
+
url.searchParams.set("size", String(Math.max(size, 100)));
|
|
153
|
+
url.searchParams.set("from", String(from));
|
|
154
|
+
const req = https.get(url.toString(), { timeout: 1e4 }, (res) => {
|
|
155
|
+
let data = "";
|
|
156
|
+
res.on("data", (chunk) => {
|
|
157
|
+
data += chunk;
|
|
158
|
+
});
|
|
159
|
+
res.on("end", () => {
|
|
160
|
+
try {
|
|
161
|
+
const result = JSON.parse(data);
|
|
162
|
+
let filteredTotal = 0;
|
|
163
|
+
if (result.objects) {
|
|
164
|
+
for (const item of result.objects) {
|
|
165
|
+
if (item?.package?.name) {
|
|
166
|
+
const pkgName = item.package.name;
|
|
167
|
+
const pkgDesc = item.package.description || "";
|
|
168
|
+
if (keyword) {
|
|
169
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
170
|
+
const shortName = pkgName.includes("/") ? pkgName.split("/")[1] : pkgName;
|
|
171
|
+
const nameMatch = shortName.toLowerCase().includes(lowerKeyword) || pkgName.toLowerCase().includes(lowerKeyword);
|
|
172
|
+
const descMatch = pkgDesc.toLowerCase().includes(lowerKeyword);
|
|
173
|
+
if (nameMatch || descMatch) {
|
|
174
|
+
packages.push(pkgName);
|
|
175
|
+
filteredTotal++;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
packages.push(pkgName);
|
|
179
|
+
filteredTotal++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
total = filteredTotal;
|
|
185
|
+
resolve({ packages, total });
|
|
186
|
+
} catch {
|
|
187
|
+
resolve({ packages: [], total: 0 });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
req.on("error", reject);
|
|
192
|
+
req.on("timeout", () => {
|
|
193
|
+
req.destroy();
|
|
194
|
+
reject(new Error("Request timeout"));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/commands/registry.ts
|
|
200
|
+
import fs2 from "fs-extra";
|
|
201
|
+
|
|
202
|
+
// src/utils/dirs.ts
|
|
203
|
+
import os2 from "os";
|
|
204
|
+
import path from "path";
|
|
205
|
+
import fs from "fs-extra";
|
|
206
|
+
|
|
207
|
+
// src/constants.ts
|
|
208
|
+
var MARKET_DIR = "skillmarket";
|
|
209
|
+
var SUBDIRS = {
|
|
210
|
+
/** npm 包下载缓存目录,用于存储从 npm 下载的 skill 包 */
|
|
211
|
+
CACHE: "cache",
|
|
212
|
+
/** 已安装 skills 的主存储目录 */
|
|
213
|
+
SKILLS: "skills",
|
|
214
|
+
/** 各平台适配层软链接目录,用于跨平台共享 skills */
|
|
215
|
+
PLATFORM_LINKS: "platform-links"
|
|
216
|
+
};
|
|
217
|
+
var PLATFORMS = [
|
|
218
|
+
"cursor",
|
|
219
|
+
// Cursor IDE - AI 代码编辑器
|
|
220
|
+
"vscode",
|
|
221
|
+
// Visual Studio Code - 微软代码编辑器
|
|
222
|
+
"codex",
|
|
223
|
+
// OpenAI Codex - OpenAI 代码生成模型
|
|
224
|
+
"opencode",
|
|
225
|
+
// OpenCode - 开源 AI 编程工具
|
|
226
|
+
"claude",
|
|
227
|
+
// Claude Code - Anthropic CLI 工具
|
|
228
|
+
"antigravity",
|
|
229
|
+
// Antigravity - AI 编程助手
|
|
230
|
+
"openclaw",
|
|
231
|
+
// OpenClaw - AgentSkills compatible agent
|
|
232
|
+
"hermes",
|
|
233
|
+
// Hermes Agent - NousResearch agent framework
|
|
234
|
+
"saitec"
|
|
235
|
+
// Saitec TUI - Custom AI coding platform
|
|
236
|
+
];
|
|
237
|
+
var REGISTRY_FILE = "registry.json";
|
|
238
|
+
var LATEST_LINK = "latest";
|
|
239
|
+
|
|
240
|
+
// src/utils/dirs.ts
|
|
241
|
+
function getMarketHome() {
|
|
242
|
+
return path.join(os2.homedir(), MARKET_DIR);
|
|
243
|
+
}
|
|
244
|
+
function getCacheDir() {
|
|
245
|
+
return path.join(getMarketHome(), SUBDIRS.CACHE);
|
|
246
|
+
}
|
|
247
|
+
function getSkillsDir() {
|
|
248
|
+
return path.join(getMarketHome(), SUBDIRS.SKILLS);
|
|
249
|
+
}
|
|
250
|
+
function getPlatformLinksDir() {
|
|
251
|
+
return path.join(getMarketHome(), SUBDIRS.PLATFORM_LINKS);
|
|
252
|
+
}
|
|
253
|
+
function getRegistryPath() {
|
|
254
|
+
return path.join(getMarketHome(), REGISTRY_FILE);
|
|
255
|
+
}
|
|
256
|
+
async function ensureMarketDirs() {
|
|
257
|
+
await fs.ensureDir(getCacheDir());
|
|
258
|
+
await fs.ensureDir(getSkillsDir());
|
|
259
|
+
await fs.ensureDir(getPlatformLinksDir());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/commands/registry.ts
|
|
263
|
+
var DEFAULT_REGISTRY = {
|
|
264
|
+
/** 空的 skills 字典,初始没有任何已安装的 skill */
|
|
265
|
+
skills: {},
|
|
266
|
+
/** 注册表创建时间 */
|
|
267
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
268
|
+
};
|
|
269
|
+
async function loadRegistry() {
|
|
270
|
+
const registryPath = getRegistryPath();
|
|
271
|
+
if (!await fs2.pathExists(registryPath)) {
|
|
272
|
+
return DEFAULT_REGISTRY;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const data = await fs2.readJson(registryPath);
|
|
276
|
+
return data;
|
|
277
|
+
} catch {
|
|
278
|
+
return DEFAULT_REGISTRY;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function saveRegistry(registry) {
|
|
282
|
+
await fs2.ensureDir(getMarketHome());
|
|
283
|
+
registry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
|
+
await fs2.writeJson(getRegistryPath(), registry, { spaces: 2 });
|
|
285
|
+
}
|
|
286
|
+
async function getInstalledSkills() {
|
|
287
|
+
const registry = await loadRegistry();
|
|
288
|
+
return Object.values(registry.skills);
|
|
289
|
+
}
|
|
290
|
+
async function isSkillInstalled(skillId) {
|
|
291
|
+
const registry = await loadRegistry();
|
|
292
|
+
return skillId in registry.skills;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/adapters/base.ts
|
|
296
|
+
import fs3 from "fs-extra";
|
|
297
|
+
import path2 from "path";
|
|
298
|
+
var BaseAdapter = class {
|
|
299
|
+
/**
|
|
300
|
+
* Get the path where a specific skill should be installed
|
|
301
|
+
*/
|
|
302
|
+
getSkillPath(skillId) {
|
|
303
|
+
return path2.join(this.skillDir, skillId);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get the path to the SKILL.md file for a skill
|
|
307
|
+
*/
|
|
308
|
+
getSkillFilePath(skillId) {
|
|
309
|
+
return path2.join(this.getSkillPath(skillId), "SKILL.md");
|
|
310
|
+
}
|
|
311
|
+
async isAvailable() {
|
|
312
|
+
try {
|
|
313
|
+
await fs3.ensureDir(this.skillDir);
|
|
314
|
+
return true;
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async isInstalled(skillId) {
|
|
320
|
+
const skillFile = this.getSkillFilePath(skillId);
|
|
321
|
+
return fs3.pathExists(skillFile);
|
|
322
|
+
}
|
|
323
|
+
async install(skillId, sourceDir) {
|
|
324
|
+
const targetDir = this.getSkillPath(skillId);
|
|
325
|
+
const targetFile = this.getSkillFilePath(skillId);
|
|
326
|
+
await fs3.ensureDir(targetDir);
|
|
327
|
+
const sourceFile = path2.join(sourceDir, "SKILL.md");
|
|
328
|
+
if (!await fs3.pathExists(sourceFile)) {
|
|
329
|
+
throw new Error(`SKILL.md not found in ${sourceDir}`);
|
|
330
|
+
}
|
|
331
|
+
await fs3.copy(sourceFile, targetFile, { overwrite: true });
|
|
332
|
+
}
|
|
333
|
+
async uninstall(skillId) {
|
|
334
|
+
const targetDir = this.getSkillPath(skillId);
|
|
335
|
+
if (await fs3.pathExists(targetDir)) {
|
|
336
|
+
await fs3.remove(targetDir);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async listInstalled() {
|
|
340
|
+
if (!await fs3.pathExists(this.skillDir)) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
const entries = await fs3.readdir(this.skillDir, { withFileTypes: true });
|
|
344
|
+
const skills = [];
|
|
345
|
+
for (const entry of entries) {
|
|
346
|
+
if (entry.isDirectory()) {
|
|
347
|
+
const skillFile = path2.join(this.skillDir, entry.name, "SKILL.md");
|
|
348
|
+
if (await fs3.pathExists(skillFile)) {
|
|
349
|
+
skills.push(entry.name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return skills;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/adapters/opencode.ts
|
|
358
|
+
import path3 from "path";
|
|
359
|
+
import os3 from "os";
|
|
360
|
+
import fs4 from "fs-extra";
|
|
361
|
+
var OpenCodeAdapter = class extends BaseAdapter {
|
|
362
|
+
id = "opencode";
|
|
363
|
+
name = "OpenCode";
|
|
364
|
+
get skillDir() {
|
|
365
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR || path3.join(os3.homedir(), ".config", "opencode");
|
|
366
|
+
return path3.join(configDir, "skills");
|
|
367
|
+
}
|
|
368
|
+
async isAvailable() {
|
|
369
|
+
if (process.env.OPENCODE) return true;
|
|
370
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR || path3.join(os3.homedir(), ".config", "opencode");
|
|
371
|
+
try {
|
|
372
|
+
await fs4.ensureDir(path3.join(configDir, "skills"));
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/adapters/claude.ts
|
|
381
|
+
import path4 from "path";
|
|
382
|
+
import os4 from "os";
|
|
383
|
+
import fs5 from "fs-extra";
|
|
384
|
+
var ClaudeAdapter = class extends BaseAdapter {
|
|
385
|
+
id = "claude";
|
|
386
|
+
name = "Claude Code";
|
|
387
|
+
get skillDir() {
|
|
388
|
+
return path4.join(os4.homedir(), ".claude", "skills");
|
|
389
|
+
}
|
|
390
|
+
async isAvailable() {
|
|
391
|
+
if (process.env.CLAUDE_CODE) return true;
|
|
392
|
+
const claudeDir = path4.join(os4.homedir(), ".claude");
|
|
393
|
+
return fs5.pathExists(claudeDir);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// src/adapters/vscode.ts
|
|
398
|
+
import path5 from "path";
|
|
399
|
+
import os5 from "os";
|
|
400
|
+
import fs6 from "fs-extra";
|
|
401
|
+
var VSCodeAdapter = class extends BaseAdapter {
|
|
402
|
+
id = "vscode";
|
|
403
|
+
name = "VSCode";
|
|
404
|
+
get skillDir() {
|
|
405
|
+
return path5.join(os5.homedir(), ".copilot", "skills");
|
|
406
|
+
}
|
|
407
|
+
async isAvailable() {
|
|
408
|
+
const possibleDirs = [
|
|
409
|
+
path5.join(os5.homedir(), ".copilot", "skills"),
|
|
410
|
+
path5.join(os5.homedir(), ".claude", "skills")
|
|
411
|
+
];
|
|
412
|
+
for (const dir of possibleDirs) {
|
|
413
|
+
try {
|
|
414
|
+
await fs6.ensureDir(dir);
|
|
415
|
+
return true;
|
|
416
|
+
} catch {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
async install(skillId, sourceDir) {
|
|
423
|
+
await super.install(skillId, sourceDir);
|
|
424
|
+
const claudeSkillDir = path5.join(os5.homedir(), ".claude", "skills");
|
|
425
|
+
const targetPath = this.getSkillPath(skillId);
|
|
426
|
+
const claudeTargetPath = path5.join(claudeSkillDir, skillId);
|
|
427
|
+
try {
|
|
428
|
+
await fs6.ensureDir(claudeSkillDir);
|
|
429
|
+
await fs6.remove(claudeTargetPath);
|
|
430
|
+
await fs6.symlink(targetPath, claudeTargetPath, "junction");
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/adapters/openclaw.ts
|
|
437
|
+
import path6 from "path";
|
|
438
|
+
import os6 from "os";
|
|
439
|
+
import fs7 from "fs-extra";
|
|
440
|
+
var OpenClawAdapter = class extends BaseAdapter {
|
|
441
|
+
id = "openclaw";
|
|
442
|
+
name = "OpenClaw";
|
|
443
|
+
skillDir = path6.join(os6.homedir(), ".openclaw", "skills");
|
|
444
|
+
async isAvailable() {
|
|
445
|
+
try {
|
|
446
|
+
return await fs7.pathExists(path6.join(os6.homedir(), ".openclaw"));
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async isInstalled(skillId) {
|
|
452
|
+
try {
|
|
453
|
+
return await fs7.pathExists(path6.join(this.skillDir, skillId));
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async install(skillId, sourceDir) {
|
|
459
|
+
await fs7.ensureDir(this.skillDir);
|
|
460
|
+
const targetDir = path6.join(this.skillDir, skillId);
|
|
461
|
+
if (await fs7.pathExists(targetDir)) {
|
|
462
|
+
await fs7.remove(targetDir);
|
|
463
|
+
}
|
|
464
|
+
await fs7.copy(sourceDir, targetDir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
async uninstall(skillId) {
|
|
467
|
+
const targetDir = path6.join(this.skillDir, skillId);
|
|
468
|
+
if (await fs7.pathExists(targetDir)) {
|
|
469
|
+
await fs7.remove(targetDir);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async listInstalled() {
|
|
473
|
+
try {
|
|
474
|
+
if (!await fs7.pathExists(this.skillDir)) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const entries = await fs7.readdir(this.skillDir, { withFileTypes: true });
|
|
478
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
479
|
+
} catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/adapters/hermes.ts
|
|
486
|
+
import path7 from "path";
|
|
487
|
+
import os7 from "os";
|
|
488
|
+
import fs8 from "fs-extra";
|
|
489
|
+
var HermesAdapter = class extends BaseAdapter {
|
|
490
|
+
id = "hermes";
|
|
491
|
+
name = "Hermes Agent";
|
|
492
|
+
skillDir = path7.join(os7.homedir(), ".hermes", "skills");
|
|
493
|
+
async isAvailable() {
|
|
494
|
+
try {
|
|
495
|
+
return await fs8.pathExists(path7.join(os7.homedir(), ".hermes"));
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async isInstalled(skillId) {
|
|
501
|
+
try {
|
|
502
|
+
return await fs8.pathExists(path7.join(this.skillDir, skillId));
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async install(skillId, sourceDir) {
|
|
508
|
+
await fs8.ensureDir(this.skillDir);
|
|
509
|
+
const targetDir = path7.join(this.skillDir, skillId);
|
|
510
|
+
if (await fs8.pathExists(targetDir)) {
|
|
511
|
+
await fs8.remove(targetDir);
|
|
512
|
+
}
|
|
513
|
+
await fs8.copy(sourceDir, targetDir, { recursive: true });
|
|
514
|
+
}
|
|
515
|
+
async uninstall(skillId) {
|
|
516
|
+
const targetDir = path7.join(this.skillDir, skillId);
|
|
517
|
+
if (await fs8.pathExists(targetDir)) {
|
|
518
|
+
await fs8.remove(targetDir);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async listInstalled() {
|
|
522
|
+
try {
|
|
523
|
+
if (!await fs8.pathExists(this.skillDir)) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
const entries = await fs8.readdir(this.skillDir, { withFileTypes: true });
|
|
527
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/adapters/saitec.ts
|
|
535
|
+
import path8 from "path";
|
|
536
|
+
import os8 from "os";
|
|
537
|
+
import fs9 from "fs-extra";
|
|
538
|
+
var SaitecAdapter = class extends BaseAdapter {
|
|
539
|
+
id = "saitec";
|
|
540
|
+
name = "Saitec TUI";
|
|
541
|
+
skillDir = path8.join(os8.homedir(), ".saitec_tui", "skills");
|
|
542
|
+
async isAvailable() {
|
|
543
|
+
try {
|
|
544
|
+
return await fs9.pathExists(path8.join(os8.homedir(), ".saitec_tui"));
|
|
545
|
+
} catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async isInstalled(skillId) {
|
|
550
|
+
try {
|
|
551
|
+
return await fs9.pathExists(path8.join(this.skillDir, skillId));
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async install(skillId, sourceDir) {
|
|
557
|
+
await fs9.ensureDir(this.skillDir);
|
|
558
|
+
const targetDir = path8.join(this.skillDir, skillId);
|
|
559
|
+
if (await fs9.pathExists(targetDir)) {
|
|
560
|
+
await fs9.remove(targetDir);
|
|
561
|
+
}
|
|
562
|
+
await fs9.copy(sourceDir, targetDir, { recursive: true });
|
|
563
|
+
}
|
|
564
|
+
async uninstall(skillId) {
|
|
565
|
+
const targetDir = path8.join(this.skillDir, skillId);
|
|
566
|
+
if (await fs9.pathExists(targetDir)) {
|
|
567
|
+
await fs9.remove(targetDir);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async listInstalled() {
|
|
571
|
+
try {
|
|
572
|
+
if (!await fs9.pathExists(this.skillDir)) {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
const entries = await fs9.readdir(this.skillDir, { withFileTypes: true });
|
|
576
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
577
|
+
} catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/adapters/registry.ts
|
|
584
|
+
var adapters = /* @__PURE__ */ new Map();
|
|
585
|
+
function registerAdapters() {
|
|
586
|
+
const opencode = new OpenCodeAdapter();
|
|
587
|
+
const claude = new ClaudeAdapter();
|
|
588
|
+
const vscode = new VSCodeAdapter();
|
|
589
|
+
const openclaw = new OpenClawAdapter();
|
|
590
|
+
const hermes = new HermesAdapter();
|
|
591
|
+
const saitec = new SaitecAdapter();
|
|
592
|
+
adapters.set(opencode.id, opencode);
|
|
593
|
+
adapters.set(claude.id, claude);
|
|
594
|
+
adapters.set(vscode.id, vscode);
|
|
595
|
+
adapters.set(openclaw.id, openclaw);
|
|
596
|
+
adapters.set(hermes.id, hermes);
|
|
597
|
+
adapters.set(saitec.id, saitec);
|
|
598
|
+
}
|
|
599
|
+
registerAdapters();
|
|
600
|
+
async function detectPlatforms() {
|
|
601
|
+
const available = [];
|
|
602
|
+
for (const adapter of adapters.values()) {
|
|
603
|
+
if (await adapter.isAvailable()) {
|
|
604
|
+
available.push(adapter);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return available;
|
|
608
|
+
}
|
|
609
|
+
function getAllAdapters() {
|
|
610
|
+
return Array.from(adapters.values());
|
|
611
|
+
}
|
|
612
|
+
function getAdapterByPlatform(platform) {
|
|
613
|
+
const idMap = {
|
|
614
|
+
opencode: "opencode",
|
|
615
|
+
claude: "claude",
|
|
616
|
+
vscode: "vscode",
|
|
617
|
+
cursor: "opencode",
|
|
618
|
+
// Cursor uses OpenCode-compatible structure
|
|
619
|
+
codex: "opencode",
|
|
620
|
+
// Codex uses OpenCode-compatible structure
|
|
621
|
+
antigravity: "opencode",
|
|
622
|
+
// Antigravity uses OpenCode-compatible structure
|
|
623
|
+
openclaw: "openclaw",
|
|
624
|
+
hermes: "hermes",
|
|
625
|
+
saitec: "saitec"
|
|
626
|
+
};
|
|
627
|
+
return adapters.get(idMap[platform]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/commands/install.ts
|
|
631
|
+
import fs10 from "fs-extra";
|
|
632
|
+
import path9 from "path";
|
|
633
|
+
import { exec } from "child_process";
|
|
634
|
+
import { promisify } from "util";
|
|
635
|
+
import * as tar from "tar";
|
|
636
|
+
var execAsync = promisify(exec);
|
|
637
|
+
async function installSkill(skillId, version, options) {
|
|
638
|
+
await ensureMarketDirs();
|
|
639
|
+
console.log(`Installing ${skillId}${version ? `@${version}` : ""}...`);
|
|
640
|
+
const pkgInfo = await fetchSkillPackage(skillId);
|
|
641
|
+
if (!pkgInfo) {
|
|
642
|
+
throw new Error(`Package ${skillId} not found`);
|
|
643
|
+
}
|
|
644
|
+
const packageName = pkgInfo.name;
|
|
645
|
+
const targetVersion = version || pkgInfo["dist-tags"]?.latest;
|
|
646
|
+
if (!targetVersion) {
|
|
647
|
+
throw new Error(`No version found for ${packageName}`);
|
|
648
|
+
}
|
|
649
|
+
const cacheDir = getCacheDir();
|
|
650
|
+
const targetDir = path9.join(cacheDir, `${packageName}@${targetVersion}`);
|
|
651
|
+
if (!await fs10.pathExists(targetDir)) {
|
|
652
|
+
console.log("Downloading package...");
|
|
653
|
+
await fs10.ensureDir(cacheDir);
|
|
654
|
+
try {
|
|
655
|
+
const { stdout } = await execAsync(
|
|
656
|
+
`npm pack ${packageName}@${targetVersion} --pack-destination "${cacheDir}"`
|
|
657
|
+
);
|
|
658
|
+
const tarballName = stdout.trim();
|
|
659
|
+
const tarballPath = path9.join(cacheDir, tarballName);
|
|
660
|
+
if (await fs10.pathExists(tarballPath)) {
|
|
661
|
+
await tar.extract({
|
|
662
|
+
file: tarballPath,
|
|
663
|
+
cwd: cacheDir
|
|
664
|
+
});
|
|
665
|
+
await fs10.remove(tarballPath);
|
|
666
|
+
await fs10.move(path9.join(cacheDir, "package"), targetDir, { overwrite: true });
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
throw new Error(`Failed to download package: ${err}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const skillsDir = getSkillsDir();
|
|
673
|
+
const skillVersionDir = path9.join(skillsDir, `${skillId}@${targetVersion}`);
|
|
674
|
+
console.log("Setting up skill...");
|
|
675
|
+
await fs10.ensureDir(skillVersionDir);
|
|
676
|
+
const pkgRoot = targetDir;
|
|
677
|
+
if (await fs10.pathExists(path9.join(pkgRoot, "SKILL.md"))) {
|
|
678
|
+
await fs10.copy(
|
|
679
|
+
path9.join(pkgRoot, "SKILL.md"),
|
|
680
|
+
path9.join(skillVersionDir, "SKILL.md")
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (await fs10.pathExists(path9.join(pkgRoot, "metadata.json"))) {
|
|
684
|
+
await fs10.copy(
|
|
685
|
+
path9.join(pkgRoot, "metadata.json"),
|
|
686
|
+
path9.join(skillVersionDir, "metadata.json")
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const skillDir = path9.join(skillsDir, skillId);
|
|
690
|
+
await fs10.ensureDir(skillDir);
|
|
691
|
+
const latestLink = path9.join(skillDir, LATEST_LINK);
|
|
692
|
+
try {
|
|
693
|
+
await fs10.remove(latestLink);
|
|
694
|
+
await fs10.symlink(skillVersionDir, latestLink, "junction");
|
|
695
|
+
} catch {
|
|
696
|
+
await fs10.copy(skillVersionDir, path9.join(skillDir, LATEST_LINK), { overwrite: true });
|
|
697
|
+
}
|
|
698
|
+
let targetAdapters = [];
|
|
699
|
+
if (options?.platforms && options.platforms.length > 0) {
|
|
700
|
+
for (const platformStr of options.platforms) {
|
|
701
|
+
const platform = platformStr;
|
|
702
|
+
const adapter = getAdapterByPlatform(platform);
|
|
703
|
+
if (adapter) {
|
|
704
|
+
targetAdapters.push(adapter);
|
|
705
|
+
} else {
|
|
706
|
+
console.warn(`\u26A0\uFE0F Unknown platform: ${platformStr}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
targetAdapters = await detectPlatforms();
|
|
711
|
+
}
|
|
712
|
+
if (targetAdapters.length === 0) {
|
|
713
|
+
console.log("No target platforms detected.");
|
|
714
|
+
console.log("Use --platform to specify platforms manually.");
|
|
715
|
+
} else {
|
|
716
|
+
console.log(`
|
|
717
|
+
Installing to ${targetAdapters.length} platform(s)...
|
|
718
|
+
`);
|
|
719
|
+
const results = [];
|
|
720
|
+
for (const adapter of targetAdapters) {
|
|
721
|
+
try {
|
|
722
|
+
const isInstalled = await adapter.isInstalled(skillId);
|
|
723
|
+
if (isInstalled && !options?.force) {
|
|
724
|
+
console.log(`${adapter.name.padEnd(12)} \u26A0\uFE0F Already installed (use --force to overwrite)`);
|
|
725
|
+
results.push({ name: adapter.name, status: "skipped" });
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
await adapter.install(skillId, skillVersionDir);
|
|
729
|
+
console.log(`${adapter.name.padEnd(12)} \u2705 Installed successfully`);
|
|
730
|
+
results.push({ name: adapter.name, status: "installed" });
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.log(`${adapter.name.padEnd(12)} \u274C Failed: ${error}`);
|
|
733
|
+
results.push({ name: adapter.name, status: "failed", error: String(error) });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const installed = results.filter((r) => r.status === "installed").length;
|
|
737
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
738
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
739
|
+
console.log(`
|
|
740
|
+
\u{1F4CA} Summary: ${installed} installed, ${skipped} skipped, ${failed} failed`);
|
|
741
|
+
}
|
|
742
|
+
const registry = await loadRegistry();
|
|
743
|
+
const installedPlatforms = targetAdapters.map((a) => a.id);
|
|
744
|
+
registry.skills[skillId] = {
|
|
745
|
+
id: skillId,
|
|
746
|
+
version: targetVersion,
|
|
747
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
748
|
+
platforms: installedPlatforms
|
|
749
|
+
};
|
|
750
|
+
await saveRegistry(registry);
|
|
751
|
+
console.log(`
|
|
752
|
+
\u2705 ${skillId}@${targetVersion} installed successfully!`);
|
|
753
|
+
console.log(` Use "skm info ${skillId}" for more details`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/commands/uninstall.ts
|
|
757
|
+
import fs11 from "fs-extra";
|
|
758
|
+
import path10 from "path";
|
|
759
|
+
import readline from "readline";
|
|
760
|
+
async function askConfirmation(message) {
|
|
761
|
+
const rl = readline.createInterface({
|
|
762
|
+
input: process.stdin,
|
|
763
|
+
output: process.stdout
|
|
764
|
+
});
|
|
765
|
+
return new Promise((resolve) => {
|
|
766
|
+
rl.question(`${message} (y/N): `, (answer) => {
|
|
767
|
+
rl.close();
|
|
768
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
async function getUninstallPreview(skillId, options) {
|
|
773
|
+
const registry = await loadRegistry();
|
|
774
|
+
const skillInfo = registry.skills[skillId];
|
|
775
|
+
let platformNames = [];
|
|
776
|
+
if (options?.platforms && options.platforms.length > 0) {
|
|
777
|
+
platformNames = options.platforms;
|
|
778
|
+
} else {
|
|
779
|
+
const adapters2 = await detectPlatforms();
|
|
780
|
+
platformNames = adapters2.map((a) => a.name);
|
|
781
|
+
}
|
|
782
|
+
const skillsDir = getSkillsDir();
|
|
783
|
+
const localPath = path10.join(skillsDir, skillId);
|
|
784
|
+
const platformLinksDir = getPlatformLinksDir();
|
|
785
|
+
const platformLinks = [];
|
|
786
|
+
for (const platform of PLATFORMS) {
|
|
787
|
+
const linkPath = path10.join(platformLinksDir, platform, "skills", skillId);
|
|
788
|
+
if (await fs11.pathExists(linkPath)) {
|
|
789
|
+
platformLinks.push(linkPath);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return { skillInfo, platforms: platformNames, localPath, platformLinks };
|
|
793
|
+
}
|
|
794
|
+
async function uninstallSkill(skillId, options) {
|
|
795
|
+
const registry = await loadRegistry();
|
|
796
|
+
if (!(skillId in registry.skills)) {
|
|
797
|
+
console.log(`\u274C Skill "${skillId}" is not installed.`);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const skillInfo = registry.skills[skillId];
|
|
801
|
+
if (options?.dryRun) {
|
|
802
|
+
const preview = await getUninstallPreview(skillId, options);
|
|
803
|
+
console.log(`
|
|
804
|
+
\u{1F4CB} Uninstall Preview for "${skillId}":`);
|
|
805
|
+
console.log(` Version: ${skillInfo.version}`);
|
|
806
|
+
console.log(` Installed: ${skillInfo.installedAt}`);
|
|
807
|
+
console.log(` Platforms (from registry): ${preview.platforms.join(", ") || "none"}`);
|
|
808
|
+
console.log(`
|
|
809
|
+
Local files to remove:`);
|
|
810
|
+
console.log(` - ${preview.localPath}`);
|
|
811
|
+
if (preview.platformLinks.length > 0) {
|
|
812
|
+
console.log(`
|
|
813
|
+
Platform links to remove:`);
|
|
814
|
+
for (const link of preview.platformLinks) {
|
|
815
|
+
console.log(` - ${link}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
console.log(`
|
|
819
|
+
\u26A0\uFE0F This was a dry-run. No files were actually deleted.`);
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
if (!options?.yes) {
|
|
823
|
+
const confirmed = await askConfirmation(`Are you sure you want to uninstall "${skillId}"?`);
|
|
824
|
+
if (!confirmed) {
|
|
825
|
+
console.log("Uninstall cancelled.");
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
console.log(`
|
|
830
|
+
Uninstalling ${skillId}@${skillInfo.version}...`);
|
|
831
|
+
let targetAdapters = [];
|
|
832
|
+
let platformUninstallErrors = [];
|
|
833
|
+
if (options?.platforms && options.platforms.length > 0) {
|
|
834
|
+
for (const platformStr of options.platforms) {
|
|
835
|
+
const platform = platformStr;
|
|
836
|
+
targetAdapters.push(getAdapterByPlatform(platform));
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
targetAdapters = await detectPlatforms();
|
|
840
|
+
}
|
|
841
|
+
const validAdapters = targetAdapters.filter((a) => a !== void 0);
|
|
842
|
+
if (validAdapters.length > 0) {
|
|
843
|
+
console.log(`
|
|
844
|
+
Uninstalling from ${validAdapters.length} platform(s)...
|
|
845
|
+
`);
|
|
846
|
+
for (const adapter of validAdapters) {
|
|
847
|
+
try {
|
|
848
|
+
await adapter.uninstall(skillId);
|
|
849
|
+
console.log(`${adapter.name.padEnd(12)} \u2705 Uninstalled`);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
852
|
+
console.log(`${adapter.name.padEnd(12)} \u274C Failed: ${errorMsg}`);
|
|
853
|
+
platformUninstallErrors.push({ name: adapter.name, error: errorMsg });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (platformUninstallErrors.length > 0 && !options?.yes) {
|
|
858
|
+
const continueAnyway = await askConfirmation(
|
|
859
|
+
`\u26A0\uFE0F ${platformUninstallErrors.length} platform(s) failed to uninstall. Continue with local cleanup?`
|
|
860
|
+
);
|
|
861
|
+
if (!continueAnyway) {
|
|
862
|
+
console.log("Uninstall aborted. Platform files may still exist.");
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const skillsDir = getSkillsDir();
|
|
867
|
+
const skillDir = path10.join(skillsDir, skillId);
|
|
868
|
+
if (await fs11.pathExists(skillDir)) {
|
|
869
|
+
await fs11.remove(skillDir);
|
|
870
|
+
console.log(`\u2705 Removed local files: ${skillDir}`);
|
|
871
|
+
}
|
|
872
|
+
const platformLinksDir = getPlatformLinksDir();
|
|
873
|
+
let removedLinks = 0;
|
|
874
|
+
for (const platform of PLATFORMS) {
|
|
875
|
+
const linkPath = path10.join(platformLinksDir, platform, "skills", skillId);
|
|
876
|
+
if (await fs11.pathExists(linkPath)) {
|
|
877
|
+
await fs11.remove(linkPath);
|
|
878
|
+
removedLinks++;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (removedLinks > 0) {
|
|
882
|
+
console.log(`\u2705 Removed ${removedLinks} platform link(s)`);
|
|
883
|
+
}
|
|
884
|
+
delete registry.skills[skillId];
|
|
885
|
+
await saveRegistry(registry);
|
|
886
|
+
console.log(`\u2705 Registry updated`);
|
|
887
|
+
console.log(`
|
|
888
|
+
\u2705 ${skillId} uninstalled successfully!`);
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
async function uninstallAll(options) {
|
|
892
|
+
const registry = await loadRegistry();
|
|
893
|
+
const installedSkills = Object.keys(registry.skills);
|
|
894
|
+
if (installedSkills.length === 0) {
|
|
895
|
+
console.log("No skills installed.");
|
|
896
|
+
return { success: 0, failed: 0 };
|
|
897
|
+
}
|
|
898
|
+
console.log(`
|
|
899
|
+
Found ${installedSkills.length} installed skill(s):`);
|
|
900
|
+
for (const skillId of installedSkills) {
|
|
901
|
+
const info = registry.skills[skillId];
|
|
902
|
+
console.log(` - ${skillId}@${info.version}`);
|
|
903
|
+
}
|
|
904
|
+
if (options?.dryRun) {
|
|
905
|
+
console.log(`
|
|
906
|
+
\u{1F4CB} Dry-run: Would uninstall ${installedSkills.length} skill(s).`);
|
|
907
|
+
console.log(`\u26A0\uFE0F No files were actually deleted.`);
|
|
908
|
+
return { success: installedSkills.length, failed: 0 };
|
|
909
|
+
}
|
|
910
|
+
if (!options?.yes) {
|
|
911
|
+
const confirmed = await askConfirmation(
|
|
912
|
+
`
|
|
913
|
+
\u26A0\uFE0F Are you sure you want to uninstall ALL ${installedSkills.length} skill(s)? This action cannot be undone.`
|
|
914
|
+
);
|
|
915
|
+
if (!confirmed) {
|
|
916
|
+
console.log("Uninstall cancelled.");
|
|
917
|
+
return { success: 0, failed: 0 };
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
console.log(`
|
|
921
|
+
Uninstalling all skills...
|
|
922
|
+
`);
|
|
923
|
+
let successCount = 0;
|
|
924
|
+
let failedCount = 0;
|
|
925
|
+
for (const skillId of installedSkills) {
|
|
926
|
+
try {
|
|
927
|
+
const success = await uninstallSkill(skillId, { ...options, yes: true });
|
|
928
|
+
if (success) {
|
|
929
|
+
successCount++;
|
|
930
|
+
} else {
|
|
931
|
+
failedCount++;
|
|
932
|
+
}
|
|
933
|
+
} catch (error) {
|
|
934
|
+
console.log(`\u274C Failed to uninstall ${skillId}: ${error}`);
|
|
935
|
+
failedCount++;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
console.log(`
|
|
939
|
+
\u{1F4CA} Summary:`);
|
|
940
|
+
console.log(` \u2705 Success: ${successCount}`);
|
|
941
|
+
console.log(` \u274C Failed: ${failedCount}`);
|
|
942
|
+
console.log(` \u{1F4E6} Total: ${installedSkills.length}`);
|
|
943
|
+
return { success: successCount, failed: failedCount };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/commands/update.ts
|
|
947
|
+
async function updateSkill(skillId) {
|
|
948
|
+
if (skillId) {
|
|
949
|
+
const pkgInfo = await fetchSkillPackage(skillId);
|
|
950
|
+
if (pkgInfo) {
|
|
951
|
+
const latestVersion = pkgInfo["dist-tags"]?.latest;
|
|
952
|
+
if (!latestVersion) {
|
|
953
|
+
console.log(`No latest version found for ${skillId}.`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
console.log(`Updating ${skillId} to ${latestVersion}...`);
|
|
957
|
+
await installSkill(skillId, latestVersion);
|
|
958
|
+
} else {
|
|
959
|
+
console.log(`Skill "${skillId}" not found in any configured scope.`);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const installed = await getInstalledSkills();
|
|
964
|
+
if (installed.length === 0) {
|
|
965
|
+
console.log("No skills installed to update.");
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
console.log(`Checking updates for ${installed.length} skill(s)...
|
|
969
|
+
`);
|
|
970
|
+
const checkResults = await Promise.allSettled(
|
|
971
|
+
installed.map(async (skill) => {
|
|
972
|
+
const pkgInfo = await fetchSkillPackage(skill.id);
|
|
973
|
+
if (!pkgInfo) {
|
|
974
|
+
return { skill, latestVersion: null, error: "failed to fetch remote" };
|
|
975
|
+
}
|
|
976
|
+
const latestVersion = pkgInfo["dist-tags"]?.latest || null;
|
|
977
|
+
return { skill, latestVersion, error: null };
|
|
978
|
+
})
|
|
979
|
+
);
|
|
980
|
+
const toUpdate = [];
|
|
981
|
+
let upToDate = 0;
|
|
982
|
+
let fetchFailed = 0;
|
|
983
|
+
for (const result of checkResults) {
|
|
984
|
+
if (result.status === "rejected") {
|
|
985
|
+
fetchFailed++;
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const { skill, latestVersion, error } = result.value;
|
|
989
|
+
if (error) {
|
|
990
|
+
console.log(` ${skill.id}: ${skill.version} (${error})`);
|
|
991
|
+
fetchFailed++;
|
|
992
|
+
} else if (latestVersion && latestVersion !== skill.version) {
|
|
993
|
+
console.log(` ${skill.id}: ${skill.version} \u2192 ${latestVersion} [UPDATE]`);
|
|
994
|
+
toUpdate.push({ skill, latestVersion });
|
|
995
|
+
} else {
|
|
996
|
+
console.log(` ${skill.id}: ${skill.version} (up to date)`);
|
|
997
|
+
upToDate++;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (toUpdate.length > 0) {
|
|
1001
|
+
console.log(`
|
|
1002
|
+
Updating ${toUpdate.length} skill(s)...
|
|
1003
|
+
`);
|
|
1004
|
+
const updateResults = await Promise.allSettled(
|
|
1005
|
+
toUpdate.map(async ({ skill, latestVersion }) => {
|
|
1006
|
+
try {
|
|
1007
|
+
await installSkill(skill.id, latestVersion);
|
|
1008
|
+
return { id: skill.id, success: true };
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
return { id: skill.id, success: false, error: err };
|
|
1011
|
+
}
|
|
1012
|
+
})
|
|
1013
|
+
);
|
|
1014
|
+
for (const result of updateResults) {
|
|
1015
|
+
if (result.status === "fulfilled" && result.value.success) {
|
|
1016
|
+
console.log(` \u2705 ${result.value.id} updated`);
|
|
1017
|
+
} else if (result.status === "fulfilled" && !result.value.success) {
|
|
1018
|
+
console.error(` \u274C Failed to update ${result.value.id}:`, result.value.error);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (toUpdate.length === 0) {
|
|
1023
|
+
console.log("\nAll skills are up to date!");
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log(`
|
|
1026
|
+
\u{1F4CA} Update summary: ${toUpdate.length} updated, ${upToDate} up-to-date, ${fetchFailed} failed`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/commands/publish.ts
|
|
1031
|
+
import { exec as exec2 } from "child_process";
|
|
1032
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1033
|
+
import { join as join2 } from "path";
|
|
1034
|
+
import { fileURLToPath } from "url";
|
|
1035
|
+
import { promisify as promisify2 } from "util";
|
|
1036
|
+
var execAsync2 = promisify2(exec2);
|
|
1037
|
+
async function publishSkill(skillName, options) {
|
|
1038
|
+
const __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
1039
|
+
const projectRoot = join2(__dirname2, "..");
|
|
1040
|
+
const skillDir = join2(projectRoot, "skills", skillName);
|
|
1041
|
+
console.log(`Publishing ${skillName}...`);
|
|
1042
|
+
if (!existsSync2(skillDir)) {
|
|
1043
|
+
throw new Error(`Skill '${skillName}' not found in skills/ directory`);
|
|
1044
|
+
}
|
|
1045
|
+
if (!options?.skipInstall) {
|
|
1046
|
+
console.log("Running npm install...");
|
|
1047
|
+
try {
|
|
1048
|
+
await execAsync2("npm install", { cwd: skillDir });
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
console.warn("Warning: npm install failed, continuing anyway...");
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (options?.version) {
|
|
1054
|
+
console.log(`Updating version to ${options.version}...`);
|
|
1055
|
+
try {
|
|
1056
|
+
await execAsync2(`npm version ${options.version} --no-git-tag-version`, {
|
|
1057
|
+
cwd: skillDir
|
|
1058
|
+
});
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
throw new Error(`Failed to update version: ${err}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const pkgJsonPath = join2(skillDir, "package.json");
|
|
1064
|
+
let baseName = skillName;
|
|
1065
|
+
if (existsSync2(pkgJsonPath)) {
|
|
1066
|
+
try {
|
|
1067
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
|
|
1068
|
+
if (pkg.name) {
|
|
1069
|
+
baseName = pkg.name.includes("/") ? pkg.name.split("/")[1] : pkg.name;
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const scopesToTry = [
|
|
1075
|
+
NPM_SCOPE,
|
|
1076
|
+
...SKILL_SCOPES.filter((s) => s !== NPM_SCOPE)
|
|
1077
|
+
];
|
|
1078
|
+
let lastError = null;
|
|
1079
|
+
let publishedName = "";
|
|
1080
|
+
for (const scope of scopesToTry) {
|
|
1081
|
+
const targetName = `${scope}/${baseName}`;
|
|
1082
|
+
if (existsSync2(pkgJsonPath)) {
|
|
1083
|
+
try {
|
|
1084
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
|
|
1085
|
+
pkg.name = targetName;
|
|
1086
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
console.log(`Publishing as ${targetName}...`);
|
|
1091
|
+
try {
|
|
1092
|
+
await execAsync2("npm publish --access=public", { cwd: skillDir });
|
|
1093
|
+
publishedName = targetName;
|
|
1094
|
+
break;
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1097
|
+
const errMsg = String(err);
|
|
1098
|
+
if (!errMsg.includes("404")) {
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
console.log(` Scope ${scope} not available, trying next...`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (!publishedName) {
|
|
1105
|
+
throw lastError || new Error("Failed to publish to npm");
|
|
1106
|
+
}
|
|
1107
|
+
console.log(`
|
|
1108
|
+
\u2705 ${skillName} published successfully as ${publishedName}!`);
|
|
1109
|
+
console.log(` View at: https://www.npmjs.com/package/${publishedName}`);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/commands/admin.ts
|
|
1113
|
+
import { execSync as execSync2 } from "child_process";
|
|
1114
|
+
async function fetchScopePackages() {
|
|
1115
|
+
const all = /* @__PURE__ */ new Set();
|
|
1116
|
+
for (const scope of SKILL_SCOPES) {
|
|
1117
|
+
try {
|
|
1118
|
+
const { packages } = await searchSkillmarketPackages({ from: 0, size: 100, keyword: scope });
|
|
1119
|
+
for (const p of packages) {
|
|
1120
|
+
if (p.startsWith(scope)) all.add(p);
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return [...all].sort();
|
|
1126
|
+
}
|
|
1127
|
+
async function adminList() {
|
|
1128
|
+
console.log("\n\u{1F50D} Fetching all published skills...\n");
|
|
1129
|
+
const packages = await fetchScopePackages();
|
|
1130
|
+
if (packages.length === 0) {
|
|
1131
|
+
console.log("No published skills found.");
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const details = await Promise.all(
|
|
1135
|
+
packages.map(async (pkg) => {
|
|
1136
|
+
try {
|
|
1137
|
+
const info = await fetchNpmPackage(pkg);
|
|
1138
|
+
if (!info) return null;
|
|
1139
|
+
const ver = info["dist-tags"]?.latest || "?";
|
|
1140
|
+
const p = info.versions?.[ver];
|
|
1141
|
+
return {
|
|
1142
|
+
name: info.name,
|
|
1143
|
+
version: ver,
|
|
1144
|
+
description: p?.description || "",
|
|
1145
|
+
hasSkillmarket: !!p?.skillmarket,
|
|
1146
|
+
platforms: (p?.skillmarket?.platforms || []).join(", "),
|
|
1147
|
+
updated: info.time?.[ver] || ""
|
|
1148
|
+
};
|
|
1149
|
+
} catch {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
})
|
|
1153
|
+
);
|
|
1154
|
+
const valid = details.filter(Boolean);
|
|
1155
|
+
console.log(`\u{1F4E6} ${valid.length} published skill(s):
|
|
1156
|
+
`);
|
|
1157
|
+
valid.sort((a, b) => a.name.localeCompare(b.name));
|
|
1158
|
+
for (const d of valid) {
|
|
1159
|
+
const flag = d.hasSkillmarket ? "\u2705" : "\u{1F4E6}";
|
|
1160
|
+
console.log(` ${flag} ${d.name}@${d.version}`);
|
|
1161
|
+
if (d.description) console.log(` ${d.description.slice(0, 80)}`);
|
|
1162
|
+
if (d.platforms) console.log(` Platforms: ${d.platforms}`);
|
|
1163
|
+
console.log();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function adminInfo(skillId) {
|
|
1167
|
+
console.log(`
|
|
1168
|
+
\u{1F50D} Fetching detailed info for "${skillId}"...
|
|
1169
|
+
`);
|
|
1170
|
+
const info = await fetchSkillPackage(skillId);
|
|
1171
|
+
if (!info) {
|
|
1172
|
+
console.error(`\u274C Skill "${skillId}" not found in any configured scope.`);
|
|
1173
|
+
console.log(` Scopes checked: ${SKILL_SCOPES.join(", ")}`);
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
const latestVer = info["dist-tags"]?.latest || "unknown";
|
|
1177
|
+
const latestPkg = info.versions?.[latestVer];
|
|
1178
|
+
const meta = latestPkg?.skillmarket;
|
|
1179
|
+
const allVersions = Object.keys(info.versions || {});
|
|
1180
|
+
console.log(`\u{1F4E6} ${info.name}`);
|
|
1181
|
+
console.log(` Description: ${info.description || "N/A"}`);
|
|
1182
|
+
console.log(` Latest: ${latestVer}`);
|
|
1183
|
+
console.log(` Total versions: ${allVersions.length}`);
|
|
1184
|
+
console.log(` Modified: ${info.time?.modified || "N/A"}`);
|
|
1185
|
+
console.log(` Author: ${info.author?.name || latestPkg?.author?.name || "N/A"}`);
|
|
1186
|
+
console.log(` License: ${info.license || latestPkg?.license || "N/A"}`);
|
|
1187
|
+
if (meta) {
|
|
1188
|
+
console.log(`
|
|
1189
|
+
\u{1F4CB} SkillMarket Metadata:`);
|
|
1190
|
+
if (meta.id) console.log(` ID: ${meta.id}`);
|
|
1191
|
+
if (meta.displayName) console.log(` Display Name: ${meta.displayName}`);
|
|
1192
|
+
if (meta.description) console.log(` Description: ${meta.description}`);
|
|
1193
|
+
if (meta.platforms && meta.platforms.length > 0) {
|
|
1194
|
+
console.log(` Platforms: ${meta.platforms.join(", ")}`);
|
|
1195
|
+
const unknown = meta.platforms.filter((p) => !PLATFORMS.includes(p));
|
|
1196
|
+
if (unknown.length > 0) {
|
|
1197
|
+
console.log(` \u26A0\uFE0F Unknown platforms: ${unknown.join(", ")}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
} else {
|
|
1201
|
+
console.log(`
|
|
1202
|
+
\u26A0\uFE0F No SkillMarket metadata (not a skillmarket skill)`);
|
|
1203
|
+
}
|
|
1204
|
+
console.log(`
|
|
1205
|
+
\u{1F4C5} Version History:`);
|
|
1206
|
+
for (const ver of allVersions.slice().reverse()) {
|
|
1207
|
+
const v = info.versions?.[ver];
|
|
1208
|
+
const time = info.time?.[ver] ? new Date(info.time[ver]).toLocaleDateString() : "?";
|
|
1209
|
+
const size = v?.dist?.unpackedSize ? ` (${(v.dist.unpackedSize / 1024).toFixed(1)} KB)` : "";
|
|
1210
|
+
const tag = ver === latestVer ? " \u2190 latest" : "";
|
|
1211
|
+
console.log(` ${ver}${tag} \u2014 ${time}${size}`);
|
|
1212
|
+
}
|
|
1213
|
+
const tags = info["dist-tags"] || {};
|
|
1214
|
+
const otherTags = Object.entries(tags).filter(([k]) => k !== "latest");
|
|
1215
|
+
if (otherTags.length > 0) {
|
|
1216
|
+
console.log(`
|
|
1217
|
+
\u{1F3F7}\uFE0F dist-tags:`);
|
|
1218
|
+
for (const [tag, ver] of otherTags) {
|
|
1219
|
+
console.log(` ${tag}: ${ver}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
console.log(`
|
|
1223
|
+
\u{1F517} Registry:`);
|
|
1224
|
+
console.log(` ${NPM_REGISTRY}/${info.name}`);
|
|
1225
|
+
console.log();
|
|
1226
|
+
}
|
|
1227
|
+
async function adminSearch(keyword, limit = 20) {
|
|
1228
|
+
console.log(`
|
|
1229
|
+
\u{1F50D} Searching published skills for "${keyword}"...
|
|
1230
|
+
`);
|
|
1231
|
+
const scopePackages = await fetchScopePackages();
|
|
1232
|
+
const matched = scopePackages.filter(
|
|
1233
|
+
(p) => p.toLowerCase().includes(keyword.toLowerCase())
|
|
1234
|
+
);
|
|
1235
|
+
if (matched.length === 0) {
|
|
1236
|
+
const { packages } = await searchSkillmarketPackages({ keyword });
|
|
1237
|
+
const filtered = packages.filter((p) => p.toLowerCase().includes(keyword.toLowerCase())).slice(0, limit);
|
|
1238
|
+
if (filtered.length === 0) {
|
|
1239
|
+
console.log(`No skills found matching "${keyword}".`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
console.log(`Found ${filtered.length} skill(s) (from npm keyword search):
|
|
1243
|
+
`);
|
|
1244
|
+
for (const pkg of filtered) {
|
|
1245
|
+
const info = await fetchNpmPackage(pkg);
|
|
1246
|
+
const ver = info?.["dist-tags"]?.latest || "?";
|
|
1247
|
+
const p = info?.versions?.[ver];
|
|
1248
|
+
console.log(` ${pkg}@${ver}`);
|
|
1249
|
+
if (p?.description) console.log(` ${p.description.slice(0, 80)}`);
|
|
1250
|
+
console.log();
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
console.log(`Found ${matched.length} skill(s) in configured scopes:
|
|
1255
|
+
`);
|
|
1256
|
+
const show = matched.slice(0, limit);
|
|
1257
|
+
for (const pkg of show) {
|
|
1258
|
+
const info = await fetchNpmPackage(pkg);
|
|
1259
|
+
const ver = info?.["dist-tags"]?.latest || "?";
|
|
1260
|
+
const p = info?.versions?.[ver];
|
|
1261
|
+
console.log(` ${pkg}@${ver}`);
|
|
1262
|
+
if (p?.skillmarket) {
|
|
1263
|
+
console.log(` \u{1F4CB} ${p.skillmarket.displayName || ""} \u2014 Platforms: ${(p.skillmarket.platforms || []).join(", ") || "none"}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (p?.description) console.log(` ${p.description.slice(0, 80)}`);
|
|
1266
|
+
console.log();
|
|
1267
|
+
}
|
|
1268
|
+
if (matched.length > limit) {
|
|
1269
|
+
console.log(` ... and ${matched.length - limit} more (use --limit to show more)`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
async function adminStats() {
|
|
1273
|
+
console.log("\n\u{1F4CA} SkillMarket Publishing Statistics\n");
|
|
1274
|
+
const packages = await fetchScopePackages();
|
|
1275
|
+
if (packages.length === 0) {
|
|
1276
|
+
console.log("No published skills found.");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const infos = (await Promise.all(
|
|
1280
|
+
packages.map(async (pkg) => {
|
|
1281
|
+
try {
|
|
1282
|
+
const info = await fetchNpmPackage(pkg);
|
|
1283
|
+
return info ? { name: pkg, info } : null;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
})
|
|
1288
|
+
)).filter(Boolean);
|
|
1289
|
+
const totalSkills = infos.length;
|
|
1290
|
+
let totalVersions = 0;
|
|
1291
|
+
let totalSize = 0;
|
|
1292
|
+
const platformSet = /* @__PURE__ */ new Set();
|
|
1293
|
+
let withMetadata = 0;
|
|
1294
|
+
let mostVersions = { name: "", count: 0 };
|
|
1295
|
+
let mostRecent = { name: "", date: "" };
|
|
1296
|
+
for (const { name, info } of infos) {
|
|
1297
|
+
const versions = Object.keys(info.versions || {});
|
|
1298
|
+
totalVersions += versions.length;
|
|
1299
|
+
if (versions.length > mostVersions.count) {
|
|
1300
|
+
mostVersions = { name, count: versions.length };
|
|
1301
|
+
}
|
|
1302
|
+
const latestVer = info["dist-tags"]?.latest;
|
|
1303
|
+
const latestPkg = latestVer ? info.versions?.[latestVer] : void 0;
|
|
1304
|
+
const meta = latestPkg?.skillmarket;
|
|
1305
|
+
if (meta) {
|
|
1306
|
+
withMetadata++;
|
|
1307
|
+
if (meta.platforms) {
|
|
1308
|
+
for (const p of meta.platforms) platformSet.add(p);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (latestPkg?.dist?.unpackedSize) {
|
|
1312
|
+
totalSize += latestPkg.dist.unpackedSize;
|
|
1313
|
+
}
|
|
1314
|
+
const modTime = info.time?.modified || "";
|
|
1315
|
+
if (modTime && modTime > mostRecent.date) {
|
|
1316
|
+
mostRecent = { name, date: modTime };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
console.log(`\u{1F4E6} Total published skills: ${totalSkills}`);
|
|
1320
|
+
console.log(`\u{1F4DD} Total versions: ${totalVersions}`);
|
|
1321
|
+
console.log(` Avg versions/skill: ${(totalVersions / totalSkills).toFixed(1)}`);
|
|
1322
|
+
console.log(`\u{1F4CB} Skills with skillmarket metadata: ${withMetadata}/${totalSkills}`);
|
|
1323
|
+
console.log(`\u{1F4BE} Total unpacked size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
|
|
1324
|
+
console.log(`\u{1F527} Platforms covered: ${platformSet.size} (${[...platformSet].join(", ")})`);
|
|
1325
|
+
console.log(`\u{1F3C6} Most versions: ${mostVersions.name} (${mostVersions.count})`);
|
|
1326
|
+
if (mostRecent.date) {
|
|
1327
|
+
console.log(`\u{1F550} Most recent update: ${mostRecent.name} (${new Date(mostRecent.date).toLocaleDateString()})`);
|
|
1328
|
+
}
|
|
1329
|
+
console.log(`\u{1F517} Registry: ${NPM_REGISTRY}`);
|
|
1330
|
+
console.log(`
|
|
1331
|
+
Configured scopes: ${SKILL_SCOPES.join(", ")}`);
|
|
1332
|
+
console.log();
|
|
1333
|
+
}
|
|
1334
|
+
async function adminVerify(skillId) {
|
|
1335
|
+
console.log(`
|
|
1336
|
+
\u{1F50D} Verifying published skill "${skillId}"...
|
|
1337
|
+
`);
|
|
1338
|
+
const info = await fetchSkillPackage(skillId);
|
|
1339
|
+
if (!info) {
|
|
1340
|
+
console.error(`\u274C Skill "${skillId}" not found.`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
let passed = 0;
|
|
1344
|
+
let failed = 0;
|
|
1345
|
+
let warnings = 0;
|
|
1346
|
+
const nameValid = /^@[^/]+\/[^/]+$/.test(info.name);
|
|
1347
|
+
if (nameValid) {
|
|
1348
|
+
console.log(`\u2705 Package name format: ${info.name}`);
|
|
1349
|
+
passed++;
|
|
1350
|
+
} else {
|
|
1351
|
+
console.log(`\u26A0\uFE0F Package name format unusual: ${info.name}`);
|
|
1352
|
+
warnings++;
|
|
1353
|
+
}
|
|
1354
|
+
const tags = info["dist-tags"] || {};
|
|
1355
|
+
if (tags.latest) {
|
|
1356
|
+
console.log(`\u2705 dist-tags.latest: ${tags.latest}`);
|
|
1357
|
+
passed++;
|
|
1358
|
+
} else {
|
|
1359
|
+
console.log(`\u274C dist-tags.latest missing`);
|
|
1360
|
+
failed++;
|
|
1361
|
+
}
|
|
1362
|
+
const latestVer = tags.latest;
|
|
1363
|
+
const latestPkg = latestVer ? info.versions?.[latestVer] : void 0;
|
|
1364
|
+
if (latestPkg) {
|
|
1365
|
+
console.log(`\u2705 Latest version ${latestVer} exists in versions`);
|
|
1366
|
+
passed++;
|
|
1367
|
+
} else {
|
|
1368
|
+
console.log(`\u274C Latest version ${latestVer} not found in versions object`);
|
|
1369
|
+
failed++;
|
|
1370
|
+
}
|
|
1371
|
+
const meta = latestPkg?.skillmarket;
|
|
1372
|
+
if (meta) {
|
|
1373
|
+
console.log(`\u2705 Has skillmarket metadata`);
|
|
1374
|
+
const checks = [
|
|
1375
|
+
{ name: "id", ok: !!meta.id },
|
|
1376
|
+
{ name: "displayName", ok: !!meta.displayName },
|
|
1377
|
+
{ name: "platforms (array)", ok: Array.isArray(meta.platforms) && meta.platforms.length > 0 }
|
|
1378
|
+
];
|
|
1379
|
+
for (const c of checks) {
|
|
1380
|
+
if (c.ok) {
|
|
1381
|
+
console.log(` \u2705 skillmarket.${c.name}`);
|
|
1382
|
+
passed++;
|
|
1383
|
+
} else {
|
|
1384
|
+
console.log(` \u26A0\uFE0F skillmarket.${c.name} missing or empty`);
|
|
1385
|
+
warnings++;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (meta.platforms) {
|
|
1389
|
+
const unknown = meta.platforms.filter(
|
|
1390
|
+
(p) => !PLATFORMS.includes(p)
|
|
1391
|
+
);
|
|
1392
|
+
if (unknown.length > 0) {
|
|
1393
|
+
console.log(` \u26A0\uFE0F Unknown platforms: ${unknown.join(", ")}`);
|
|
1394
|
+
warnings++;
|
|
1395
|
+
} else {
|
|
1396
|
+
console.log(` \u2705 All platforms recognized`);
|
|
1397
|
+
passed++;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
} else {
|
|
1401
|
+
console.log(`\u26A0\uFE0F No skillmarket metadata (not a skillmarket-formatted skill)`);
|
|
1402
|
+
warnings++;
|
|
1403
|
+
}
|
|
1404
|
+
if (latestPkg?.description) {
|
|
1405
|
+
console.log(`\u2705 Has description (${latestPkg.description.length} chars)`);
|
|
1406
|
+
passed++;
|
|
1407
|
+
} else {
|
|
1408
|
+
console.log(`\u26A0\uFE0F No description`);
|
|
1409
|
+
warnings++;
|
|
1410
|
+
}
|
|
1411
|
+
if (info.license || latestPkg?.license) {
|
|
1412
|
+
console.log(`\u2705 License: ${info.license || latestPkg?.license}`);
|
|
1413
|
+
passed++;
|
|
1414
|
+
} else {
|
|
1415
|
+
console.log(`\u26A0\uFE0F No license specified`);
|
|
1416
|
+
warnings++;
|
|
1417
|
+
}
|
|
1418
|
+
if (latestPkg?.dist?.unpackedSize) {
|
|
1419
|
+
const sizeKB = (latestPkg.dist.unpackedSize / 1024).toFixed(1);
|
|
1420
|
+
console.log(`\u2705 Package size: ${sizeKB} KB (unpacked)`);
|
|
1421
|
+
passed++;
|
|
1422
|
+
}
|
|
1423
|
+
const versionCount = Object.keys(info.versions || {}).length;
|
|
1424
|
+
console.log(`\u2139\uFE0F Total versions: ${versionCount}`);
|
|
1425
|
+
const total = passed + failed;
|
|
1426
|
+
console.log(`
|
|
1427
|
+
\u{1F4CA} Verification Result:`);
|
|
1428
|
+
console.log(` \u2705 Passed: ${passed}`);
|
|
1429
|
+
console.log(` \u26A0\uFE0F Warnings: ${warnings}`);
|
|
1430
|
+
console.log(` \u274C Failed: ${failed}`);
|
|
1431
|
+
if (failed === 0) {
|
|
1432
|
+
console.log(`
|
|
1433
|
+
\u2705 Skill "${skillId}" is valid!
|
|
1434
|
+
`);
|
|
1435
|
+
} else {
|
|
1436
|
+
console.log(`
|
|
1437
|
+
\u26A0\uFE0F Skill "${skillId}" has issues that need attention.
|
|
1438
|
+
`);
|
|
1439
|
+
process.exitCode = 1;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function resolveFullPackageName(skillId) {
|
|
1443
|
+
if (skillId.startsWith("@")) {
|
|
1444
|
+
const info = await fetchNpmPackage(skillId);
|
|
1445
|
+
if (info) return skillId;
|
|
1446
|
+
throw new Error(`Package "${skillId}" not found in npm registry`);
|
|
1447
|
+
}
|
|
1448
|
+
for (const scope of SKILL_SCOPES) {
|
|
1449
|
+
const fullName = `${scope}/${skillId}`;
|
|
1450
|
+
const info = await fetchNpmPackage(fullName);
|
|
1451
|
+
if (info) return fullName;
|
|
1452
|
+
}
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
`Could not resolve "${skillId}" to any known scope.
|
|
1455
|
+
Scopes checked: ${SKILL_SCOPES.join(", ")}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
function npmExec(command) {
|
|
1459
|
+
try {
|
|
1460
|
+
return execSync2(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
1463
|
+
const stderr = err.stderr;
|
|
1464
|
+
const msg = Buffer.isBuffer(stderr) ? stderr.toString() : stderr;
|
|
1465
|
+
throw new Error(`npm command failed: ${msg.trim()}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (err instanceof Error) {
|
|
1468
|
+
throw new Error(`npm command failed: ${err.message}`);
|
|
1469
|
+
}
|
|
1470
|
+
throw new Error("npm command failed with an unknown error");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async function adminDeprecate(skillId, options = {}) {
|
|
1474
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1475
|
+
const version = options.version || "";
|
|
1476
|
+
const message = options.message || "This skill is deprecated. Please use an alternative.";
|
|
1477
|
+
const target = version ? `${pkgName}@${version}` : pkgName;
|
|
1478
|
+
console.log(`
|
|
1479
|
+
\u26A0\uFE0F Deprecating ${target}...
|
|
1480
|
+
`);
|
|
1481
|
+
npmExec(`npm deprecate "${target}" "${message}"`);
|
|
1482
|
+
console.log(`\u2705 Successfully deprecated ${target}`);
|
|
1483
|
+
console.log(` Message: "${message}"
|
|
1484
|
+
`);
|
|
1485
|
+
}
|
|
1486
|
+
async function adminUnpublish(skillId, options = {}) {
|
|
1487
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1488
|
+
const version = options.version;
|
|
1489
|
+
let target;
|
|
1490
|
+
if (version) {
|
|
1491
|
+
target = `${pkgName}@${version}`;
|
|
1492
|
+
console.log(`
|
|
1493
|
+
\u{1F5D1}\uFE0F Unpublishing ${target}...
|
|
1494
|
+
`);
|
|
1495
|
+
} else {
|
|
1496
|
+
target = pkgName;
|
|
1497
|
+
console.log(`
|
|
1498
|
+
\u{1F5D1}\uFE0F Unpublishing entire package ${target}...
|
|
1499
|
+
`);
|
|
1500
|
+
if (!options.force) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
"Unpublishing entire package requires --force flag.\n Usage: skm admin unpublish <skill> --force\n Or unpublish a specific version: skm admin unpublish <skill> --version <ver>"
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const forceFlag = options.force ? " --force" : "";
|
|
1507
|
+
npmExec(`npm unpublish "${target}"${forceFlag}`);
|
|
1508
|
+
if (version) {
|
|
1509
|
+
console.log(`\u2705 Successfully unpublished ${target}
|
|
1510
|
+
`);
|
|
1511
|
+
} else {
|
|
1512
|
+
console.log(`\u2705 Successfully unpublished entire package ${target}
|
|
1513
|
+
`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function adminTagSet(skillId, tag, version) {
|
|
1517
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1518
|
+
console.log(`
|
|
1519
|
+
\u{1F3F7}\uFE0F Setting dist-tag "${tag}" for ${pkgName}@${version}...
|
|
1520
|
+
`);
|
|
1521
|
+
npmExec(`npm dist-tag add "${pkgName}@${version}" "${tag}"`);
|
|
1522
|
+
console.log(`\u2705 dist-tag "${tag}" set to ${pkgName}@${version}
|
|
1523
|
+
`);
|
|
1524
|
+
}
|
|
1525
|
+
async function adminTagRemove(skillId, tag) {
|
|
1526
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1527
|
+
console.log(`
|
|
1528
|
+
\u{1F3F7}\uFE0F Removing dist-tag "${tag}" from ${pkgName}...
|
|
1529
|
+
`);
|
|
1530
|
+
npmExec(`npm dist-tag rm "${pkgName}" "${tag}"`);
|
|
1531
|
+
console.log(`\u2705 dist-tag "${tag}" removed from ${pkgName}
|
|
1532
|
+
`);
|
|
1533
|
+
}
|
|
1534
|
+
async function adminTagList(skillId) {
|
|
1535
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1536
|
+
console.log(`
|
|
1537
|
+
\u{1F3F7}\uFE0F dist-tags for ${pkgName}:
|
|
1538
|
+
`);
|
|
1539
|
+
const output = npmExec(`npm dist-tag ls "${pkgName}"`);
|
|
1540
|
+
if (!output) {
|
|
1541
|
+
console.log(" (no dist-tags found)\n");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const lines = output.split("\n").filter(Boolean);
|
|
1545
|
+
for (const line of lines) {
|
|
1546
|
+
const [tag, version] = line.split(": ").map((s) => s.trim());
|
|
1547
|
+
const isLatest = tag === "latest" ? " \u2190 latest" : "";
|
|
1548
|
+
console.log(` ${tag}: ${version}${isLatest}`);
|
|
1549
|
+
}
|
|
1550
|
+
console.log();
|
|
1551
|
+
}
|
|
1552
|
+
async function adminOwnerAdd(skillId, user) {
|
|
1553
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1554
|
+
console.log(`
|
|
1555
|
+
\u{1F465} Adding owner "${user}" to ${pkgName}...
|
|
1556
|
+
`);
|
|
1557
|
+
npmExec(`npm owner add "${user}" "${pkgName}"`);
|
|
1558
|
+
console.log(`\u2705 Owner "${user}" added to ${pkgName}
|
|
1559
|
+
`);
|
|
1560
|
+
}
|
|
1561
|
+
async function adminOwnerRemove(skillId, user) {
|
|
1562
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1563
|
+
console.log(`
|
|
1564
|
+
\u{1F465} Removing owner "${user}" from ${pkgName}...
|
|
1565
|
+
`);
|
|
1566
|
+
npmExec(`npm owner rm "${user}" "${pkgName}"`);
|
|
1567
|
+
console.log(`\u2705 Owner "${user}" removed from ${pkgName}
|
|
1568
|
+
`);
|
|
1569
|
+
}
|
|
1570
|
+
async function adminAccess(skillId, level) {
|
|
1571
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1572
|
+
console.log(`
|
|
1573
|
+
\u{1F512} Setting access for ${pkgName} to "${level}"...
|
|
1574
|
+
`);
|
|
1575
|
+
npmExec(`npm access "${level}" "${pkgName}"`);
|
|
1576
|
+
console.log(`\u2705 Access for ${pkgName} set to "${level}"
|
|
1577
|
+
`);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/commands/ui.ts
|
|
1581
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
1582
|
+
var __dirname = dirname(__filename);
|
|
1583
|
+
var guiDir = join3(__dirname, "..", "gui");
|
|
1584
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1585
|
+
function getCached2(key) {
|
|
1586
|
+
const entry = cache.get(key);
|
|
1587
|
+
if (!entry) return null;
|
|
1588
|
+
if (Date.now() > entry.expiry) {
|
|
1589
|
+
cache.delete(key);
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
return entry.data;
|
|
1593
|
+
}
|
|
1594
|
+
function setCache2(key, data, ttlMs = 6e4) {
|
|
1595
|
+
cache.set(key, { data, expiry: Date.now() + ttlMs });
|
|
1596
|
+
}
|
|
1597
|
+
async function throttledMap(items, fn, concurrency = 3) {
|
|
1598
|
+
const results = [];
|
|
1599
|
+
for (let i = 0; i < items.length; i += concurrency) {
|
|
1600
|
+
const batch = items.slice(i, i + concurrency);
|
|
1601
|
+
const batchResults = await Promise.all(batch.map((item, idx) => fn(item, i + idx)));
|
|
1602
|
+
results.push(...batchResults);
|
|
1603
|
+
if (i + concurrency < items.length) {
|
|
1604
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return results;
|
|
1608
|
+
}
|
|
1609
|
+
var MIME_TYPES = {
|
|
1610
|
+
".html": "text/html; charset=utf-8",
|
|
1611
|
+
".js": "application/javascript; charset=utf-8",
|
|
1612
|
+
".css": "text/css; charset=utf-8",
|
|
1613
|
+
".json": "application/json; charset=utf-8",
|
|
1614
|
+
".png": "image/png",
|
|
1615
|
+
".svg": "image/svg+xml",
|
|
1616
|
+
".ico": "image/x-icon"
|
|
1617
|
+
};
|
|
1618
|
+
function jsonResponse(res, status, data) {
|
|
1619
|
+
res.writeHead(status, {
|
|
1620
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1621
|
+
"Access-Control-Allow-Origin": "*"
|
|
1622
|
+
});
|
|
1623
|
+
res.end(JSON.stringify(data));
|
|
1624
|
+
}
|
|
1625
|
+
function parseBody(req) {
|
|
1626
|
+
return new Promise((resolve, reject) => {
|
|
1627
|
+
let body = "";
|
|
1628
|
+
req.on("data", (chunk) => {
|
|
1629
|
+
body += chunk.toString();
|
|
1630
|
+
});
|
|
1631
|
+
req.on("end", () => {
|
|
1632
|
+
if (!body) return resolve({});
|
|
1633
|
+
try {
|
|
1634
|
+
resolve(JSON.parse(body));
|
|
1635
|
+
} catch {
|
|
1636
|
+
reject(new Error("Invalid JSON body"));
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
req.on("error", reject);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
var API_ROUTES = {
|
|
1643
|
+
GET: {},
|
|
1644
|
+
POST: {}
|
|
1645
|
+
};
|
|
1646
|
+
API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
1647
|
+
try {
|
|
1648
|
+
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1"));
|
|
1649
|
+
const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20")));
|
|
1650
|
+
const search = url.searchParams.get("search") || "";
|
|
1651
|
+
const sort = url.searchParams.get("sort") || "name";
|
|
1652
|
+
const platform = url.searchParams.get("platform") || "";
|
|
1653
|
+
const cacheKey = `search:${search}:limit:${limit}`;
|
|
1654
|
+
let searchResult = getCached2(cacheKey);
|
|
1655
|
+
if (!searchResult) {
|
|
1656
|
+
searchResult = await searchSkillmarketPackages({
|
|
1657
|
+
from: 0,
|
|
1658
|
+
size: 100,
|
|
1659
|
+
// 一次拉取更多,避免分页
|
|
1660
|
+
keyword: search || void 0
|
|
1661
|
+
});
|
|
1662
|
+
setCache2(cacheKey, searchResult, 3e4);
|
|
1663
|
+
}
|
|
1664
|
+
const { packages, total } = searchResult;
|
|
1665
|
+
let fetchErrors = 0;
|
|
1666
|
+
const skillDetails = await throttledMap(packages, async (pkgName) => {
|
|
1667
|
+
try {
|
|
1668
|
+
const pkgCacheKey = `pkg:${pkgName}`;
|
|
1669
|
+
let info = getCached2(pkgCacheKey);
|
|
1670
|
+
if (!info) {
|
|
1671
|
+
info = await fetchNpmPackage(pkgName);
|
|
1672
|
+
if (info) setCache2(pkgCacheKey, info, 3e4);
|
|
1673
|
+
}
|
|
1674
|
+
if (!info) {
|
|
1675
|
+
fetchErrors++;
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
const latestVersion = info["dist-tags"]?.latest || "unknown";
|
|
1679
|
+
const pkg = info.versions?.[latestVersion];
|
|
1680
|
+
const meta = pkg?.skillmarket;
|
|
1681
|
+
return {
|
|
1682
|
+
id: meta?.id || info.name.replace(/^@[^/]+\//, ""),
|
|
1683
|
+
name: info.name,
|
|
1684
|
+
displayName: meta?.displayName || info.name,
|
|
1685
|
+
version: latestVersion,
|
|
1686
|
+
description: pkg?.description || "",
|
|
1687
|
+
platforms: meta?.platforms || [],
|
|
1688
|
+
author: info.author?.name || pkg?.author?.name || "",
|
|
1689
|
+
homepage: pkg?.homepage || "",
|
|
1690
|
+
repository: pkg?.repository?.url || "",
|
|
1691
|
+
updated: info.time?.[latestVersion] || info.time?.modified || ""
|
|
1692
|
+
};
|
|
1693
|
+
} catch {
|
|
1694
|
+
fetchErrors++;
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
}, 3);
|
|
1698
|
+
let skills = skillDetails.filter(Boolean);
|
|
1699
|
+
if (platform) {
|
|
1700
|
+
skills = skills.filter(
|
|
1701
|
+
(s) => Array.isArray(s.platforms) && s.platforms.includes(platform)
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
skills.sort((a, b) => {
|
|
1705
|
+
const nameA = (a.displayName || a.id || "").toLowerCase();
|
|
1706
|
+
const nameB = (b.displayName || b.id || "").toLowerCase();
|
|
1707
|
+
switch (sort) {
|
|
1708
|
+
case "-name":
|
|
1709
|
+
return nameB.localeCompare(nameA);
|
|
1710
|
+
case "updated":
|
|
1711
|
+
return (a.updated || "").localeCompare(b.updated || "");
|
|
1712
|
+
case "-updated":
|
|
1713
|
+
return (b.updated || "").localeCompare(a.updated || "");
|
|
1714
|
+
case "name":
|
|
1715
|
+
default:
|
|
1716
|
+
return nameA.localeCompare(nameB);
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
const filteredTotal = skills.length;
|
|
1720
|
+
const totalPages = Math.ceil(filteredTotal / limit) || 1;
|
|
1721
|
+
const start = (page - 1) * limit;
|
|
1722
|
+
const pagedSkills = skills.slice(start, start + limit);
|
|
1723
|
+
jsonResponse(res, 200, { skills: pagedSkills, page, totalPages, total: filteredTotal, fetchErrors });
|
|
1724
|
+
} catch (err) {
|
|
1725
|
+
jsonResponse(res, 500, {
|
|
1726
|
+
error: String(err),
|
|
1727
|
+
skills: [],
|
|
1728
|
+
page: 1,
|
|
1729
|
+
totalPages: 1,
|
|
1730
|
+
total: 0
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
API_ROUTES.GET["/api/installed"] = async (_req, res, _url) => {
|
|
1735
|
+
try {
|
|
1736
|
+
const skills = await getInstalledSkills();
|
|
1737
|
+
jsonResponse(res, 200, skills.map((s) => ({
|
|
1738
|
+
id: s.id,
|
|
1739
|
+
displayName: s.id,
|
|
1740
|
+
version: s.version,
|
|
1741
|
+
installedAt: s.installedAt,
|
|
1742
|
+
platforms: s.platforms
|
|
1743
|
+
})));
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
API_ROUTES.GET["/api/platforms"] = async (_req, res, _url) => {
|
|
1749
|
+
try {
|
|
1750
|
+
const available = await detectPlatforms();
|
|
1751
|
+
const allAdapters = getAllAdapters();
|
|
1752
|
+
const platforms = await Promise.all(
|
|
1753
|
+
allAdapters.map(async (adapter) => {
|
|
1754
|
+
const isAvailable = available.find((a) => a.id === adapter.id);
|
|
1755
|
+
const installed = await adapter.listInstalled();
|
|
1756
|
+
return {
|
|
1757
|
+
id: adapter.id,
|
|
1758
|
+
name: adapter.name,
|
|
1759
|
+
available: !!isAvailable,
|
|
1760
|
+
installedCount: Array.isArray(installed) ? installed.length : 0,
|
|
1761
|
+
installedSkills: Array.isArray(installed) ? installed : []
|
|
1762
|
+
};
|
|
1763
|
+
})
|
|
1764
|
+
);
|
|
1765
|
+
jsonResponse(res, 200, platforms);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
API_ROUTES.GET["/api/platform-info"] = async (_req, res, url) => {
|
|
1771
|
+
try {
|
|
1772
|
+
const platformId = url.searchParams.get("id") || "";
|
|
1773
|
+
if (!platformId) {
|
|
1774
|
+
jsonResponse(res, 400, { error: 'Missing "id" query parameter' });
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const allAdapters = getAllAdapters();
|
|
1778
|
+
const adapter = allAdapters.find((a) => a.id === platformId);
|
|
1779
|
+
if (!adapter) {
|
|
1780
|
+
jsonResponse(res, 404, { error: `Platform "${platformId}" not found` });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const available = await detectPlatforms();
|
|
1784
|
+
const isAvailable = available.find((a) => a.id === adapter.id);
|
|
1785
|
+
const installed = await adapter.listInstalled();
|
|
1786
|
+
const installedSkills = Array.isArray(installed) ? installed : [];
|
|
1787
|
+
jsonResponse(res, 200, {
|
|
1788
|
+
id: adapter.id,
|
|
1789
|
+
name: adapter.name,
|
|
1790
|
+
available: !!isAvailable,
|
|
1791
|
+
installedCount: installedSkills.length,
|
|
1792
|
+
installedSkills
|
|
1793
|
+
});
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
|
|
1799
|
+
try {
|
|
1800
|
+
const skillName = url.searchParams.get("skill") || "";
|
|
1801
|
+
if (!skillName) {
|
|
1802
|
+
jsonResponse(res, 400, { error: 'Missing "skill" query parameter' });
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
const cacheKey = `skill-info:${skillName}`;
|
|
1806
|
+
let info = getCached2(cacheKey);
|
|
1807
|
+
if (!info) {
|
|
1808
|
+
info = await fetchSkillPackage(skillName);
|
|
1809
|
+
if (info) setCache2(cacheKey, info, 3e4);
|
|
1810
|
+
}
|
|
1811
|
+
if (!info) {
|
|
1812
|
+
jsonResponse(res, 404, { error: `Skill "${skillName}" not found` });
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const latestVersion = info["dist-tags"]?.latest || "unknown";
|
|
1816
|
+
const pkg = latestVersion ? info.versions?.[latestVersion] : void 0;
|
|
1817
|
+
const meta = pkg?.skillmarket;
|
|
1818
|
+
const versionKeys = Object.keys(info.versions || {});
|
|
1819
|
+
const recentVersions = versionKeys.slice(-20);
|
|
1820
|
+
jsonResponse(res, 200, {
|
|
1821
|
+
id: meta?.id || info.name.replace(/^@[^/]+\//, ""),
|
|
1822
|
+
name: info.name,
|
|
1823
|
+
displayName: meta?.displayName || info.name,
|
|
1824
|
+
description: pkg?.description || "",
|
|
1825
|
+
version: latestVersion,
|
|
1826
|
+
platforms: meta?.platforms || [],
|
|
1827
|
+
versions: recentVersions,
|
|
1828
|
+
author: info.author?.name || pkg?.author?.name || "",
|
|
1829
|
+
license: info.license || "",
|
|
1830
|
+
homepage: pkg?.homepage || "",
|
|
1831
|
+
repository: pkg?.repository?.url || "",
|
|
1832
|
+
readme: info.readme || ""
|
|
1833
|
+
});
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
API_ROUTES.GET["/api/version"] = async (_req, res, _url) => {
|
|
1839
|
+
try {
|
|
1840
|
+
const pkgPath = join3(__dirname, "..", "package.json");
|
|
1841
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
1842
|
+
jsonResponse(res, 200, { version: pkg.version || "1.0.0" });
|
|
1843
|
+
} catch {
|
|
1844
|
+
jsonResponse(res, 200, { version: "1.0.0" });
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
API_ROUTES.GET["/api/config"] = async (_req, res, _url) => {
|
|
1848
|
+
jsonResponse(res, 200, {
|
|
1849
|
+
npmScope: NPM_SCOPE,
|
|
1850
|
+
npmScopeFallback: NPM_SCOPE_FALLBACK,
|
|
1851
|
+
npmRegistry: NPM_REGISTRY,
|
|
1852
|
+
skmUrl: SKM_URL,
|
|
1853
|
+
skillScopes: SKILL_SCOPES
|
|
1854
|
+
});
|
|
1855
|
+
};
|
|
1856
|
+
API_ROUTES.POST["/api/install"] = async (req, res, _url) => {
|
|
1857
|
+
try {
|
|
1858
|
+
const body = await parseBody(req);
|
|
1859
|
+
const skillId = String(body.skillId || "");
|
|
1860
|
+
const version = body.version ? String(body.version) : void 0;
|
|
1861
|
+
const platform = body.platform ? String(body.platform) : void 0;
|
|
1862
|
+
if (!skillId) {
|
|
1863
|
+
jsonResponse(res, 400, { error: "Missing skillId" });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
await installSkill(skillId, version, {
|
|
1867
|
+
platforms: platform ? [platform] : void 0,
|
|
1868
|
+
force: true
|
|
1869
|
+
});
|
|
1870
|
+
jsonResponse(res, 200, { success: true, message: `${skillId} installed successfully` });
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
API_ROUTES.POST["/api/uninstall"] = async (req, res, _url) => {
|
|
1876
|
+
try {
|
|
1877
|
+
const body = await parseBody(req);
|
|
1878
|
+
const skillId = String(body.skillId || "");
|
|
1879
|
+
const platform = body.platform ? String(body.platform) : void 0;
|
|
1880
|
+
if (!skillId) {
|
|
1881
|
+
jsonResponse(res, 400, { error: "Missing skillId" });
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
await uninstallSkill(skillId, {
|
|
1885
|
+
platforms: platform ? [platform] : void 0,
|
|
1886
|
+
yes: true
|
|
1887
|
+
});
|
|
1888
|
+
jsonResponse(res, 200, { success: true, message: `${skillId} uninstalled successfully` });
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
API_ROUTES.GET["/api/admin/stats"] = async (_req, res, _url) => {
|
|
1894
|
+
try {
|
|
1895
|
+
const packages = await fetchScopePackages();
|
|
1896
|
+
const infos = (await Promise.all(
|
|
1897
|
+
packages.map(async (pkg) => {
|
|
1898
|
+
try {
|
|
1899
|
+
const info = await fetchNpmPackage(pkg);
|
|
1900
|
+
return info ? { name: pkg, info } : null;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
})
|
|
1905
|
+
)).filter(Boolean);
|
|
1906
|
+
let totalVersions = 0;
|
|
1907
|
+
let totalSize = 0;
|
|
1908
|
+
const platformSet = /* @__PURE__ */ new Set();
|
|
1909
|
+
let withMetadata = 0;
|
|
1910
|
+
for (const { info } of infos) {
|
|
1911
|
+
const versions = Object.keys(info.versions || {});
|
|
1912
|
+
totalVersions += versions.length;
|
|
1913
|
+
const latestVer = info["dist-tags"]?.latest;
|
|
1914
|
+
const latestPkg = latestVer ? info.versions?.[latestVer] : void 0;
|
|
1915
|
+
const meta = latestPkg?.skillmarket;
|
|
1916
|
+
if (meta) {
|
|
1917
|
+
withMetadata++;
|
|
1918
|
+
if (meta.platforms) meta.platforms.forEach((p) => platformSet.add(p));
|
|
1919
|
+
}
|
|
1920
|
+
if (latestPkg?.dist?.unpackedSize) totalSize += latestPkg.dist.unpackedSize;
|
|
1921
|
+
}
|
|
1922
|
+
jsonResponse(res, 200, {
|
|
1923
|
+
totalSkills: infos.length,
|
|
1924
|
+
totalVersions,
|
|
1925
|
+
averageVersions: infos.length > 0 ? (totalVersions / infos.length).toFixed(1) : "0",
|
|
1926
|
+
withMetadata,
|
|
1927
|
+
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
|
1928
|
+
platformCount: platformSet.size,
|
|
1929
|
+
platforms: [...platformSet]
|
|
1930
|
+
});
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
API_ROUTES.POST["/api/admin/deprecate"] = async (req, res, _url) => {
|
|
1936
|
+
try {
|
|
1937
|
+
const body = await parseBody(req);
|
|
1938
|
+
const skillId = String(body.skillId || "");
|
|
1939
|
+
const version = body.version ? String(body.version) : "";
|
|
1940
|
+
const message = body.message ? String(body.message) : "";
|
|
1941
|
+
if (!skillId) {
|
|
1942
|
+
jsonResponse(res, 400, { error: "Missing skillId" });
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1946
|
+
const target = version ? `${pkgName}@${version}` : pkgName;
|
|
1947
|
+
const deprecateMsg = message || "This skill is deprecated. Please use an alternative.";
|
|
1948
|
+
npmExec(`npm deprecate "${target}" "${deprecateMsg}"`);
|
|
1949
|
+
jsonResponse(res, 200, { success: true, message: `${target} deprecated` });
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
API_ROUTES.POST["/api/admin/unpublish"] = async (req, res, _url) => {
|
|
1955
|
+
try {
|
|
1956
|
+
const body = await parseBody(req);
|
|
1957
|
+
const skillId = String(body.skillId || "");
|
|
1958
|
+
const version = body.version ? String(body.version) : "";
|
|
1959
|
+
const force = !!body.force;
|
|
1960
|
+
if (!skillId) {
|
|
1961
|
+
jsonResponse(res, 400, { error: "Missing skillId" });
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1965
|
+
let target;
|
|
1966
|
+
if (version) {
|
|
1967
|
+
target = `${pkgName}@${version}`;
|
|
1968
|
+
} else {
|
|
1969
|
+
if (!force) {
|
|
1970
|
+
jsonResponse(res, 400, { error: "Unpublishing entire package requires force=true" });
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
target = pkgName;
|
|
1974
|
+
}
|
|
1975
|
+
const forceFlag = force ? " --force" : "";
|
|
1976
|
+
npmExec(`npm unpublish "${target}"${forceFlag}`);
|
|
1977
|
+
jsonResponse(res, 200, { success: true, message: `${target} unpublished` });
|
|
1978
|
+
} catch (err) {
|
|
1979
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
API_ROUTES.POST["/api/admin/tag"] = async (req, res, _url) => {
|
|
1983
|
+
try {
|
|
1984
|
+
const body = await parseBody(req);
|
|
1985
|
+
const skillId = String(body.skillId || "");
|
|
1986
|
+
const action = String(body.action || "");
|
|
1987
|
+
const tag = body.tag ? String(body.tag) : "";
|
|
1988
|
+
const version = body.version ? String(body.version) : "";
|
|
1989
|
+
if (!skillId || !action) {
|
|
1990
|
+
jsonResponse(res, 400, { error: "Missing skillId or action" });
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
1994
|
+
if (action === "set") {
|
|
1995
|
+
if (!tag || !version) {
|
|
1996
|
+
jsonResponse(res, 400, { error: "Tag set requires tag and version" });
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
npmExec(`npm dist-tag add "${pkgName}@${version}" "${tag}"`);
|
|
2000
|
+
jsonResponse(res, 200, { success: true, message: `Tag "${tag}" set to ${pkgName}@${version}` });
|
|
2001
|
+
} else if (action === "rm") {
|
|
2002
|
+
if (!tag) {
|
|
2003
|
+
jsonResponse(res, 400, { error: "Tag rm requires tag" });
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
npmExec(`npm dist-tag rm "${pkgName}" "${tag}"`);
|
|
2007
|
+
jsonResponse(res, 200, { success: true, message: `Tag "${tag}" removed from ${pkgName}` });
|
|
2008
|
+
} else if (action === "ls") {
|
|
2009
|
+
const output = npmExec(`npm dist-tag ls "${pkgName}"`);
|
|
2010
|
+
const tags = {};
|
|
2011
|
+
if (output) {
|
|
2012
|
+
output.split("\n").filter(Boolean).forEach((line) => {
|
|
2013
|
+
const parts = line.split(": ");
|
|
2014
|
+
if (parts.length >= 2) tags[parts[0].trim()] = parts[1].trim();
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
jsonResponse(res, 200, { success: true, tags, packageName: pkgName });
|
|
2018
|
+
} else {
|
|
2019
|
+
jsonResponse(res, 400, { error: `Unknown action: ${action} (use set/rm/ls)` });
|
|
2020
|
+
}
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
API_ROUTES.POST["/api/admin/owner"] = async (req, res, _url) => {
|
|
2026
|
+
try {
|
|
2027
|
+
const body = await parseBody(req);
|
|
2028
|
+
const skillId = String(body.skillId || "");
|
|
2029
|
+
const action = String(body.action || "");
|
|
2030
|
+
const user = String(body.user || "");
|
|
2031
|
+
if (!skillId || !action || !user) {
|
|
2032
|
+
jsonResponse(res, 400, { error: "Missing skillId, action, or user" });
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
2036
|
+
const npmAction = action === "add" ? "add" : "rm";
|
|
2037
|
+
npmExec(`npm owner ${npmAction} "${user}" "${pkgName}"`);
|
|
2038
|
+
jsonResponse(res, 200, {
|
|
2039
|
+
success: true,
|
|
2040
|
+
message: `Owner ${npmAction === "add" ? "added" : "removed"}: ${user} ${npmAction === "add" ? "to" : "from"} ${pkgName}`
|
|
2041
|
+
});
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
API_ROUTES.POST["/api/admin/access"] = async (req, res, _url) => {
|
|
2047
|
+
try {
|
|
2048
|
+
const body = await parseBody(req);
|
|
2049
|
+
const skillId = String(body.skillId || "");
|
|
2050
|
+
const level = String(body.level || "");
|
|
2051
|
+
if (!skillId || !level) {
|
|
2052
|
+
jsonResponse(res, 400, { error: "Missing skillId or level" });
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
if (level !== "public" && level !== "restricted") {
|
|
2056
|
+
jsonResponse(res, 400, { error: 'Level must be "public" or "restricted"' });
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const pkgName = await resolveFullPackageName(skillId);
|
|
2060
|
+
npmExec(`npm access "${level}" "${pkgName}"`);
|
|
2061
|
+
jsonResponse(res, 200, { success: true, message: `Access for ${pkgName} set to "${level}"` });
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
API_ROUTES.POST["/api/update"] = async (req, res, _url) => {
|
|
2067
|
+
try {
|
|
2068
|
+
const body = await parseBody(req);
|
|
2069
|
+
const skillId = body.skillId ? String(body.skillId) : void 0;
|
|
2070
|
+
await updateSkill(skillId);
|
|
2071
|
+
const msg = skillId ? `${skillId} updated successfully` : "All skills updated successfully";
|
|
2072
|
+
jsonResponse(res, 200, { success: true, message: msg });
|
|
2073
|
+
} catch (err) {
|
|
2074
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
var PROJECT_ROOT = join3(__dirname, "..");
|
|
2078
|
+
API_ROUTES.POST["/api/upload"] = async (req, res, _url) => {
|
|
2079
|
+
try {
|
|
2080
|
+
const body = await parseBody(req);
|
|
2081
|
+
const fileData = String(body.fileData || "");
|
|
2082
|
+
const fileName = String(body.fileName || "upload.zip");
|
|
2083
|
+
const skillNameOverride = body.skillNameOverride ? String(body.skillNameOverride).trim() : "";
|
|
2084
|
+
if (!fileData) {
|
|
2085
|
+
jsonResponse(res, 400, { error: "Missing fileData" });
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const buffer = Buffer.from(fileData, "base64");
|
|
2089
|
+
if (buffer.length === 0) {
|
|
2090
|
+
jsonResponse(res, 400, { error: "Empty file data" });
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
const zip = new AdmZip(buffer);
|
|
2094
|
+
const entries = zip.getEntries();
|
|
2095
|
+
if (entries.length === 0) {
|
|
2096
|
+
jsonResponse(res, 400, { error: "ZIP archive is empty" });
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const normalizeEntryName = (name) => name.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2100
|
+
const pkgEntry = entries.find((e) => {
|
|
2101
|
+
const normalized = normalizeEntryName(e.entryName);
|
|
2102
|
+
return normalized === "package.json" || normalized.endsWith("/package.json");
|
|
2103
|
+
});
|
|
2104
|
+
let skillName = "";
|
|
2105
|
+
let pkgInfo = {};
|
|
2106
|
+
if (pkgEntry) {
|
|
2107
|
+
try {
|
|
2108
|
+
pkgInfo = JSON.parse(pkgEntry.getData().toString("utf-8"));
|
|
2109
|
+
skillName = pkgInfo.skillmarket?.id || pkgInfo.name?.replace(/^@[^/]+\//, "") || "";
|
|
2110
|
+
} catch {
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (skillNameOverride) {
|
|
2114
|
+
skillName = skillNameOverride;
|
|
2115
|
+
}
|
|
2116
|
+
if (!skillName) {
|
|
2117
|
+
const rootDirs = [...new Set(entries.map((e) => e.entryName.split("/")[0]))].filter(Boolean);
|
|
2118
|
+
skillName = rootDirs.length === 1 ? rootDirs[0] : basename(fileName, ".zip");
|
|
2119
|
+
}
|
|
2120
|
+
skillName = skillName.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
|
|
2121
|
+
if (!skillName) skillName = "untitled-skill";
|
|
2122
|
+
const skillDir = join3(PROJECT_ROOT, "skills", skillName);
|
|
2123
|
+
if (existsSync3(skillDir)) {
|
|
2124
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
2125
|
+
}
|
|
2126
|
+
mkdirSync(skillDir, { recursive: true });
|
|
2127
|
+
zip.extractAllTo(skillDir, true);
|
|
2128
|
+
const extractedItems = readdirSync(skillDir, { withFileTypes: true });
|
|
2129
|
+
const subDirs = extractedItems.filter((i) => i.isDirectory());
|
|
2130
|
+
const files = extractedItems.filter((i) => !i.isDirectory());
|
|
2131
|
+
if (subDirs.length === 1 && files.length === 0) {
|
|
2132
|
+
const subDirPath = join3(skillDir, subDirs[0].name);
|
|
2133
|
+
const subItems = readdirSync(subDirPath, { withFileTypes: true });
|
|
2134
|
+
for (const item of subItems) {
|
|
2135
|
+
renameSync(join3(subDirPath, item.name), join3(skillDir, item.name));
|
|
2136
|
+
}
|
|
2137
|
+
rmSync(subDirPath, { recursive: true, force: true });
|
|
2138
|
+
}
|
|
2139
|
+
const skillMdPath = findFileSync(skillDir, "SKILL.md");
|
|
2140
|
+
const skillMdExists = skillMdPath !== null;
|
|
2141
|
+
const pkgJsonPath = findFileSync(skillDir, "package.json") || join3(skillDir, "package.json");
|
|
2142
|
+
if (existsSync3(pkgJsonPath)) {
|
|
2143
|
+
try {
|
|
2144
|
+
pkgInfo = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
|
|
2145
|
+
} catch {
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const meta = pkgInfo?.skillmarket || {};
|
|
2149
|
+
const result = {
|
|
2150
|
+
skillName,
|
|
2151
|
+
displayName: meta.displayName || pkgInfo.displayName || skillName,
|
|
2152
|
+
version: pkgInfo.version || "0.0.0",
|
|
2153
|
+
description: pkgInfo.description || meta.description || "",
|
|
2154
|
+
platforms: meta.platforms || [],
|
|
2155
|
+
hasPackageJson: existsSync3(pkgJsonPath),
|
|
2156
|
+
hasSkillMd: skillMdExists,
|
|
2157
|
+
fileCount: entries.length
|
|
2158
|
+
};
|
|
2159
|
+
jsonResponse(res, 200, result);
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
API_ROUTES.POST["/api/upload/action"] = async (req, res, _url) => {
|
|
2165
|
+
try {
|
|
2166
|
+
const body = await parseBody(req);
|
|
2167
|
+
const skillName = String(body.skillName || "");
|
|
2168
|
+
const action = String(body.action || "");
|
|
2169
|
+
if (!skillName) {
|
|
2170
|
+
jsonResponse(res, 400, { error: "Missing skillName" });
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
if (!["publish", "install", "both"].includes(action)) {
|
|
2174
|
+
jsonResponse(res, 400, { error: 'action must be "publish", "install", or "both"' });
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const skillDir = join3(PROJECT_ROOT, "skills", skillName);
|
|
2178
|
+
if (!existsSync3(skillDir)) {
|
|
2179
|
+
jsonResponse(res, 404, { error: `Skill "${skillName}" not found in skills/ directory. Upload first.` });
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const results = {};
|
|
2183
|
+
if (action === "publish" || action === "both") {
|
|
2184
|
+
try {
|
|
2185
|
+
await publishSkill(skillName);
|
|
2186
|
+
results.publish = { success: true, message: `${skillName} published to npm` };
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
results.publish = { success: false, message: String(err) };
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (action === "install" || action === "both") {
|
|
2192
|
+
try {
|
|
2193
|
+
await installSkill(skillName, void 0, { force: true });
|
|
2194
|
+
results.install = { success: true, message: `${skillName} installed locally` };
|
|
2195
|
+
} catch (err) {
|
|
2196
|
+
results.install = { success: false, message: String(err) };
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
jsonResponse(res, 200, { success: true, skillName, action, results });
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
function findFileSync(dir, filename) {
|
|
2205
|
+
try {
|
|
2206
|
+
const items = readdirSync(dir, { withFileTypes: true });
|
|
2207
|
+
for (const item of items) {
|
|
2208
|
+
const fullPath = join3(dir, item.name);
|
|
2209
|
+
if (item.isDirectory()) {
|
|
2210
|
+
const found = findFileSync(fullPath, filename);
|
|
2211
|
+
if (found) return found;
|
|
2212
|
+
} else if (item.name === filename) {
|
|
2213
|
+
return fullPath;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
function serveStaticFile(res, filePath) {
|
|
2221
|
+
if (!existsSync3(filePath)) {
|
|
2222
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2223
|
+
res.end("Not Found");
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const content = readFileSync3(filePath);
|
|
2227
|
+
const ext = extname(filePath);
|
|
2228
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
2229
|
+
res.writeHead(200, {
|
|
2230
|
+
"Content-Type": mime,
|
|
2231
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
2232
|
+
"Pragma": "no-cache",
|
|
2233
|
+
"Expires": "0"
|
|
2234
|
+
});
|
|
2235
|
+
res.end(content);
|
|
2236
|
+
}
|
|
2237
|
+
async function handleRequest(req, res) {
|
|
2238
|
+
const method = req.method || "GET";
|
|
2239
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
2240
|
+
const pathname = url.pathname;
|
|
2241
|
+
const routeHandler = API_ROUTES[method]?.[pathname];
|
|
2242
|
+
if (routeHandler) {
|
|
2243
|
+
try {
|
|
2244
|
+
await routeHandler(req, res, url);
|
|
2245
|
+
} catch (err) {
|
|
2246
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2247
|
+
}
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
if (pathname.startsWith("/api/")) {
|
|
2251
|
+
jsonResponse(res, 404, { error: `Unknown API endpoint: ${method} ${pathname}` });
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const filePath = join3(guiDir, pathname === "/" ? "index.html" : pathname);
|
|
2255
|
+
if (filePath.startsWith(guiDir)) {
|
|
2256
|
+
serveStaticFile(res, filePath);
|
|
2257
|
+
} else {
|
|
2258
|
+
res.writeHead(403);
|
|
2259
|
+
res.end("Forbidden");
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function startGuiServer(port = 18770) {
|
|
2263
|
+
const server = createServer(handleRequest);
|
|
2264
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2265
|
+
console.log(`
|
|
2266
|
+
\u{1F680} SkillMarket GUI started!`);
|
|
2267
|
+
console.log(` Local: http://localhost:${port}`);
|
|
2268
|
+
console.log(`
|
|
2269
|
+
Press Ctrl+C to stop
|
|
2270
|
+
`);
|
|
2271
|
+
});
|
|
2272
|
+
server.on("error", (err) => {
|
|
2273
|
+
if (err.code === "EADDRINUSE") {
|
|
2274
|
+
console.error(`\u274C Port ${port} is already in use. Try: skm gui ${port + 1}`);
|
|
2275
|
+
} else {
|
|
2276
|
+
console.error("\u274C Failed to start GUI server:", err.message);
|
|
2277
|
+
}
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
});
|
|
2280
|
+
return server;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
export {
|
|
2284
|
+
fetchNpmPackage,
|
|
2285
|
+
fetchSkillPackage,
|
|
2286
|
+
searchSkillmarketPackages,
|
|
2287
|
+
PLATFORMS,
|
|
2288
|
+
LATEST_LINK,
|
|
2289
|
+
getSkillsDir,
|
|
2290
|
+
getPlatformLinksDir,
|
|
2291
|
+
ensureMarketDirs,
|
|
2292
|
+
loadRegistry,
|
|
2293
|
+
saveRegistry,
|
|
2294
|
+
getInstalledSkills,
|
|
2295
|
+
isSkillInstalled,
|
|
2296
|
+
detectPlatforms,
|
|
2297
|
+
getAllAdapters,
|
|
2298
|
+
getAdapterByPlatform,
|
|
2299
|
+
installSkill,
|
|
2300
|
+
uninstallSkill,
|
|
2301
|
+
uninstallAll,
|
|
2302
|
+
updateSkill,
|
|
2303
|
+
publishSkill,
|
|
2304
|
+
adminList,
|
|
2305
|
+
adminInfo,
|
|
2306
|
+
adminSearch,
|
|
2307
|
+
adminStats,
|
|
2308
|
+
adminVerify,
|
|
2309
|
+
adminDeprecate,
|
|
2310
|
+
adminUnpublish,
|
|
2311
|
+
adminTagSet,
|
|
2312
|
+
adminTagRemove,
|
|
2313
|
+
adminTagList,
|
|
2314
|
+
adminOwnerAdd,
|
|
2315
|
+
adminOwnerRemove,
|
|
2316
|
+
adminAccess,
|
|
2317
|
+
startGuiServer
|
|
2318
|
+
};
|