wittyhub 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -97
- package/dist/_chunks/libs/simple-git.mjs +1 -1
- package/dist/cli.mjs +1370 -593
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11,14 +11,19 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
11
11
|
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { promisify, stripVTControlCharacters } from "node:util";
|
|
16
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { existsSync as existsSync$1, statSync } from "node:fs";
|
|
18
|
+
import { homedir, tmpdir } from "node:os";
|
|
19
|
+
import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1, sep as sep$1 } from "node:path";
|
|
20
|
+
import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
|
|
21
|
+
import { parse } from "yaml";
|
|
16
22
|
import * as readline from "readline";
|
|
17
23
|
import { Writable } from "stream";
|
|
18
|
-
import { promisify } from "util";
|
|
19
|
-
import { execFile, execSync, spawn, spawnSync } from "child_process";
|
|
20
|
-
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
21
|
-
import { parse } from "yaml";
|
|
24
|
+
import { promisify as promisify$1 } from "util";
|
|
25
|
+
import { execFile as execFile$1, execSync, spawn, spawnSync } from "child_process";
|
|
26
|
+
import { access, cp, lstat, mkdir, mkdtemp as mkdtemp$1, readFile, readdir, readlink, realpath, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
|
|
22
27
|
import { createHash as createHash$1 } from "crypto";
|
|
23
28
|
import { gunzipSync, inflateRawSync } from "node:zlib";
|
|
24
29
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
@@ -55,15 +60,6 @@ function parseOwnerRepo(ownerRepo) {
|
|
|
55
60
|
};
|
|
56
61
|
return null;
|
|
57
62
|
}
|
|
58
|
-
async function isRepoPrivate(owner, repo) {
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
|
61
|
-
if (!res.ok) return null;
|
|
62
|
-
return (await res.json()).private === true;
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
63
|
function sanitizeSubpath(subpath) {
|
|
68
64
|
const segments = subpath.replace(/\\/g, "/").split("/");
|
|
69
65
|
for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
|
|
@@ -251,6 +247,156 @@ function isWellKnownUrl(input) {
|
|
|
251
247
|
return false;
|
|
252
248
|
}
|
|
253
249
|
}
|
|
250
|
+
const CLI_CONFIG_PATH = join(homedir$1(), ".config", "wittyhub", "cli.yaml");
|
|
251
|
+
function loadCliConfig() {
|
|
252
|
+
if (!existsSync(CLI_CONFIG_PATH)) return {};
|
|
253
|
+
try {
|
|
254
|
+
const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
|
|
255
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
256
|
+
return parsed;
|
|
257
|
+
} catch {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const cliConfig = loadCliConfig();
|
|
262
|
+
const API_URL = typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim().replace(/\/$/, "") : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : "https://skillhub.openeuler.org/api/v1";
|
|
263
|
+
const SEARCH_URL = `${API_URL}/index/search`;
|
|
264
|
+
const TELEMETRY_URL = `${API_URL}/skills/telemetry`;
|
|
265
|
+
const AUDIT_URL = `${API_URL}/skills/{skill_id}/audit`;
|
|
266
|
+
const GET_URL = `${API_URL}/skills/{skill_id}`;
|
|
267
|
+
const LIST_URL = `${API_URL}/skills/`;
|
|
268
|
+
const DOWNLOAD_URL = `${API_URL}/skills/{skill_id}/download`;
|
|
269
|
+
let cliVersion = null;
|
|
270
|
+
let detectedAgentName = null;
|
|
271
|
+
function setDetectedAgent(agentName) {
|
|
272
|
+
detectedAgentName = agentName;
|
|
273
|
+
}
|
|
274
|
+
function isCI() {
|
|
275
|
+
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
276
|
+
}
|
|
277
|
+
function isEnabled() {
|
|
278
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
279
|
+
}
|
|
280
|
+
function setVersion(version) {
|
|
281
|
+
cliVersion = version;
|
|
282
|
+
}
|
|
283
|
+
function slugifyTelemetryValue(value) {
|
|
284
|
+
const lowered = value.trim().toLowerCase();
|
|
285
|
+
if (!lowered) return "";
|
|
286
|
+
let normalized = lowered.replace(/[^a-z0-9._-]+/g, "-");
|
|
287
|
+
normalized = normalized.replace(/-{2,}/g, "-");
|
|
288
|
+
return normalized.replace(/^-|-$/g, "");
|
|
289
|
+
}
|
|
290
|
+
async function fetchAuditBySkillId(skillId, timeoutMs = 15e3) {
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(AUDIT_URL.replace("{skill_id}", skillId), { signal: controller.signal });
|
|
295
|
+
if (response.ok) {
|
|
296
|
+
const data = await response.json();
|
|
297
|
+
if (!data.error) {
|
|
298
|
+
const auditData = data.data || data;
|
|
299
|
+
return { [skillId.split("/").pop() || skillId]: {
|
|
300
|
+
risk_level: auditData.risk_level ?? "unknown",
|
|
301
|
+
risk_score: auditData.risk_score ?? null,
|
|
302
|
+
risk_signals: auditData.risk_signals ?? [],
|
|
303
|
+
audited_at: auditData.audited_at ?? null
|
|
304
|
+
} };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
} finally {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const pendingTelemetry = [];
|
|
315
|
+
function track(data) {
|
|
316
|
+
if (!isEnabled()) return;
|
|
317
|
+
try {
|
|
318
|
+
const params = new URLSearchParams();
|
|
319
|
+
if (cliVersion) params.set("v", cliVersion);
|
|
320
|
+
if (isCI()) params.set("ci", "1");
|
|
321
|
+
if (detectedAgentName) params.set("agent", detectedAgentName);
|
|
322
|
+
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
323
|
+
const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {});
|
|
324
|
+
pendingTelemetry.push(p);
|
|
325
|
+
} catch {}
|
|
326
|
+
}
|
|
327
|
+
async function flushTelemetry(timeoutMs = 5e3) {
|
|
328
|
+
if (pendingTelemetry.length === 0) return;
|
|
329
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
330
|
+
await Promise.race([Promise.all(pendingTelemetry), timeout]);
|
|
331
|
+
}
|
|
332
|
+
const SOURCE_TYPE_PREFIXES = [
|
|
333
|
+
"github",
|
|
334
|
+
"gitcode",
|
|
335
|
+
"gitlab",
|
|
336
|
+
"gitee"
|
|
337
|
+
];
|
|
338
|
+
function normalizeSkillId(input) {
|
|
339
|
+
const trimmed = input.trim();
|
|
340
|
+
const colonIdx = trimmed.indexOf(":");
|
|
341
|
+
if (colonIdx > 0) {
|
|
342
|
+
const prefix = trimmed.slice(0, colonIdx).toLowerCase();
|
|
343
|
+
if (SOURCE_TYPE_PREFIXES.includes(prefix)) return trimmed;
|
|
344
|
+
}
|
|
345
|
+
return `github:${trimmed}`;
|
|
346
|
+
}
|
|
347
|
+
function isRepoLevelRef(input) {
|
|
348
|
+
const trimmed = input.trim();
|
|
349
|
+
const colonIdx = trimmed.indexOf(":");
|
|
350
|
+
if (colonIdx <= 0) return false;
|
|
351
|
+
const prefix = trimmed.slice(0, colonIdx).toLowerCase();
|
|
352
|
+
if (!SOURCE_TYPE_PREFIXES.includes(prefix)) return false;
|
|
353
|
+
return trimmed.slice(colonIdx + 1).split("/").length === 2;
|
|
354
|
+
}
|
|
355
|
+
const HOST_TO_SOURCE_TYPE = {
|
|
356
|
+
"github.com": "github",
|
|
357
|
+
"gitcode.com": "gitcode",
|
|
358
|
+
"gitlab.com": "gitlab",
|
|
359
|
+
"gitee.com": "gitee"
|
|
360
|
+
};
|
|
361
|
+
function repoUrlToRef(input) {
|
|
362
|
+
const trimmed = input.trim().replace(/\.git$/, "");
|
|
363
|
+
if (!trimmed) return null;
|
|
364
|
+
const scpLike = trimmed.match(/^git@([^:]+):(.+)$/);
|
|
365
|
+
if (scpLike) return refFromHostAndPath(scpLike[1], scpLike[2]);
|
|
366
|
+
const sshLike = trimmed.match(/^ssh:\/\/[^@]+@([^/]+)\/(.+)$/);
|
|
367
|
+
if (sshLike) return refFromHostAndPath(sshLike[1], sshLike[2]);
|
|
368
|
+
const urlLike = trimmed.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)\/(.+)$/i);
|
|
369
|
+
if (urlLike) return refFromHostAndPath(urlLike[1], urlLike[2]);
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function refFromHostAndPath(host, path) {
|
|
373
|
+
const sourceType = HOST_TO_SOURCE_TYPE[host.toLowerCase()];
|
|
374
|
+
if (!sourceType) return null;
|
|
375
|
+
const parts = path.replace(/\/+$/, "").split("/");
|
|
376
|
+
if (parts.length !== 2 || parts.some((p) => !p)) return null;
|
|
377
|
+
return `${sourceType}:${parts.join("/")}`;
|
|
378
|
+
}
|
|
379
|
+
async function listSkillsByRepo(sourceType, ownerRepo) {
|
|
380
|
+
const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
|
|
381
|
+
const url = `${LIST_URL}?${new URLSearchParams({
|
|
382
|
+
source_type: sourceType,
|
|
383
|
+
repo: slugifiedOwnerRepo,
|
|
384
|
+
limit: "100"
|
|
385
|
+
}).toString()}`;
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch(url);
|
|
388
|
+
if (!res.ok) return [];
|
|
389
|
+
const raw = await res.json();
|
|
390
|
+
return ((raw.data || raw).skills ?? []).filter((skill) => skill && typeof skill.skill_id === "string" && skill.skill_id).map((skill) => ({
|
|
391
|
+
skill_id: skill.skill_id,
|
|
392
|
+
name: skill.name ?? "",
|
|
393
|
+
description: skill.description ?? "",
|
|
394
|
+
source_url: skill.source_url ?? ""
|
|
395
|
+
}));
|
|
396
|
+
} catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
254
400
|
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
255
401
|
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
256
402
|
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
@@ -453,7 +599,7 @@ const CLONE_TIMEOUT_MS = (() => {
|
|
|
453
599
|
const parsed = Number.parseInt(raw, 10);
|
|
454
600
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
455
601
|
})();
|
|
456
|
-
const execFileAsync = promisify(execFile);
|
|
602
|
+
const execFileAsync$1 = promisify$1(execFile$1);
|
|
457
603
|
var GitCloneError = class extends Error {
|
|
458
604
|
url;
|
|
459
605
|
isTimeout;
|
|
@@ -513,22 +659,22 @@ function isAuthFailure(message) {
|
|
|
513
659
|
function createGitClient(extraEnv) {
|
|
514
660
|
return esm_default({
|
|
515
661
|
timeout: { block: CLONE_TIMEOUT_MS },
|
|
516
|
-
env: {
|
|
517
|
-
...process.env,
|
|
518
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
519
|
-
GIT_LFS_SKIP_SMUDGE: "1",
|
|
520
|
-
...extraEnv
|
|
521
|
-
},
|
|
522
662
|
config: [
|
|
523
663
|
"filter.lfs.required=false",
|
|
524
664
|
"filter.lfs.smudge=",
|
|
525
665
|
"filter.lfs.clean=",
|
|
526
666
|
"filter.lfs.process="
|
|
527
|
-
]
|
|
667
|
+
],
|
|
668
|
+
unsafe: { allowUnsafeFilter: true }
|
|
669
|
+
}).env({
|
|
670
|
+
...process.env,
|
|
671
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
672
|
+
GIT_LFS_SKIP_SMUDGE: "1",
|
|
673
|
+
...extraEnv
|
|
528
674
|
});
|
|
529
675
|
}
|
|
530
676
|
async function resetTempDir(dir) {
|
|
531
|
-
await rm(dir, {
|
|
677
|
+
await rm$1(dir, {
|
|
532
678
|
recursive: true,
|
|
533
679
|
force: true
|
|
534
680
|
}).catch(() => {});
|
|
@@ -537,7 +683,7 @@ async function resetTempDir(dir) {
|
|
|
537
683
|
async function tryGhClone(repo, tempDir, ref) {
|
|
538
684
|
let cloneTarget = repo.slug;
|
|
539
685
|
try {
|
|
540
|
-
const { stdout, stderr } = await execFileAsync("gh", [
|
|
686
|
+
const { stdout, stderr } = await execFileAsync$1("gh", [
|
|
541
687
|
"auth",
|
|
542
688
|
"status",
|
|
543
689
|
"-h",
|
|
@@ -554,7 +700,7 @@ async function tryGhClone(repo, tempDir, ref) {
|
|
|
554
700
|
} catch {
|
|
555
701
|
return false;
|
|
556
702
|
}
|
|
557
|
-
await execFileAsync("gh", [
|
|
703
|
+
await execFileAsync$1("gh", [
|
|
558
704
|
"repo",
|
|
559
705
|
"clone",
|
|
560
706
|
cloneTarget,
|
|
@@ -580,7 +726,7 @@ function buildGitHubAuthError(url, repo, message) {
|
|
|
580
726
|
return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
|
|
581
727
|
}
|
|
582
728
|
async function cloneRepo(url, ref) {
|
|
583
|
-
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
729
|
+
const tempDir = await mkdtemp$1(join(tmpdir$1(), "skills-"));
|
|
584
730
|
const cloneOptions = ref ? [
|
|
585
731
|
"--depth",
|
|
586
732
|
"1",
|
|
@@ -596,7 +742,7 @@ async function cloneRepo(url, ref) {
|
|
|
596
742
|
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
597
743
|
const isAuthError = isAuthFailure(errorMessage);
|
|
598
744
|
if (isTimeout) {
|
|
599
|
-
await rm(tempDir, {
|
|
745
|
+
await rm$1(tempDir, {
|
|
600
746
|
recursive: true,
|
|
601
747
|
force: true
|
|
602
748
|
}).catch(() => {});
|
|
@@ -613,7 +759,7 @@ async function cloneRepo(url, ref) {
|
|
|
613
759
|
return tempDir;
|
|
614
760
|
} catch {}
|
|
615
761
|
}
|
|
616
|
-
await rm(tempDir, {
|
|
762
|
+
await rm$1(tempDir, {
|
|
617
763
|
recursive: true,
|
|
618
764
|
force: true
|
|
619
765
|
}).catch(() => {});
|
|
@@ -623,9 +769,9 @@ async function cloneRepo(url, ref) {
|
|
|
623
769
|
}
|
|
624
770
|
async function cleanupTempDir(dir) {
|
|
625
771
|
const normalizedDir = normalize(resolve(dir));
|
|
626
|
-
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
772
|
+
const normalizedTmpDir = normalize(resolve(tmpdir$1()));
|
|
627
773
|
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
628
|
-
await rm(dir, {
|
|
774
|
+
await rm$1(dir, {
|
|
629
775
|
recursive: true,
|
|
630
776
|
force: true
|
|
631
777
|
});
|
|
@@ -731,7 +877,7 @@ async function writeLocalLock(lock, cwd) {
|
|
|
731
877
|
version: lock.version,
|
|
732
878
|
skills: sortedSkills
|
|
733
879
|
};
|
|
734
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
880
|
+
await writeFile$1(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
735
881
|
}
|
|
736
882
|
async function computeSkillFolderHash(skillDir) {
|
|
737
883
|
const files = [];
|
|
@@ -827,13 +973,14 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
827
973
|
const { data } = parseFrontmatter(content);
|
|
828
974
|
if (!data.name || !data.description) return null;
|
|
829
975
|
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
830
|
-
|
|
976
|
+
const metadata = typeof data.metadata === "object" && data.metadata !== null ? data.metadata : void 0;
|
|
977
|
+
if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
831
978
|
return {
|
|
832
979
|
name: sanitizeMetadata(data.name),
|
|
833
980
|
description: sanitizeMetadata(data.description),
|
|
834
981
|
path: dirname(skillMdPath),
|
|
835
982
|
rawContent: content,
|
|
836
|
-
metadata
|
|
983
|
+
metadata
|
|
837
984
|
};
|
|
838
985
|
} catch {
|
|
839
986
|
return null;
|
|
@@ -950,7 +1097,7 @@ function filterSkills(skills, inputNames) {
|
|
|
950
1097
|
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
951
1098
|
});
|
|
952
1099
|
}
|
|
953
|
-
const home = homedir();
|
|
1100
|
+
const home = homedir$1();
|
|
954
1101
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
955
1102
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
956
1103
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
@@ -1679,7 +1826,7 @@ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
|
|
|
1679
1826
|
}
|
|
1680
1827
|
}
|
|
1681
1828
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1682
|
-
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1829
|
+
return join(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1683
1830
|
}
|
|
1684
1831
|
function getEveSubagentSkillsDir(subagent, cwd) {
|
|
1685
1832
|
return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
|
|
@@ -1688,7 +1835,7 @@ function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
|
1688
1835
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1689
1836
|
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1690
1837
|
const agent = agents[agentType];
|
|
1691
|
-
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
1838
|
+
const baseDir = global ? homedir$1() : cwd || process.cwd();
|
|
1692
1839
|
if (global) {
|
|
1693
1840
|
if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
|
|
1694
1841
|
return agent.globalSkillsDir;
|
|
@@ -1700,7 +1847,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
|
|
|
1700
1847
|
}
|
|
1701
1848
|
async function cleanAndCreateDirectory(path) {
|
|
1702
1849
|
try {
|
|
1703
|
-
await rm(path, {
|
|
1850
|
+
await rm$1(path, {
|
|
1704
1851
|
recursive: true,
|
|
1705
1852
|
force: true
|
|
1706
1853
|
});
|
|
@@ -1727,11 +1874,11 @@ async function createSymlink(target, linkPath) {
|
|
|
1727
1874
|
try {
|
|
1728
1875
|
if ((await lstat(linkPath)).isSymbolicLink()) {
|
|
1729
1876
|
if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
|
|
1730
|
-
await rm(linkPath);
|
|
1731
|
-
} else await rm(linkPath, { recursive: true });
|
|
1877
|
+
await rm$1(linkPath);
|
|
1878
|
+
} else await rm$1(linkPath, { recursive: true });
|
|
1732
1879
|
} catch (err) {
|
|
1733
1880
|
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
|
|
1734
|
-
await rm(linkPath, { force: true });
|
|
1881
|
+
await rm$1(linkPath, { force: true });
|
|
1735
1882
|
} catch {}
|
|
1736
1883
|
}
|
|
1737
1884
|
const linkDir = dirname(linkPath);
|
|
@@ -1872,7 +2019,7 @@ async function copyDirectory(src, dest, agentType) {
|
|
|
1872
2019
|
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1873
2020
|
else try {
|
|
1874
2021
|
if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
|
|
1875
|
-
await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
2022
|
+
await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
1876
2023
|
return;
|
|
1877
2024
|
}
|
|
1878
2025
|
await cp(srcPath, destPath, {
|
|
@@ -1927,88 +2074,6 @@ function getCanonicalPath(skillName, options = {}) {
|
|
|
1927
2074
|
if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1928
2075
|
return canonicalPath;
|
|
1929
2076
|
}
|
|
1930
|
-
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
1931
|
-
const agent = agents[agentType];
|
|
1932
|
-
const isGlobal = options.global ?? false;
|
|
1933
|
-
const cwd = options.cwd || process.cwd();
|
|
1934
|
-
const installMode = options.mode ?? "symlink";
|
|
1935
|
-
const eveSubagent = options.eveSubagent;
|
|
1936
|
-
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
1937
|
-
success: false,
|
|
1938
|
-
path: "",
|
|
1939
|
-
mode: installMode,
|
|
1940
|
-
error: `${agent.displayName} does not support global skill installation`
|
|
1941
|
-
};
|
|
1942
|
-
const skillName = sanitizeName(skill.installName);
|
|
1943
|
-
const canonicalBase = agentType === "eve" && installMode === "symlink" ? getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent) : getCanonicalSkillsDir(isGlobal, cwd);
|
|
1944
|
-
const canonicalDir = join(canonicalBase, skillName);
|
|
1945
|
-
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd, eveSubagent);
|
|
1946
|
-
const agentDir = join(agentBase, skillName);
|
|
1947
|
-
if (!isPathSafe$1(canonicalBase, canonicalDir)) return {
|
|
1948
|
-
success: false,
|
|
1949
|
-
path: agentDir,
|
|
1950
|
-
mode: installMode,
|
|
1951
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1952
|
-
};
|
|
1953
|
-
if (!isPathSafe$1(agentBase, agentDir)) return {
|
|
1954
|
-
success: false,
|
|
1955
|
-
path: agentDir,
|
|
1956
|
-
mode: installMode,
|
|
1957
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1958
|
-
};
|
|
1959
|
-
async function writeSkillFiles(targetDir) {
|
|
1960
|
-
for (const [filePath, content] of skill.files) {
|
|
1961
|
-
const fullPath = join(targetDir, filePath);
|
|
1962
|
-
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
1963
|
-
const parentDir = dirname(fullPath);
|
|
1964
|
-
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1965
|
-
await writeFile(fullPath, agentType === "eve" && basename(filePath).toLowerCase() === "skill.md" && typeof content === "string" ? stripIgnoredEveFrontmatter(content) : content);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
try {
|
|
1969
|
-
if (installMode === "copy") {
|
|
1970
|
-
await cleanAndCreateDirectory(agentDir);
|
|
1971
|
-
await writeSkillFiles(agentDir);
|
|
1972
|
-
return {
|
|
1973
|
-
success: true,
|
|
1974
|
-
path: agentDir,
|
|
1975
|
-
mode: "copy"
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
await cleanAndCreateDirectory(canonicalDir);
|
|
1979
|
-
await writeSkillFiles(canonicalDir);
|
|
1980
|
-
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1981
|
-
success: true,
|
|
1982
|
-
path: canonicalDir,
|
|
1983
|
-
canonicalPath: canonicalDir,
|
|
1984
|
-
mode: "symlink"
|
|
1985
|
-
};
|
|
1986
|
-
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
1987
|
-
await cleanAndCreateDirectory(agentDir);
|
|
1988
|
-
await writeSkillFiles(agentDir);
|
|
1989
|
-
return {
|
|
1990
|
-
success: true,
|
|
1991
|
-
path: agentDir,
|
|
1992
|
-
canonicalPath: canonicalDir,
|
|
1993
|
-
mode: "symlink",
|
|
1994
|
-
symlinkFailed: true
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
return {
|
|
1998
|
-
success: true,
|
|
1999
|
-
path: agentDir,
|
|
2000
|
-
canonicalPath: canonicalDir,
|
|
2001
|
-
mode: "symlink"
|
|
2002
|
-
};
|
|
2003
|
-
} catch (error) {
|
|
2004
|
-
return {
|
|
2005
|
-
success: false,
|
|
2006
|
-
path: agentDir,
|
|
2007
|
-
mode: installMode,
|
|
2008
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
2077
|
async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
2013
2078
|
const agent = agents[agentType];
|
|
2014
2079
|
const isGlobal = options.global ?? false;
|
|
@@ -2033,11 +2098,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2033
2098
|
};
|
|
2034
2099
|
try {
|
|
2035
2100
|
await mkdir(agentBase, { recursive: true });
|
|
2036
|
-
await rm(flatSkillPath, {
|
|
2101
|
+
await rm$1(flatSkillPath, {
|
|
2037
2102
|
recursive: true,
|
|
2038
2103
|
force: true
|
|
2039
2104
|
});
|
|
2040
|
-
await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
2105
|
+
await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
2041
2106
|
return {
|
|
2042
2107
|
success: true,
|
|
2043
2108
|
path: flatSkillPath,
|
|
@@ -2073,7 +2138,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2073
2138
|
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
2074
2139
|
const parentDir = dirname(fullPath);
|
|
2075
2140
|
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
2076
|
-
await writeFile(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
2141
|
+
await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
2077
2142
|
}
|
|
2078
2143
|
}
|
|
2079
2144
|
try {
|
|
@@ -2260,53 +2325,6 @@ async function listInstalledSkills(options = {}) {
|
|
|
2260
2325
|
} catch {}
|
|
2261
2326
|
return Array.from(skillsMap.values());
|
|
2262
2327
|
}
|
|
2263
|
-
const CLI_CONFIG_PATH = join(homedir(), ".config", "wittyhub", "cli.yaml");
|
|
2264
|
-
function loadCliConfig() {
|
|
2265
|
-
if (!existsSync(CLI_CONFIG_PATH)) return {};
|
|
2266
|
-
try {
|
|
2267
|
-
const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
|
|
2268
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
2269
|
-
return parsed;
|
|
2270
|
-
} catch {
|
|
2271
|
-
return {};
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
const DEFAULT_TELEMETRY_URL = "http://localhost:8080/api/v1/skills/telemetry";
|
|
2275
|
-
const cliConfig = loadCliConfig();
|
|
2276
|
-
const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
|
|
2277
|
-
typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() && cliConfig.audit_url.trim();
|
|
2278
|
-
let cliVersion = null;
|
|
2279
|
-
let detectedAgentName = null;
|
|
2280
|
-
function setDetectedAgent(agentName) {
|
|
2281
|
-
detectedAgentName = agentName;
|
|
2282
|
-
}
|
|
2283
|
-
function isCI() {
|
|
2284
|
-
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
2285
|
-
}
|
|
2286
|
-
function isEnabled() {
|
|
2287
|
-
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
2288
|
-
}
|
|
2289
|
-
function setVersion(version) {
|
|
2290
|
-
cliVersion = version;
|
|
2291
|
-
}
|
|
2292
|
-
const pendingTelemetry = [];
|
|
2293
|
-
function track(data) {
|
|
2294
|
-
if (!isEnabled()) return;
|
|
2295
|
-
try {
|
|
2296
|
-
const params = new URLSearchParams();
|
|
2297
|
-
if (cliVersion) params.set("v", cliVersion);
|
|
2298
|
-
if (isCI()) params.set("ci", "1");
|
|
2299
|
-
if (detectedAgentName) params.set("agent", detectedAgentName);
|
|
2300
|
-
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
2301
|
-
const p = fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {}).then(() => {});
|
|
2302
|
-
pendingTelemetry.push(p);
|
|
2303
|
-
} catch {}
|
|
2304
|
-
}
|
|
2305
|
-
async function flushTelemetry(timeoutMs = 5e3) {
|
|
2306
|
-
if (pendingTelemetry.length === 0) return;
|
|
2307
|
-
const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
2308
|
-
await Promise.race([Promise.all(pendingTelemetry), timeout]);
|
|
2309
|
-
}
|
|
2310
2328
|
var import_dist = require_dist();
|
|
2311
2329
|
let cachedResult = null;
|
|
2312
2330
|
const agentNameToType = {
|
|
@@ -2806,7 +2824,7 @@ const CURRENT_VERSION = 3;
|
|
|
2806
2824
|
function getSkillLockPath() {
|
|
2807
2825
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2808
2826
|
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2809
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2827
|
+
return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
|
|
2810
2828
|
}
|
|
2811
2829
|
async function readSkillLock() {
|
|
2812
2830
|
const lockPath = getSkillLockPath();
|
|
@@ -2823,7 +2841,7 @@ async function readSkillLock() {
|
|
|
2823
2841
|
async function writeSkillLock(lock) {
|
|
2824
2842
|
const lockPath = getSkillLockPath();
|
|
2825
2843
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2826
|
-
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2844
|
+
await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2827
2845
|
}
|
|
2828
2846
|
let _ghWarningShown = false;
|
|
2829
2847
|
function getGitHubToken() {
|
|
@@ -3160,65 +3178,153 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
3160
3178
|
tree
|
|
3161
3179
|
};
|
|
3162
3180
|
}
|
|
3163
|
-
var version$1 = "0.0.
|
|
3181
|
+
var version$1 = "0.0.5";
|
|
3164
3182
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
3165
|
-
async function isSourcePrivate(source) {
|
|
3166
|
-
const ownerRepo = parseOwnerRepo(source);
|
|
3167
|
-
if (!ownerRepo) return false;
|
|
3168
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
|
|
3169
|
-
}
|
|
3170
3183
|
function getLockSource(parsedUrl, normalizedSource) {
|
|
3171
3184
|
return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
|
|
3172
3185
|
}
|
|
3173
3186
|
function initTelemetry(version) {
|
|
3174
3187
|
setVersion(version);
|
|
3175
3188
|
}
|
|
3176
|
-
|
|
3189
|
+
const execFileAsync = promisify(execFile);
|
|
3190
|
+
function normalizeRepoRef(input) {
|
|
3191
|
+
let s = input.trim().toLowerCase();
|
|
3192
|
+
s = s.replace(/^(https?:\/\/|git:\/\/|ssh:\/\/)/, "");
|
|
3193
|
+
s = s.replace(/^git@([^:]+):/, "$1/");
|
|
3194
|
+
s = s.replace(/\.git$/, "");
|
|
3195
|
+
s = s.replace(/\/+$/, "");
|
|
3196
|
+
return s;
|
|
3197
|
+
}
|
|
3198
|
+
function looksLikeLocalPath(source) {
|
|
3199
|
+
const s = source.trim();
|
|
3200
|
+
if (!s) return false;
|
|
3201
|
+
if (isAbsolute$1(s)) return true;
|
|
3202
|
+
if (s.startsWith("./") || s.startsWith("../") || s === "." || s === "..") return true;
|
|
3203
|
+
if (s.startsWith("~/")) return true;
|
|
3204
|
+
if (!s.includes(":") && !s.includes("/") && existsSync$1(resolve$1(process.cwd(), s))) return statSync(resolve$1(process.cwd(), s)).isDirectory();
|
|
3205
|
+
return false;
|
|
3206
|
+
}
|
|
3207
|
+
function expandLocalPath(source) {
|
|
3208
|
+
const s = source.trim();
|
|
3209
|
+
if (s.startsWith("~/")) return join$1(homedir(), s.slice(2));
|
|
3210
|
+
return resolve$1(process.cwd(), s);
|
|
3211
|
+
}
|
|
3212
|
+
function isDirectory(path) {
|
|
3213
|
+
return statSync(path).isDirectory();
|
|
3214
|
+
}
|
|
3215
|
+
function looksLikeRepoSource(source) {
|
|
3216
|
+
const s = source.trim();
|
|
3217
|
+
if (s.startsWith("http://") || s.startsWith("https://")) return true;
|
|
3218
|
+
if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
|
|
3219
|
+
return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
|
|
3220
|
+
}
|
|
3221
|
+
async function resolveSkillId(source, skillName) {
|
|
3222
|
+
if (isRepoLevelRef(source)) {
|
|
3223
|
+
const [sourceType, ownerRepo] = source.split(":", 2);
|
|
3224
|
+
const skills = await listSkillsByRepo(sourceType, ownerRepo);
|
|
3225
|
+
if (skills.length === 0) throw new Error(`仓库 "${source}" 下未找到已收录的技能。`);
|
|
3226
|
+
return skills.map((s) => s.skill_id);
|
|
3227
|
+
}
|
|
3228
|
+
if (!looksLikeRepoSource(source)) return normalizeSkillId(source);
|
|
3229
|
+
if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
|
|
3230
|
+
const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
|
|
3231
|
+
q: skillName.trim(),
|
|
3232
|
+
limit: "20",
|
|
3233
|
+
mode: "text",
|
|
3234
|
+
scope: "summary"
|
|
3235
|
+
}).toString()}`;
|
|
3236
|
+
let res;
|
|
3237
|
+
try {
|
|
3238
|
+
res = await fetch(searchUrl);
|
|
3239
|
+
} catch {
|
|
3240
|
+
throw new Error(`无法连接搜索服务: ${SEARCH_URL}`);
|
|
3241
|
+
}
|
|
3242
|
+
if (!res.ok) throw new Error(`搜索服务返回 HTTP ${res.status}`);
|
|
3243
|
+
const payload = await res.json();
|
|
3244
|
+
const results = payload.data?.results ?? payload.results ?? [];
|
|
3245
|
+
if (results.length === 0) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3246
|
+
const wantedRef = normalizeRepoRef(source);
|
|
3247
|
+
const match = results.find((r) => {
|
|
3248
|
+
const ref = normalizeRepoRef(r.source_url);
|
|
3249
|
+
return ref === wantedRef || ref.startsWith(`${wantedRef}/`);
|
|
3250
|
+
}) ?? results[0];
|
|
3251
|
+
if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3252
|
+
return match.skill_id;
|
|
3253
|
+
}
|
|
3254
|
+
async function downloadAndExtractSkill(skillId, targetDir, version) {
|
|
3255
|
+
let downloadUrl = DOWNLOAD_URL.replace("{skill_id}", skillId);
|
|
3256
|
+
if (version) downloadUrl += `?version=${encodeURIComponent(version)}`;
|
|
3257
|
+
const response = await fetch(downloadUrl);
|
|
3258
|
+
if (!response.ok) {
|
|
3259
|
+
const detail = await response.json().then((data) => typeof data?.detail === "string" && data.detail ? `: ${data.detail}` : "").catch(() => "");
|
|
3260
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}${detail}`);
|
|
3261
|
+
}
|
|
3262
|
+
const tempDir = targetDir ?? await mkdtemp(join$1(tmpdir(), "skills-"));
|
|
3263
|
+
const zipPath = join$1(tempDir, ".skill.zip");
|
|
3264
|
+
try {
|
|
3265
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
|
|
3266
|
+
try {
|
|
3267
|
+
await execFileAsync("unzip", [
|
|
3268
|
+
"-q",
|
|
3269
|
+
zipPath,
|
|
3270
|
+
"-d",
|
|
3271
|
+
tempDir
|
|
3272
|
+
]);
|
|
3273
|
+
} catch {
|
|
3274
|
+
throw new Error("Failed to extract skill archive. Ensure the 'unzip' command is available on PATH.");
|
|
3275
|
+
}
|
|
3276
|
+
} finally {
|
|
3277
|
+
await rm(zipPath, { force: true });
|
|
3278
|
+
}
|
|
3279
|
+
return tempDir;
|
|
3280
|
+
}
|
|
3281
|
+
function riskLabel$1(risk) {
|
|
3177
3282
|
switch (risk) {
|
|
3178
|
-
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical
|
|
3179
|
-
case "high": return import_picocolors.default.red("High
|
|
3180
|
-
case "medium": return import_picocolors.default.yellow("Med
|
|
3181
|
-
case "low": return import_picocolors.default.green("Low
|
|
3283
|
+
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical"));
|
|
3284
|
+
case "high": return import_picocolors.default.red("High");
|
|
3285
|
+
case "medium": return import_picocolors.default.yellow("Med");
|
|
3286
|
+
case "low": return import_picocolors.default.green("Low");
|
|
3182
3287
|
case "safe": return import_picocolors.default.green("Safe");
|
|
3183
|
-
default: return import_picocolors.default.dim("
|
|
3288
|
+
default: return import_picocolors.default.dim("Unknown");
|
|
3184
3289
|
}
|
|
3185
3290
|
}
|
|
3186
|
-
function socketLabel(audit) {
|
|
3187
|
-
if (!audit) return import_picocolors.default.dim("--");
|
|
3188
|
-
const count = audit.alerts ?? 0;
|
|
3189
|
-
return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
|
|
3190
|
-
}
|
|
3191
3291
|
function padEnd(str, width) {
|
|
3192
3292
|
const visible = stripTerminalEscapes(str);
|
|
3193
3293
|
const pad = Math.max(0, width - visible.length);
|
|
3194
3294
|
return str + " ".repeat(pad);
|
|
3195
3295
|
}
|
|
3196
|
-
function buildSecurityLines(auditData, skills
|
|
3296
|
+
function buildSecurityLines(auditData, skills) {
|
|
3197
3297
|
if (!auditData) return [];
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
})) return [];
|
|
3202
|
-
const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
|
|
3298
|
+
const rows = skills.filter((s) => auditData[s.slug]);
|
|
3299
|
+
if (rows.length === 0) return [];
|
|
3300
|
+
const nameWidth = Math.min(Math.max(...rows.map((s) => s.displayName.length)), 36);
|
|
3203
3301
|
const lines = [];
|
|
3204
|
-
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("
|
|
3302
|
+
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Risk"), 18) + padEnd(import_picocolors.default.dim("Score"), 8) + import_picocolors.default.dim("Signals");
|
|
3205
3303
|
lines.push(header);
|
|
3206
|
-
for (const skill of
|
|
3304
|
+
for (const skill of rows) {
|
|
3207
3305
|
const data = auditData[skill.slug];
|
|
3208
3306
|
const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
|
|
3209
|
-
const
|
|
3210
|
-
const
|
|
3211
|
-
const
|
|
3212
|
-
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(
|
|
3307
|
+
const risk = riskLabel$1(data.risk_level);
|
|
3308
|
+
const score = data.risk_score != null ? import_picocolors.default.dim(String(data.risk_score)) : import_picocolors.default.dim("--");
|
|
3309
|
+
const signals = data.risk_signals.length > 0 ? import_picocolors.default.yellow(String(data.risk_signals.length)) : import_picocolors.default.dim("0");
|
|
3310
|
+
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(risk, 18) + padEnd(score, 8) + signals);
|
|
3213
3311
|
}
|
|
3214
|
-
lines.push("");
|
|
3215
|
-
lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
|
|
3216
3312
|
return lines;
|
|
3217
3313
|
}
|
|
3314
|
+
function getHighRiskSkills(auditData, skills) {
|
|
3315
|
+
if (!auditData) return [];
|
|
3316
|
+
return skills.filter((s) => {
|
|
3317
|
+
const data = auditData[s.slug];
|
|
3318
|
+
return data && (data.risk_level === "high" || data.risk_level === "critical");
|
|
3319
|
+
}).map((s) => ({
|
|
3320
|
+
name: s.displayName,
|
|
3321
|
+
riskLevel: auditData[s.slug].risk_level
|
|
3322
|
+
}));
|
|
3323
|
+
}
|
|
3218
3324
|
function shortenPath$2(fullPath, cwd) {
|
|
3219
3325
|
const home = homedir();
|
|
3220
|
-
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
3221
|
-
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
3326
|
+
if (fullPath === home || fullPath.startsWith(home + sep$1)) return "~" + fullPath.slice(home.length);
|
|
3327
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep$1)) return "." + fullPath.slice(cwd.length);
|
|
3222
3328
|
return fullPath;
|
|
3223
3329
|
}
|
|
3224
3330
|
function computeSingleFileSkillHash(contents) {
|
|
@@ -3233,6 +3339,13 @@ function formatList$1(items, maxShow = 5) {
|
|
|
3233
3339
|
const remaining = items.length - maxShow;
|
|
3234
3340
|
return `${shown.join(", ")} +${remaining} more`;
|
|
3235
3341
|
}
|
|
3342
|
+
function buildSkillFiles(selectedSkills, tempDir, blobResult) {
|
|
3343
|
+
const skillFiles = {};
|
|
3344
|
+
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
3345
|
+
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
3346
|
+
else if (tempDir && skill.path.startsWith(tempDir + sep$1)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep$1).join("/") + "/SKILL.md";
|
|
3347
|
+
return skillFiles;
|
|
3348
|
+
}
|
|
3236
3349
|
function splitAgentsByType(agentTypes) {
|
|
3237
3350
|
const universal = [];
|
|
3238
3351
|
const symlinked = [];
|
|
@@ -3243,18 +3356,6 @@ function splitAgentsByType(agentTypes) {
|
|
|
3243
3356
|
symlinked
|
|
3244
3357
|
};
|
|
3245
3358
|
}
|
|
3246
|
-
function buildAgentSummaryLines(targetAgents, installMode) {
|
|
3247
|
-
const lines = [];
|
|
3248
|
-
const { universal, symlinked } = splitAgentsByType(targetAgents);
|
|
3249
|
-
if (installMode === "symlink") {
|
|
3250
|
-
if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
|
|
3251
|
-
if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
|
|
3252
|
-
} else {
|
|
3253
|
-
const allNames = targetAgents.map((a) => agents[a].displayName);
|
|
3254
|
-
lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
|
|
3255
|
-
}
|
|
3256
|
-
return lines;
|
|
3257
|
-
}
|
|
3258
3359
|
function targetDisplayName(target) {
|
|
3259
3360
|
const base = agents[target.agent].displayName;
|
|
3260
3361
|
return target.subagent ? `${base} (${target.subagent})` : base;
|
|
@@ -3316,12 +3417,28 @@ function buildCopyResultLines(results, cwd) {
|
|
|
3316
3417
|
}
|
|
3317
3418
|
return lines;
|
|
3318
3419
|
}
|
|
3420
|
+
function withQuitOnQ(promise) {
|
|
3421
|
+
const handler = (_str, key) => {
|
|
3422
|
+
if (key?.name !== "q" || key.ctrl || key.meta) return;
|
|
3423
|
+
process.stdin.removeListener("keypress", handler);
|
|
3424
|
+
if (process.stdin.isTTY) try {
|
|
3425
|
+
process.stdin.setRawMode(false);
|
|
3426
|
+
} catch {}
|
|
3427
|
+
process.stdout.write("\n");
|
|
3428
|
+
xe("Cancelled");
|
|
3429
|
+
process.exit(0);
|
|
3430
|
+
};
|
|
3431
|
+
process.stdin.on("keypress", handler);
|
|
3432
|
+
return promise.finally(() => {
|
|
3433
|
+
process.stdin.removeListener("keypress", handler);
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3319
3436
|
function multiselect(opts) {
|
|
3320
|
-
return fe({
|
|
3437
|
+
return withQuitOnQ(fe({
|
|
3321
3438
|
...opts,
|
|
3322
3439
|
options: opts.options,
|
|
3323
|
-
message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
|
|
3324
|
-
});
|
|
3440
|
+
message: `${opts.message} ${import_picocolors.default.dim("(space to toggle, q to quit)")}`
|
|
3441
|
+
}));
|
|
3325
3442
|
}
|
|
3326
3443
|
async function promptForAgents(message, choices) {
|
|
3327
3444
|
let lastSelected;
|
|
@@ -3382,57 +3499,44 @@ async function selectAgentsInteractive(options) {
|
|
|
3382
3499
|
return selected;
|
|
3383
3500
|
}
|
|
3384
3501
|
setVersion(version$1);
|
|
3385
|
-
async function
|
|
3386
|
-
spinner
|
|
3387
|
-
const skills = await wellKnownProvider.fetchAllSkills(url);
|
|
3388
|
-
if (skills.length === 0) {
|
|
3389
|
-
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3390
|
-
Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
|
|
3391
|
-
process.exit(1);
|
|
3392
|
-
}
|
|
3393
|
-
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3394
|
-
for (const skill of skills) {
|
|
3395
|
-
M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
|
|
3396
|
-
M.message(import_picocolors.default.dim(skill.description));
|
|
3397
|
-
if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
|
|
3398
|
-
}
|
|
3502
|
+
async function installLocalSkills(source, localDir, skills, options, agentResult) {
|
|
3503
|
+
const spinner = Y();
|
|
3399
3504
|
if (options.list) {
|
|
3400
3505
|
console.log();
|
|
3401
3506
|
M.step(import_picocolors.default.bold("Available Skills"));
|
|
3402
3507
|
for (const skill of skills) {
|
|
3403
|
-
M.message(` ${import_picocolors.default.cyan(skill
|
|
3508
|
+
M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
|
|
3404
3509
|
M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
3405
|
-
if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
|
|
3406
3510
|
}
|
|
3407
3511
|
console.log();
|
|
3408
|
-
Se("
|
|
3409
|
-
|
|
3512
|
+
Se("Use --skill <name> to install specific skills");
|
|
3513
|
+
return;
|
|
3410
3514
|
}
|
|
3411
3515
|
let selectedSkills;
|
|
3412
3516
|
if (options.skill?.includes("*")) {
|
|
3413
3517
|
selectedSkills = skills;
|
|
3414
3518
|
M.info(`Installing all ${skills.length} skills`);
|
|
3415
3519
|
} else if (options.skill && options.skill.length > 0) {
|
|
3416
|
-
selectedSkills = skills
|
|
3520
|
+
selectedSkills = filterSkills(skills, options.skill);
|
|
3417
3521
|
if (selectedSkills.length === 0) {
|
|
3418
3522
|
M.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
3419
3523
|
M.info("Available skills:");
|
|
3420
|
-
for (const s of skills) M.message(` - ${s
|
|
3524
|
+
for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
|
|
3421
3525
|
process.exit(1);
|
|
3422
3526
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
const firstSkill = skills[0];
|
|
3426
|
-
M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
|
|
3427
|
-
} else if (options.yes) {
|
|
3527
|
+
M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
|
|
3528
|
+
} else if (skills.length === 1 || options.yes) {
|
|
3428
3529
|
selectedSkills = skills;
|
|
3429
|
-
|
|
3530
|
+
if (skills.length === 1) {
|
|
3531
|
+
M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
|
|
3532
|
+
M.message(import_picocolors.default.dim(skills[0].description));
|
|
3533
|
+
} else M.info(`Installing all ${skills.length} skills`);
|
|
3430
3534
|
} else {
|
|
3431
3535
|
const selected = await multiselect({
|
|
3432
3536
|
message: "Select skills to install",
|
|
3433
|
-
options: skills.map((s) => ({
|
|
3537
|
+
options: [...skills].sort((a, b) => getSkillDisplayName(a).localeCompare(getSkillDisplayName(b))).map((s) => ({
|
|
3434
3538
|
value: s,
|
|
3435
|
-
label: s
|
|
3539
|
+
label: getSkillDisplayName(s),
|
|
3436
3540
|
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
3437
3541
|
})),
|
|
3438
3542
|
required: true
|
|
@@ -3466,7 +3570,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3466
3570
|
M.info("Installing to all agents");
|
|
3467
3571
|
} else {
|
|
3468
3572
|
M.info("Select agents to install skills to");
|
|
3469
|
-
const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
|
|
3573
|
+
const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).filter(([key]) => key !== "eve").map(([key, config]) => ({
|
|
3470
3574
|
value: key,
|
|
3471
3575
|
label: config.displayName
|
|
3472
3576
|
})));
|
|
@@ -3476,24 +3580,14 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3476
3580
|
}
|
|
3477
3581
|
targetAgents = selected;
|
|
3478
3582
|
}
|
|
3479
|
-
else
|
|
3583
|
+
else {
|
|
3480
3584
|
targetAgents = ensureUniversalAgents(installedAgents);
|
|
3481
|
-
|
|
3482
|
-
const firstAgent = installedAgents[0];
|
|
3483
|
-
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
3484
|
-
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
3485
|
-
} else {
|
|
3486
|
-
const selected = await selectAgentsInteractive({ global: options.global });
|
|
3487
|
-
if (pD(selected)) {
|
|
3488
|
-
xe("Installation cancelled");
|
|
3489
|
-
process.exit(0);
|
|
3490
|
-
}
|
|
3491
|
-
targetAgents = selected;
|
|
3585
|
+
M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
3492
3586
|
}
|
|
3493
3587
|
}
|
|
3588
|
+
const installTargets = buildInstallTargets(targetAgents, [void 0]);
|
|
3494
3589
|
let installGlobally = options.global ?? false;
|
|
3495
|
-
|
|
3496
|
-
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
3590
|
+
if (options.global === void 0 && !options.yes) {
|
|
3497
3591
|
const scope = await ve({
|
|
3498
3592
|
message: "Installation scope",
|
|
3499
3593
|
options: [{
|
|
@@ -3512,71 +3606,27 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3512
3606
|
}
|
|
3513
3607
|
installGlobally = scope;
|
|
3514
3608
|
}
|
|
3515
|
-
let installMode = options.copy ? "copy" : "symlink";
|
|
3516
|
-
const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
|
|
3517
|
-
if (!options.copy && !options.yes && uniqueDirs.size > 1) {
|
|
3518
|
-
const modeChoice = await ve({
|
|
3519
|
-
message: "Installation method",
|
|
3520
|
-
options: [{
|
|
3521
|
-
value: "symlink",
|
|
3522
|
-
label: "Symlink (Recommended)",
|
|
3523
|
-
hint: "Single source of truth, easy updates"
|
|
3524
|
-
}, {
|
|
3525
|
-
value: "copy",
|
|
3526
|
-
label: "Copy to all agents",
|
|
3527
|
-
hint: "Independent copies for each agent"
|
|
3528
|
-
}]
|
|
3529
|
-
});
|
|
3530
|
-
if (pD(modeChoice)) {
|
|
3531
|
-
xe("Installation cancelled");
|
|
3532
|
-
process.exit(0);
|
|
3533
|
-
}
|
|
3534
|
-
installMode = modeChoice;
|
|
3535
|
-
} else if (uniqueDirs.size <= 1) installMode = "copy";
|
|
3536
|
-
const cwd = process.cwd();
|
|
3537
|
-
const summaryLines = [];
|
|
3538
|
-
targetAgents.map((a) => agents[a].displayName);
|
|
3539
|
-
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
3540
|
-
skillName: skill.installName,
|
|
3541
|
-
agent,
|
|
3542
|
-
installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
|
|
3543
|
-
}))));
|
|
3544
|
-
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
3545
|
-
for (const { skillName, agent, installed } of overwriteChecks) {
|
|
3546
|
-
if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
|
|
3547
|
-
overwriteStatus.get(skillName).set(agent, installed);
|
|
3548
|
-
}
|
|
3549
|
-
for (const skill of selectedSkills) {
|
|
3550
|
-
if (summaryLines.length > 0) summaryLines.push("");
|
|
3551
|
-
const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
|
|
3552
|
-
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
3553
|
-
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
3554
|
-
if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
|
|
3555
|
-
const skillOverwrites = overwriteStatus.get(skill.installName);
|
|
3556
|
-
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
3557
|
-
if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
|
|
3558
|
-
}
|
|
3559
|
-
console.log();
|
|
3560
|
-
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3561
3609
|
if (!options.yes) {
|
|
3562
|
-
const confirmed = await ye({
|
|
3610
|
+
const confirmed = await ye({
|
|
3611
|
+
message: "Proceed with installation?",
|
|
3612
|
+
initialValue: true
|
|
3613
|
+
});
|
|
3563
3614
|
if (pD(confirmed) || !confirmed) {
|
|
3564
3615
|
xe("Installation cancelled");
|
|
3565
3616
|
process.exit(0);
|
|
3566
3617
|
}
|
|
3567
3618
|
}
|
|
3568
|
-
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
3569
|
-
const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
|
|
3570
3619
|
spinner.start("Installing skills...");
|
|
3571
3620
|
const results = [];
|
|
3572
|
-
for (const skill of selectedSkills) for (const
|
|
3573
|
-
const result = await
|
|
3621
|
+
for (const skill of selectedSkills) for (const target of installTargets) {
|
|
3622
|
+
const result = await installSkillForAgent(skill, target.agent, {
|
|
3574
3623
|
global: installGlobally,
|
|
3575
|
-
mode:
|
|
3624
|
+
mode: "copy",
|
|
3625
|
+
eveSubagent: target.subagent
|
|
3576
3626
|
});
|
|
3577
3627
|
results.push({
|
|
3578
|
-
skill: skill
|
|
3579
|
-
agent:
|
|
3628
|
+
skill: getSkillDisplayName(skill),
|
|
3629
|
+
agent: targetDisplayName(target),
|
|
3580
3630
|
...result
|
|
3581
3631
|
});
|
|
3582
3632
|
}
|
|
@@ -3584,73 +3634,18 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3584
3634
|
console.log();
|
|
3585
3635
|
const successful = results.filter((r) => r.success);
|
|
3586
3636
|
const failed = results.filter((r) => !r.success);
|
|
3587
|
-
const skillFiles = {};
|
|
3588
|
-
for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
|
|
3589
|
-
if (await wellKnownPrivacyPromise !== true) track({
|
|
3590
|
-
event: "install",
|
|
3591
|
-
source: sourceIdentifier,
|
|
3592
|
-
skills: selectedSkills.map((s) => s.installName).join(","),
|
|
3593
|
-
agents: targetAgents.join(","),
|
|
3594
|
-
...installGlobally && { global: "1" },
|
|
3595
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
3596
|
-
sourceType: "well-known"
|
|
3597
|
-
});
|
|
3598
|
-
if (successful.length > 0 && installGlobally) {
|
|
3599
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3600
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3601
|
-
await addSkillToLock(skill.installName, {
|
|
3602
|
-
source: sourceIdentifier,
|
|
3603
|
-
sourceType: "well-known",
|
|
3604
|
-
sourceUrl: skill.sourceUrl,
|
|
3605
|
-
skillFolderHash: ""
|
|
3606
|
-
});
|
|
3607
|
-
} catch {}
|
|
3608
|
-
}
|
|
3609
|
-
if (successful.length > 0 && !installGlobally) {
|
|
3610
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3611
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3612
|
-
const matchingResult = successful.find((r) => r.skill === skill.installName);
|
|
3613
|
-
const installDir = matchingResult?.canonicalPath || matchingResult?.path;
|
|
3614
|
-
if (installDir) {
|
|
3615
|
-
const computedHash = await computeSkillFolderHash(installDir);
|
|
3616
|
-
await addSkillToLocalLock(skill.installName, {
|
|
3617
|
-
source: sourceIdentifier,
|
|
3618
|
-
sourceType: "well-known",
|
|
3619
|
-
computedHash
|
|
3620
|
-
}, cwd);
|
|
3621
|
-
}
|
|
3622
|
-
} catch {}
|
|
3623
|
-
}
|
|
3624
3637
|
if (successful.length > 0) {
|
|
3625
3638
|
const bySkill = /* @__PURE__ */ new Map();
|
|
3626
3639
|
for (const r of successful) {
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
bySkill.set(r.skill, skillResults);
|
|
3640
|
+
if (!bySkill.has(r.skill)) bySkill.set(r.skill, []);
|
|
3641
|
+
bySkill.get(r.skill).push(r);
|
|
3630
3642
|
}
|
|
3631
|
-
const skillCount = bySkill.size;
|
|
3632
|
-
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
3633
|
-
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
3634
3643
|
const resultLines = [];
|
|
3635
3644
|
for (const [skillName, skillResults] of bySkill) {
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
|
|
3639
|
-
resultLines.push(...buildCopyResultLines(skillResults, cwd));
|
|
3640
|
-
} else {
|
|
3641
|
-
if (firstResult.canonicalPath) {
|
|
3642
|
-
const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
|
|
3643
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
|
|
3644
|
-
} else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
|
|
3645
|
-
resultLines.push(...buildResultLines(skillResults, targetAgents));
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3649
|
-
Me(resultLines.join("\n"), title);
|
|
3650
|
-
if (symlinkFailures.length > 0) {
|
|
3651
|
-
M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
|
|
3652
|
-
M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
3645
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
|
|
3646
|
+
resultLines.push(...buildCopyResultLines(skillResults, process.cwd()));
|
|
3653
3647
|
}
|
|
3648
|
+
Me(resultLines.join("\n"), import_picocolors.default.green(`Installed ${bySkill.size} skill${bySkill.size !== 1 ? "s" : ""}`));
|
|
3654
3649
|
}
|
|
3655
3650
|
if (failed.length > 0) {
|
|
3656
3651
|
console.log();
|
|
@@ -3659,6 +3654,14 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
3659
3654
|
}
|
|
3660
3655
|
console.log();
|
|
3661
3656
|
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3657
|
+
if (successful.length > 0 && !installGlobally) for (const skill of selectedSkills) try {
|
|
3658
|
+
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3659
|
+
await addSkillToLocalLock(skill.name, {
|
|
3660
|
+
source,
|
|
3661
|
+
sourceType: "local",
|
|
3662
|
+
computedHash
|
|
3663
|
+
}, process.cwd());
|
|
3664
|
+
} catch {}
|
|
3662
3665
|
await promptForFindSkills(options, targetAgents);
|
|
3663
3666
|
}
|
|
3664
3667
|
async function runAdd(args, options = {}) {
|
|
@@ -3699,102 +3702,139 @@ async function runAdd(args, options = {}) {
|
|
|
3699
3702
|
if (agentResult.isAgent) M.info(import_picocolors.default.bgCyan(import_picocolors.default.black(import_picocolors.default.bold(` ${agentResult.agent.name} `))) + " Agent detected — installing non-interactively");
|
|
3700
3703
|
else if (!process.stdin.isTTY) showInstallTip();
|
|
3701
3704
|
let tempDir = null;
|
|
3705
|
+
if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
|
|
3706
|
+
console.log();
|
|
3707
|
+
M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
|
|
3708
|
+
M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
|
|
3709
|
+
M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
|
|
3710
|
+
console.log();
|
|
3711
|
+
M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
|
|
3712
|
+
Se(import_picocolors.default.red("Installation blocked"));
|
|
3713
|
+
process.exit(1);
|
|
3714
|
+
}
|
|
3702
3715
|
try {
|
|
3703
3716
|
const spinner = Y();
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
const repoPrivacyPromise = (() => {
|
|
3709
|
-
if (!ownerRepoRaw || parsed.type !== "github") return Promise.resolve(null);
|
|
3710
|
-
const ownerRepo = parseOwnerRepo(ownerRepoRaw);
|
|
3711
|
-
if (!ownerRepo) return Promise.resolve(null);
|
|
3712
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo).catch(() => null);
|
|
3713
|
-
})();
|
|
3714
|
-
if (ownerRepoRaw?.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
|
|
3715
|
-
console.log();
|
|
3716
|
-
M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
|
|
3717
|
-
M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
|
|
3718
|
-
M.message(import_picocolors.default.yellow("Skills run with full agent permissions and could be malicious."));
|
|
3719
|
-
console.log();
|
|
3720
|
-
M.message(`If you understand the risks, re-run with:\n\n ${import_picocolors.default.cyan(`npx wittyhub add ${source} --dangerously-accept-openclaw-risks`)}\n`);
|
|
3721
|
-
Se(import_picocolors.default.red("Installation blocked"));
|
|
3722
|
-
process.exit(1);
|
|
3723
|
-
}
|
|
3724
|
-
if (parsed.type === "well-known") {
|
|
3725
|
-
await handleWellKnownSkills(source, parsed.url, options, spinner);
|
|
3726
|
-
return;
|
|
3727
|
-
}
|
|
3728
|
-
if (parsed.skillFilter) {
|
|
3729
|
-
options.skill = options.skill || [];
|
|
3730
|
-
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
3731
|
-
}
|
|
3732
|
-
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3733
|
-
let skills;
|
|
3734
|
-
let blobResult = null;
|
|
3735
|
-
if (parsed.type === "local") {
|
|
3736
|
-
spinner.start("Validating local path...");
|
|
3737
|
-
if (!existsSync(parsed.localPath)) {
|
|
3738
|
-
spinner.stop(import_picocolors.default.red("Path not found"));
|
|
3739
|
-
Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
|
|
3717
|
+
if (looksLikeLocalPath(source)) {
|
|
3718
|
+
const expanded = expandLocalPath(source);
|
|
3719
|
+
if (!existsSync$1(expanded)) {
|
|
3720
|
+
Se(import_picocolors.default.red(`Local path does not exist: ${expanded}`));
|
|
3740
3721
|
process.exit(1);
|
|
3741
3722
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
includeInternal,
|
|
3746
|
-
fullDepth: options.fullDepth
|
|
3747
|
-
});
|
|
3748
|
-
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
3749
|
-
const BLOB_ALLOWED_OWNERS = [
|
|
3750
|
-
"vercel",
|
|
3751
|
-
"vercel-labs",
|
|
3752
|
-
"heygen-com"
|
|
3753
|
-
];
|
|
3754
|
-
const ownerRepo = getOwnerRepo(parsed);
|
|
3755
|
-
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
3756
|
-
const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
|
|
3757
|
-
if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
|
|
3758
|
-
spinner.start("Fetching skills...");
|
|
3759
|
-
blobResult = await tryBlobInstall(ownerRepo, {
|
|
3760
|
-
subpath: parsed.subpath,
|
|
3761
|
-
skillFilter: parsed.skillFilter,
|
|
3762
|
-
ref: parsed.ref,
|
|
3763
|
-
getToken: getGitHubToken,
|
|
3764
|
-
includeInternal
|
|
3765
|
-
});
|
|
3766
|
-
if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
|
|
3723
|
+
if (!isDirectory(expanded)) {
|
|
3724
|
+
Se(import_picocolors.default.red(`Local path is not a directory: ${expanded}`));
|
|
3725
|
+
process.exit(1);
|
|
3767
3726
|
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
3774
|
-
spinner.stop("Repository cloned");
|
|
3775
|
-
spinner.start("Discovering skills...");
|
|
3776
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
3727
|
+
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3728
|
+
spinner.start("Discovering skills...");
|
|
3729
|
+
let skills;
|
|
3730
|
+
try {
|
|
3731
|
+
skills = await discoverSkills(expanded, void 0, {
|
|
3777
3732
|
includeInternal,
|
|
3778
3733
|
fullDepth: options.fullDepth
|
|
3779
3734
|
});
|
|
3735
|
+
} finally {
|
|
3736
|
+
spinner.stop("Discovery complete");
|
|
3780
3737
|
}
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
fullDepth: options.fullDepth
|
|
3789
|
-
});
|
|
3738
|
+
if (skills.length === 0) {
|
|
3739
|
+
M.error(import_picocolors.default.red("No skills found"));
|
|
3740
|
+
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
3741
|
+
process.exit(1);
|
|
3742
|
+
}
|
|
3743
|
+
await installLocalSkills(source, expanded, skills, options, agentResult);
|
|
3744
|
+
return;
|
|
3790
3745
|
}
|
|
3746
|
+
const skillNameForLookup = options.skill?.[0];
|
|
3747
|
+
let skillIds;
|
|
3748
|
+
const repoRef = isRepoLevelRef(source) ? source : repoUrlToRef(source);
|
|
3749
|
+
const repoLevel = repoRef !== null;
|
|
3750
|
+
if (repoLevel) {
|
|
3751
|
+
const [sourceType, ownerRepo] = repoRef.split(":", 2);
|
|
3752
|
+
spinner.start("查询仓库下的技能...");
|
|
3753
|
+
let repoSkills;
|
|
3754
|
+
try {
|
|
3755
|
+
repoSkills = await listSkillsByRepo(sourceType, ownerRepo);
|
|
3756
|
+
} finally {
|
|
3757
|
+
spinner.stop("查询完成");
|
|
3758
|
+
}
|
|
3759
|
+
if (repoSkills.length === 0) {
|
|
3760
|
+
Se(import_picocolors.default.red(`仓库 "${source}" 下未找到已收录的技能。`));
|
|
3761
|
+
process.exit(1);
|
|
3762
|
+
}
|
|
3763
|
+
if (options.list) {
|
|
3764
|
+
console.log();
|
|
3765
|
+
M.step(import_picocolors.default.bold("Available Skills"));
|
|
3766
|
+
const sorted = [...repoSkills].sort((a, b) => a.name.localeCompare(b.name));
|
|
3767
|
+
for (const skill of sorted) {
|
|
3768
|
+
M.message(` ${import_picocolors.default.cyan(skill.name)}`);
|
|
3769
|
+
if (skill.description) M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
3770
|
+
}
|
|
3771
|
+
console.log();
|
|
3772
|
+
Se("Use --skill <name> to install specific skills");
|
|
3773
|
+
process.exit(0);
|
|
3774
|
+
}
|
|
3775
|
+
if (options.skill && !options.skill.includes("*")) {
|
|
3776
|
+
const matched = repoSkills.filter((s) => options.skill.some((name) => s.name === name || s.skill_id.endsWith("/" + name)));
|
|
3777
|
+
if (matched.length === 0) {
|
|
3778
|
+
Se(import_picocolors.default.red(`未找到匹配 --skill 的技能: ${options.skill.join(", ")}`));
|
|
3779
|
+
process.exit(1);
|
|
3780
|
+
}
|
|
3781
|
+
skillIds = matched.map((s) => s.skill_id);
|
|
3782
|
+
} else if (options.skill?.includes("*") || options.all || options.yes) skillIds = repoSkills.map((s) => s.skill_id);
|
|
3783
|
+
else {
|
|
3784
|
+
const selected = await multiselect({
|
|
3785
|
+
message: "Select skills to install",
|
|
3786
|
+
options: [...repoSkills].sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
|
|
3787
|
+
value: s.skill_id,
|
|
3788
|
+
label: s.name,
|
|
3789
|
+
hint: s.description && s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description ?? ""
|
|
3790
|
+
})),
|
|
3791
|
+
required: true
|
|
3792
|
+
});
|
|
3793
|
+
if (pD(selected)) {
|
|
3794
|
+
xe("Installation cancelled");
|
|
3795
|
+
process.exit(0);
|
|
3796
|
+
}
|
|
3797
|
+
skillIds = selected;
|
|
3798
|
+
}
|
|
3799
|
+
} else try {
|
|
3800
|
+
const result = await resolveSkillId(source, skillNameForLookup);
|
|
3801
|
+
skillIds = Array.isArray(result) ? result : [result];
|
|
3802
|
+
} catch (e) {
|
|
3803
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3804
|
+
Se(import_picocolors.default.red(message));
|
|
3805
|
+
process.exit(1);
|
|
3806
|
+
}
|
|
3807
|
+
spinner.start(`Downloading ${skillIds.length > 1 ? `${skillIds.length} skills` : `skill ${import_picocolors.default.cyan(skillIds[0])}`} from API...`);
|
|
3808
|
+
try {
|
|
3809
|
+
tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
|
|
3810
|
+
for (const id of skillIds) await downloadAndExtractSkill(id, tempDir, options.version);
|
|
3811
|
+
} catch (e) {
|
|
3812
|
+
spinner.stop(import_picocolors.default.red("Download failed"));
|
|
3813
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3814
|
+
Se(import_picocolors.default.red(`Failed to download skill: ${message}`));
|
|
3815
|
+
process.exit(1);
|
|
3816
|
+
}
|
|
3817
|
+
spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
|
|
3818
|
+
const parsed = {
|
|
3819
|
+
type: "git",
|
|
3820
|
+
url: skillIds[0]
|
|
3821
|
+
};
|
|
3822
|
+
const repoPrivacyPromise = Promise.resolve(null);
|
|
3823
|
+
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3824
|
+
let skills;
|
|
3825
|
+
let blobResult = null;
|
|
3826
|
+
spinner.start("Discovering skills...");
|
|
3827
|
+
skills = await discoverSkills(tempDir, void 0, {
|
|
3828
|
+
includeInternal,
|
|
3829
|
+
fullDepth: options.fullDepth
|
|
3830
|
+
});
|
|
3791
3831
|
if (skills.length === 0) {
|
|
3792
3832
|
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3793
3833
|
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
3794
3834
|
await cleanup(tempDir);
|
|
3795
3835
|
process.exit(1);
|
|
3796
3836
|
}
|
|
3797
|
-
|
|
3837
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3798
3838
|
if (options.list) {
|
|
3799
3839
|
console.log();
|
|
3800
3840
|
M.step(import_picocolors.default.bold("Available Skills"));
|
|
@@ -3828,7 +3868,13 @@ async function runAdd(args, options = {}) {
|
|
|
3828
3868
|
process.exit(0);
|
|
3829
3869
|
}
|
|
3830
3870
|
let selectedSkills;
|
|
3831
|
-
if (
|
|
3871
|
+
if (repoLevel) {
|
|
3872
|
+
selectedSkills = skills;
|
|
3873
|
+
if (skills.length === 1) {
|
|
3874
|
+
M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(skills[0]))}`);
|
|
3875
|
+
M.message(import_picocolors.default.dim(skills[0].description));
|
|
3876
|
+
} else M.info(`Installing ${skills.length} skills`);
|
|
3877
|
+
} else if (options.skill?.includes("*")) {
|
|
3832
3878
|
selectedSkills = skills;
|
|
3833
3879
|
M.info(`Installing all ${skills.length} skills`);
|
|
3834
3880
|
} else if (options.skill && options.skill.length > 0) {
|
|
@@ -3891,8 +3937,10 @@ async function runAdd(args, options = {}) {
|
|
|
3891
3937
|
}
|
|
3892
3938
|
selectedSkills = selected;
|
|
3893
3939
|
}
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3940
|
+
const auditPromise = skillIds.length === 1 ? fetchAuditBySkillId(skillIds[0]) : Promise.all(skillIds.map((id) => fetchAuditBySkillId(id))).then((results) => results.reduce((acc, r) => ({
|
|
3941
|
+
...acc,
|
|
3942
|
+
...r
|
|
3943
|
+
}), {}));
|
|
3896
3944
|
let targetAgents;
|
|
3897
3945
|
const validAgents = Object.keys(agents);
|
|
3898
3946
|
if (options.agent?.includes("*")) {
|
|
@@ -4093,18 +4141,28 @@ async function runAdd(args, options = {}) {
|
|
|
4093
4141
|
}
|
|
4094
4142
|
console.log();
|
|
4095
4143
|
Me(summaryLines.join("\n"), "Installation Summary");
|
|
4144
|
+
const auditSkills = selectedSkills.map((s) => ({
|
|
4145
|
+
slug: getSkillDisplayName(s),
|
|
4146
|
+
displayName: getSkillDisplayName(s)
|
|
4147
|
+
}));
|
|
4148
|
+
let auditData = null;
|
|
4096
4149
|
try {
|
|
4097
|
-
|
|
4098
|
-
if (auditData
|
|
4099
|
-
const securityLines = buildSecurityLines(auditData,
|
|
4100
|
-
slug: getSkillDisplayName(s),
|
|
4101
|
-
displayName: getSkillDisplayName(s)
|
|
4102
|
-
})), ownerRepoForAudit);
|
|
4150
|
+
auditData = await auditPromise;
|
|
4151
|
+
if (auditData) {
|
|
4152
|
+
const securityLines = buildSecurityLines(auditData, auditSkills);
|
|
4103
4153
|
if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
|
|
4104
4154
|
}
|
|
4105
4155
|
} catch {}
|
|
4156
|
+
const highRiskSkills = getHighRiskSkills(auditData, auditSkills);
|
|
4157
|
+
if (highRiskSkills.length > 0) {
|
|
4158
|
+
const list = highRiskSkills.map((s) => `${s.name} 经安全审计标记为 ${s.riskLevel} 风险`).join(",");
|
|
4159
|
+
M.warn(`${import_picocolors.default.red(import_picocolors.default.bold("⚠ 高风险警告:"))} ${list},请谨慎安装。`);
|
|
4160
|
+
}
|
|
4106
4161
|
if (!options.yes) {
|
|
4107
|
-
const confirmed = await ye({
|
|
4162
|
+
const confirmed = await ye({
|
|
4163
|
+
message: highRiskSkills.length > 0 ? `检测到以下技能存在安全风险:${highRiskSkills.map((s) => `${s.name} (${s.riskLevel})`).join(", ")}。仍要继续安装?` : "Proceed with installation?",
|
|
4164
|
+
initialValue: highRiskSkills.length === 0
|
|
4165
|
+
});
|
|
4108
4166
|
if (pD(confirmed) || !confirmed) {
|
|
4109
4167
|
xe("Installation cancelled");
|
|
4110
4168
|
await cleanup(tempDir);
|
|
@@ -4116,17 +4174,7 @@ async function runAdd(args, options = {}) {
|
|
|
4116
4174
|
for (const skill of selectedSkills) for (const target of installTargets) {
|
|
4117
4175
|
const { agent, subagent } = target;
|
|
4118
4176
|
let result;
|
|
4119
|
-
if (
|
|
4120
|
-
const blobSkill = skill;
|
|
4121
|
-
result = await installBlobSkillForAgent({
|
|
4122
|
-
installName: blobSkill.name,
|
|
4123
|
-
files: blobSkill.files
|
|
4124
|
-
}, agent, {
|
|
4125
|
-
global: installGlobally,
|
|
4126
|
-
mode: installMode,
|
|
4127
|
-
eveSubagent: subagent
|
|
4128
|
-
});
|
|
4129
|
-
} else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
4177
|
+
if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
4130
4178
|
installName: skill.name,
|
|
4131
4179
|
files: [{
|
|
4132
4180
|
path: "SKILL.md",
|
|
@@ -4152,16 +4200,37 @@ async function runAdd(args, options = {}) {
|
|
|
4152
4200
|
console.log();
|
|
4153
4201
|
const successful = results.filter((r) => r.success);
|
|
4154
4202
|
const failed = results.filter((r) => !r.success);
|
|
4155
|
-
const skillFiles =
|
|
4156
|
-
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
4157
|
-
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
4158
|
-
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
4159
|
-
else continue;
|
|
4203
|
+
const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
|
|
4160
4204
|
const normalizedSource = getOwnerRepo(parsed);
|
|
4161
4205
|
const lockSource = getLockSource(parsed.url, normalizedSource);
|
|
4162
4206
|
if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
|
|
4163
4207
|
const isPrivate = await repoPrivacyPromise;
|
|
4164
|
-
if (isPrivate === false || isPrivate === null && parsed.type !== "github")
|
|
4208
|
+
if (isPrivate === false || isPrivate === null && parsed.type !== "github") {
|
|
4209
|
+
console.log("[telemetry-debug-1]", JSON.stringify({
|
|
4210
|
+
isPrivate,
|
|
4211
|
+
parsedType: parsed.type,
|
|
4212
|
+
normalizedSource,
|
|
4213
|
+
sourceType: parsed.type,
|
|
4214
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
4215
|
+
}));
|
|
4216
|
+
track({
|
|
4217
|
+
event: "install",
|
|
4218
|
+
source: normalizedSource,
|
|
4219
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4220
|
+
agents: targetAgents.join(","),
|
|
4221
|
+
...installGlobally && { global: "1" },
|
|
4222
|
+
skillFiles: JSON.stringify(skillFiles),
|
|
4223
|
+
sourceType: parsed.type
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
} else {
|
|
4227
|
+
console.log("[telemetry-debug-2]", JSON.stringify({
|
|
4228
|
+
parsedType: parsed.type,
|
|
4229
|
+
normalizedSource,
|
|
4230
|
+
sourceType: parsed.type,
|
|
4231
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
4232
|
+
}));
|
|
4233
|
+
track({
|
|
4165
4234
|
event: "install",
|
|
4166
4235
|
source: normalizedSource,
|
|
4167
4236
|
skills: selectedSkills.map((s) => s.name).join(","),
|
|
@@ -4170,32 +4239,21 @@ async function runAdd(args, options = {}) {
|
|
|
4170
4239
|
skillFiles: JSON.stringify(skillFiles),
|
|
4171
4240
|
sourceType: parsed.type
|
|
4172
4241
|
});
|
|
4173
|
-
}
|
|
4174
|
-
event: "install",
|
|
4175
|
-
source: normalizedSource,
|
|
4176
|
-
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4177
|
-
agents: targetAgents.join(","),
|
|
4178
|
-
...installGlobally && { global: "1" },
|
|
4179
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
4180
|
-
sourceType: parsed.type
|
|
4181
|
-
});
|
|
4242
|
+
}
|
|
4182
4243
|
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
4183
4244
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
4184
4245
|
let cachedTree;
|
|
4185
|
-
if (parsed.type === "github" &&
|
|
4246
|
+
if (parsed.type === "github" && true) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
|
|
4186
4247
|
for (const skill of selectedSkills) {
|
|
4187
4248
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4188
4249
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4189
4250
|
let skillFolderHash = "";
|
|
4190
4251
|
const skillPathValue = skillFiles[skill.name];
|
|
4191
|
-
if (
|
|
4192
|
-
const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
|
|
4193
|
-
if (hash) skillFolderHash = hash;
|
|
4194
|
-
} else if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
4252
|
+
if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
4195
4253
|
const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
|
|
4196
4254
|
if (hash) skillFolderHash = hash;
|
|
4197
4255
|
} else if (skillPathValue && tempDir) {
|
|
4198
|
-
const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
|
|
4256
|
+
const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
|
|
4199
4257
|
if (hash) skillFolderHash = hash;
|
|
4200
4258
|
}
|
|
4201
4259
|
await addSkillToLock(skill.name, {
|
|
@@ -4217,7 +4275,7 @@ async function runAdd(args, options = {}) {
|
|
|
4217
4275
|
for (const skill of selectedSkills) {
|
|
4218
4276
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4219
4277
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4220
|
-
const computedHash =
|
|
4278
|
+
const computedHash = tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
|
|
4221
4279
|
const skillPathValue = skillFiles[skill.name];
|
|
4222
4280
|
await addSkillToLocalLock(skill.name, {
|
|
4223
4281
|
source: lockSource || parsed.url,
|
|
@@ -4391,24 +4449,188 @@ function parseAddOptions(args) {
|
|
|
4391
4449
|
}
|
|
4392
4450
|
i--;
|
|
4393
4451
|
} else if (arg === "--dangerously-accept-openclaw-risks") options.dangerouslyAcceptOpenclawRisks = true;
|
|
4394
|
-
else if (arg
|
|
4452
|
+
else if (arg === "-v" || arg === "--version") {
|
|
4453
|
+
i++;
|
|
4454
|
+
options.version = args[i];
|
|
4455
|
+
} else if (arg && !arg.startsWith("-")) source.push(arg);
|
|
4395
4456
|
}
|
|
4396
4457
|
return {
|
|
4397
4458
|
source,
|
|
4398
4459
|
options
|
|
4399
4460
|
};
|
|
4400
4461
|
}
|
|
4462
|
+
function parseAuditOptions(args) {
|
|
4463
|
+
const errors = [];
|
|
4464
|
+
let skillId = "";
|
|
4465
|
+
for (const arg of args) if (arg && !arg.startsWith("-")) {
|
|
4466
|
+
skillId = arg.trim();
|
|
4467
|
+
break;
|
|
4468
|
+
}
|
|
4469
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4470
|
+
return {
|
|
4471
|
+
skillId: normalizeSkillId(skillId),
|
|
4472
|
+
errors
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
|
|
4476
|
+
const controller = new AbortController();
|
|
4477
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4478
|
+
try {
|
|
4479
|
+
const url = AUDIT_URL.replace("{skill_id}", skillId);
|
|
4480
|
+
let response;
|
|
4481
|
+
try {
|
|
4482
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4483
|
+
} catch {
|
|
4484
|
+
return {
|
|
4485
|
+
status: "error",
|
|
4486
|
+
message: `无法连接审计服务: ${AUDIT_URL}`
|
|
4487
|
+
};
|
|
4488
|
+
}
|
|
4489
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4490
|
+
if (!response.ok) return {
|
|
4491
|
+
status: "error",
|
|
4492
|
+
message: `审计服务返回 HTTP ${response.status}`
|
|
4493
|
+
};
|
|
4494
|
+
let data;
|
|
4495
|
+
try {
|
|
4496
|
+
data = await response.json();
|
|
4497
|
+
} catch {
|
|
4498
|
+
return {
|
|
4499
|
+
status: "error",
|
|
4500
|
+
message: "审计服务响应格式无效"
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
if (data.error) return {
|
|
4504
|
+
status: "no_audit",
|
|
4505
|
+
message: String(data.error)
|
|
4506
|
+
};
|
|
4507
|
+
const auditData = data.data || data;
|
|
4508
|
+
return {
|
|
4509
|
+
status: "ok",
|
|
4510
|
+
data: {
|
|
4511
|
+
risk_level: auditData.risk_level ?? "unknown",
|
|
4512
|
+
risk_score: auditData.risk_score ?? null,
|
|
4513
|
+
risk_signals: auditData.risk_signals ?? [],
|
|
4514
|
+
audited_at: auditData.audited_at ?? null,
|
|
4515
|
+
audit_type: auditData.audit_type ?? null,
|
|
4516
|
+
details: auditData.details ?? null
|
|
4517
|
+
}
|
|
4518
|
+
};
|
|
4519
|
+
} finally {
|
|
4520
|
+
clearTimeout(timeout);
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
function riskColor(level) {
|
|
4524
|
+
switch (level) {
|
|
4525
|
+
case "critical":
|
|
4526
|
+
case "high": return import_picocolors.default.red;
|
|
4527
|
+
case "medium": return import_picocolors.default.yellow;
|
|
4528
|
+
case "low":
|
|
4529
|
+
case "safe": return import_picocolors.default.green;
|
|
4530
|
+
default: return import_picocolors.default.dim;
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
const RISK_LABELS = {
|
|
4534
|
+
critical: "Critical",
|
|
4535
|
+
high: "High",
|
|
4536
|
+
medium: "Medium",
|
|
4537
|
+
low: "Low",
|
|
4538
|
+
safe: "Safe"
|
|
4539
|
+
};
|
|
4540
|
+
function riskLabel(level) {
|
|
4541
|
+
const color = riskColor(level);
|
|
4542
|
+
const label = RISK_LABELS[level] || level || "Unknown";
|
|
4543
|
+
return level === "critical" ? color(import_picocolors.default.bold(label)) : color(label);
|
|
4544
|
+
}
|
|
4545
|
+
function severityColor(severity) {
|
|
4546
|
+
const s = severity.toLowerCase();
|
|
4547
|
+
if (s === "critical" || s === "high") return import_picocolors.default.red(severity);
|
|
4548
|
+
if (s === "medium") return import_picocolors.default.yellow(severity);
|
|
4549
|
+
if (s === "low") return import_picocolors.default.green(severity);
|
|
4550
|
+
return import_picocolors.default.dim(severity);
|
|
4551
|
+
}
|
|
4552
|
+
function buildAuditOutput(skillId, data) {
|
|
4553
|
+
const lines = [];
|
|
4554
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(skillId)}`);
|
|
4555
|
+
const color = riskColor(data.risk_level);
|
|
4556
|
+
const score = data.risk_score != null ? color(`(score ${data.risk_score})`) : color("(score --)");
|
|
4557
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel(data.risk_level)} ${score}`);
|
|
4558
|
+
if (data.audited_at) lines.push(`${import_picocolors.default.bold("Audited at:")} ${import_picocolors.default.dim(data.audited_at)}`);
|
|
4559
|
+
lines.push("");
|
|
4560
|
+
if (data.risk_signals.length === 0) {
|
|
4561
|
+
lines.push(import_picocolors.default.green("No risk signals detected."));
|
|
4562
|
+
return lines;
|
|
4563
|
+
}
|
|
4564
|
+
lines.push(import_picocolors.default.bold("Risk signals:"));
|
|
4565
|
+
for (const signal of data.risk_signals) {
|
|
4566
|
+
const severity = severityColor(signal.severity || "unknown");
|
|
4567
|
+
lines.push(` ${import_picocolors.default.bold(signal.name)} ${import_picocolors.default.dim(`[${severity}]`)}`);
|
|
4568
|
+
if (signal.description) lines.push(` ${import_picocolors.default.dim(signal.description)}`);
|
|
4569
|
+
}
|
|
4570
|
+
return lines;
|
|
4571
|
+
}
|
|
4572
|
+
async function runAudit(args) {
|
|
4573
|
+
const { skillId, errors } = parseAuditOptions(args);
|
|
4574
|
+
if (errors.length > 0) {
|
|
4575
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
4576
|
+
console.error("Usage: wittyhub audit <skill_id>");
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
const result = await fetchSkillAudit(skillId);
|
|
4580
|
+
track({
|
|
4581
|
+
event: "audit",
|
|
4582
|
+
skillId,
|
|
4583
|
+
found: result.status === "ok" ? "1" : "0"
|
|
4584
|
+
});
|
|
4585
|
+
switch (result.status) {
|
|
4586
|
+
case "ok":
|
|
4587
|
+
for (const line of buildAuditOutput(skillId, result.data)) console.log(line);
|
|
4588
|
+
break;
|
|
4589
|
+
case "no_audit":
|
|
4590
|
+
console.log(`${import_picocolors.default.yellow("No audit result yet.")} ${import_picocolors.default.dim(result.message)}`);
|
|
4591
|
+
break;
|
|
4592
|
+
case "not_found":
|
|
4593
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
4594
|
+
break;
|
|
4595
|
+
case "error":
|
|
4596
|
+
console.error(import_picocolors.default.red(result.message));
|
|
4597
|
+
break;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4401
4600
|
const RESET$3 = "\x1B[0m";
|
|
4402
4601
|
const BOLD$3 = "\x1B[1m";
|
|
4403
4602
|
const DIM$3 = "\x1B[38;5;102m";
|
|
4404
4603
|
const TEXT$2 = "\x1B[38;5;145m";
|
|
4405
4604
|
const CYAN$1 = "\x1B[36m";
|
|
4406
|
-
const
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4605
|
+
const MAGENTA = "\x1B[35m";
|
|
4606
|
+
const YELLOW$1 = "\x1B[33m";
|
|
4607
|
+
const RED = "\x1B[31m";
|
|
4608
|
+
const GREEN = "\x1B[32m";
|
|
4609
|
+
function formatInstalls$1(count) {
|
|
4610
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "0";
|
|
4611
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
|
|
4612
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
|
|
4613
|
+
return String(count);
|
|
4614
|
+
}
|
|
4615
|
+
function formatRiskLevel$1(riskScore) {
|
|
4616
|
+
if (riskScore === void 0 || riskScore === null) return "";
|
|
4617
|
+
if (riskScore <= 20) return `${GREEN}safe${RESET$3}`;
|
|
4618
|
+
if (riskScore <= 50) return `${GREEN}low${RESET$3}`;
|
|
4619
|
+
if (riskScore <= 80) return `${YELLOW$1}medium${RESET$3}`;
|
|
4620
|
+
return `${RED}high${RESET$3}`;
|
|
4621
|
+
}
|
|
4622
|
+
const KV_KEY_WIDTH = 12;
|
|
4623
|
+
function kvKey(key) {
|
|
4624
|
+
return `${DIM$3}${key.padEnd(KV_KEY_WIDTH)}${RESET$3}`;
|
|
4625
|
+
}
|
|
4626
|
+
function escapeRegExp(s) {
|
|
4627
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4628
|
+
}
|
|
4629
|
+
function highlightMatches(text, query) {
|
|
4630
|
+
if (!text || !query) return text;
|
|
4631
|
+
const terms = query.trim().split(/\s+/).filter(Boolean).map(escapeRegExp);
|
|
4632
|
+
if (terms.length === 0) return text;
|
|
4633
|
+
return text.replace(new RegExp(`(${terms.join("|")})`, "gi"), `${MAGENTA}$1${RESET$3}`);
|
|
4412
4634
|
}
|
|
4413
4635
|
const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
|
|
4414
4636
|
function parseFindOptions(args) {
|
|
@@ -4422,7 +4644,7 @@ function parseFindOptions(args) {
|
|
|
4422
4644
|
if (arg === "--owner") {
|
|
4423
4645
|
const value = args[i + 1];
|
|
4424
4646
|
if (!value || value.startsWith("-")) {
|
|
4425
|
-
errors.push("--owner requires
|
|
4647
|
+
errors.push("--owner requires an owner");
|
|
4426
4648
|
continue;
|
|
4427
4649
|
}
|
|
4428
4650
|
ownerValue = value;
|
|
@@ -4430,7 +4652,7 @@ function parseFindOptions(args) {
|
|
|
4430
4652
|
} else if (arg.startsWith("--owner=")) {
|
|
4431
4653
|
ownerValue = arg.slice(8);
|
|
4432
4654
|
if (!ownerValue) {
|
|
4433
|
-
errors.push("--owner requires
|
|
4655
|
+
errors.push("--owner requires an owner");
|
|
4434
4656
|
continue;
|
|
4435
4657
|
}
|
|
4436
4658
|
} else {
|
|
@@ -4439,7 +4661,7 @@ function parseFindOptions(args) {
|
|
|
4439
4661
|
}
|
|
4440
4662
|
const owner = ownerValue.trim().toLowerCase();
|
|
4441
4663
|
if (!GITHUB_OWNER_PATTERN.test(owner)) {
|
|
4442
|
-
errors.push("--owner must be a valid
|
|
4664
|
+
errors.push("--owner must be a valid owner");
|
|
4443
4665
|
continue;
|
|
4444
4666
|
}
|
|
4445
4667
|
options.owner = owner;
|
|
@@ -4451,24 +4673,52 @@ function parseFindOptions(args) {
|
|
|
4451
4673
|
};
|
|
4452
4674
|
}
|
|
4453
4675
|
async function searchSkillsAPI(query, owner) {
|
|
4676
|
+
const url = `${SEARCH_URL}?${new URLSearchParams({
|
|
4677
|
+
q: query,
|
|
4678
|
+
limit: "10",
|
|
4679
|
+
mode: "text"
|
|
4680
|
+
}).toString()}`;
|
|
4681
|
+
let res;
|
|
4454
4682
|
try {
|
|
4455
|
-
|
|
4456
|
-
q: query,
|
|
4457
|
-
limit: "10"
|
|
4458
|
-
});
|
|
4459
|
-
if (owner) params.set("owner", owner);
|
|
4460
|
-
const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
|
|
4461
|
-
const res = await fetch(url);
|
|
4462
|
-
if (!res.ok) return [];
|
|
4463
|
-
return (await res.json()).skills.map((skill) => ({
|
|
4464
|
-
name: sanitizeMetadata(skill.name),
|
|
4465
|
-
slug: sanitizeMetadata(skill.id),
|
|
4466
|
-
source: sanitizeMetadata(skill.source || ""),
|
|
4467
|
-
installs: skill.installs
|
|
4468
|
-
})).sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
|
4683
|
+
res = await fetch(url);
|
|
4469
4684
|
} catch {
|
|
4470
|
-
return
|
|
4685
|
+
return {
|
|
4686
|
+
skills: [],
|
|
4687
|
+
error: `无法连接搜索服务: ${SEARCH_URL}`
|
|
4688
|
+
};
|
|
4471
4689
|
}
|
|
4690
|
+
if (!res.ok) return {
|
|
4691
|
+
skills: [],
|
|
4692
|
+
error: `搜索服务返回 HTTP ${res.status}`
|
|
4693
|
+
};
|
|
4694
|
+
let data;
|
|
4695
|
+
try {
|
|
4696
|
+
data = await res.json();
|
|
4697
|
+
} catch {
|
|
4698
|
+
return {
|
|
4699
|
+
skills: [],
|
|
4700
|
+
error: "搜索服务响应格式无效"
|
|
4701
|
+
};
|
|
4702
|
+
}
|
|
4703
|
+
let skills = (data.data?.results ?? data.results ?? []).map((skill) => ({
|
|
4704
|
+
name: sanitizeMetadata(skill.name),
|
|
4705
|
+
slug: sanitizeMetadata(skill.skill_id),
|
|
4706
|
+
source: sanitizeMetadata(skill.source || ""),
|
|
4707
|
+
installs: skill.download_count || 0,
|
|
4708
|
+
description: skill.description ? sanitizeMetadata(skill.description) : void 0,
|
|
4709
|
+
riskScore: skill.risk_score ?? void 0,
|
|
4710
|
+
sourceUrl: skill.source_url ? sanitizeMetadata(skill.source_url) : void 0
|
|
4711
|
+
}));
|
|
4712
|
+
if (owner) {
|
|
4713
|
+
const ownerLower = owner.toLowerCase();
|
|
4714
|
+
skills = skills.filter((skill) => {
|
|
4715
|
+
if (skill.sourceUrl) try {
|
|
4716
|
+
if (new URL(skill.sourceUrl).pathname.split("/").filter(Boolean)[0]?.toLowerCase() === ownerLower) return true;
|
|
4717
|
+
} catch {}
|
|
4718
|
+
return skill.slug.toLowerCase().includes(`/${ownerLower}/`);
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
|
|
4472
4722
|
}
|
|
4473
4723
|
const HIDE_CURSOR = "\x1B[?25l";
|
|
4474
4724
|
const SHOW_CURSOR = "\x1B[?25h";
|
|
@@ -4480,6 +4730,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4480
4730
|
let selectedIndex = 0;
|
|
4481
4731
|
let query = initialQuery;
|
|
4482
4732
|
let loading = false;
|
|
4733
|
+
let searchError = null;
|
|
4483
4734
|
let debounceTimer = null;
|
|
4484
4735
|
let lastRenderedLines = 0;
|
|
4485
4736
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
@@ -4495,19 +4746,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4495
4746
|
lines.push("");
|
|
4496
4747
|
if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
|
|
4497
4748
|
else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
|
|
4749
|
+
else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
|
|
4498
4750
|
else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
|
|
4499
4751
|
else {
|
|
4500
|
-
const visible = results.slice(0,
|
|
4752
|
+
const visible = results.slice(0, 6);
|
|
4501
4753
|
for (let i = 0; i < visible.length; i++) {
|
|
4502
4754
|
const skill = visible[i];
|
|
4503
4755
|
const isSelected = i === selectedIndex;
|
|
4504
4756
|
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
4505
|
-
const
|
|
4506
|
-
const
|
|
4507
|
-
const
|
|
4508
|
-
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
|
|
4757
|
+
const hlName = highlightMatches(skill.name, query);
|
|
4758
|
+
const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
|
|
4759
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4509
4760
|
const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
|
|
4510
|
-
lines.push(` ${arrow} ${
|
|
4761
|
+
lines.push(` ${arrow} ${kvKey("skill_name:")} ${name}${loadingIndicator}`);
|
|
4762
|
+
lines.push(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
|
|
4763
|
+
lines.push(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
|
|
4764
|
+
if (risk) lines.push(` ${kvKey("risk:")} ${risk}`);
|
|
4765
|
+
if (skill.description) {
|
|
4766
|
+
const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
|
|
4767
|
+
lines.push(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4768
|
+
}
|
|
4511
4769
|
}
|
|
4512
4770
|
}
|
|
4513
4771
|
lines.push("");
|
|
@@ -4523,6 +4781,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4523
4781
|
loading = false;
|
|
4524
4782
|
if (!q || q.length < 2) {
|
|
4525
4783
|
results = [];
|
|
4784
|
+
searchError = null;
|
|
4526
4785
|
selectedIndex = 0;
|
|
4527
4786
|
render();
|
|
4528
4787
|
return;
|
|
@@ -4532,10 +4791,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4532
4791
|
const debounceMs = Math.max(150, 350 - q.length * 50);
|
|
4533
4792
|
debounceTimer = setTimeout(async () => {
|
|
4534
4793
|
try {
|
|
4535
|
-
|
|
4794
|
+
const result = await searchSkillsAPI(q, owner);
|
|
4795
|
+
results = result.skills;
|
|
4796
|
+
searchError = result.error ?? null;
|
|
4536
4797
|
selectedIndex = 0;
|
|
4537
4798
|
} catch {
|
|
4538
4799
|
results = [];
|
|
4800
|
+
searchError = null;
|
|
4539
4801
|
} finally {
|
|
4540
4802
|
loading = false;
|
|
4541
4803
|
debounceTimer = null;
|
|
@@ -4592,49 +4854,47 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4592
4854
|
process.stdin.on("keypress", handleKeypress);
|
|
4593
4855
|
});
|
|
4594
4856
|
}
|
|
4595
|
-
function getOwnerRepoFromString(pkg) {
|
|
4596
|
-
const atIndex = pkg.lastIndexOf("@");
|
|
4597
|
-
const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
|
|
4598
|
-
if (match) return {
|
|
4599
|
-
owner: match[1],
|
|
4600
|
-
repo: match[2]
|
|
4601
|
-
};
|
|
4602
|
-
return null;
|
|
4603
|
-
}
|
|
4604
|
-
async function isRepoPublic(owner, repo) {
|
|
4605
|
-
return await isRepoPrivate(owner, repo) === false;
|
|
4606
|
-
}
|
|
4607
4857
|
async function runFind(args) {
|
|
4608
4858
|
const { query, options: findOptions, errors } = parseFindOptions(args);
|
|
4609
4859
|
const owner = findOptions.owner;
|
|
4610
4860
|
const isNonInteractive = !process.stdin.isTTY;
|
|
4611
4861
|
const agentTip = `${DIM$3}Tip: if running in a coding agent, follow these steps:${RESET$3}
|
|
4612
4862
|
${DIM$3} 1) npx wittyhub find [query] [--owner <owner>]${RESET$3}
|
|
4613
|
-
${DIM$3} 2) npx wittyhub add <
|
|
4863
|
+
${DIM$3} 2) npx wittyhub add <skill_id>${RESET$3}`;
|
|
4614
4864
|
if (errors.length > 0) {
|
|
4615
4865
|
for (const error of errors) console.error(error);
|
|
4616
|
-
console.error("Usage: npx
|
|
4866
|
+
console.error("Usage: npx wittyhub find <query> [--owner <owner>]");
|
|
4617
4867
|
return;
|
|
4618
4868
|
}
|
|
4619
4869
|
if (query) {
|
|
4620
|
-
const results = await searchSkillsAPI(query, owner);
|
|
4870
|
+
const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
|
|
4621
4871
|
track({
|
|
4622
4872
|
event: "find",
|
|
4623
4873
|
query,
|
|
4624
4874
|
resultCount: String(results.length)
|
|
4625
4875
|
});
|
|
4876
|
+
if (searchError) {
|
|
4877
|
+
console.error(`${RED}${searchError}${RESET$3}`);
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4626
4880
|
if (results.length === 0) {
|
|
4627
4881
|
const ownerSuffix = owner ? ` from owner "${owner}"` : "";
|
|
4628
4882
|
console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
|
|
4629
4883
|
return;
|
|
4630
4884
|
}
|
|
4631
|
-
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <
|
|
4885
|
+
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add ${TEXT$2}<skill_id>${RESET$3}`);
|
|
4632
4886
|
console.log();
|
|
4633
4887
|
for (const skill of results.slice(0, 6)) {
|
|
4634
|
-
const
|
|
4635
|
-
const
|
|
4636
|
-
console.log(`${
|
|
4637
|
-
console.log(
|
|
4888
|
+
const hlName = highlightMatches(skill.name, query);
|
|
4889
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4890
|
+
console.log(`${BOLD$3}●${RESET$3} ${kvKey("skill_name:")} ${BOLD$3}${hlName}${RESET$3}`);
|
|
4891
|
+
console.log(` ${kvKey("skill_id:")} ${CYAN$1}${skill.slug}${RESET$3}`);
|
|
4892
|
+
console.log(` ${kvKey("installs:")} ${CYAN$1}${formatInstalls$1(skill.installs)}${RESET$3}`);
|
|
4893
|
+
if (risk) console.log(` ${kvKey("risk:")} ${risk}`);
|
|
4894
|
+
if (skill.description) {
|
|
4895
|
+
const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
|
|
4896
|
+
console.log(` ${kvKey("description:")} ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4897
|
+
}
|
|
4638
4898
|
console.log();
|
|
4639
4899
|
}
|
|
4640
4900
|
return;
|
|
@@ -4657,26 +4917,154 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4657
4917
|
console.log();
|
|
4658
4918
|
return;
|
|
4659
4919
|
}
|
|
4660
|
-
const
|
|
4920
|
+
const skillId = selected.slug;
|
|
4661
4921
|
const skillName = selected.name;
|
|
4662
4922
|
console.log();
|
|
4663
|
-
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3}
|
|
4923
|
+
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} ${DIM$3}(${skillId})${RESET$3}...`);
|
|
4664
4924
|
console.log();
|
|
4665
|
-
const { source, options: addOptions } = parseAddOptions([
|
|
4666
|
-
pkg,
|
|
4667
|
-
"--skill",
|
|
4668
|
-
skillName
|
|
4669
|
-
]);
|
|
4925
|
+
const { source, options: addOptions } = parseAddOptions([skillId]);
|
|
4670
4926
|
await runAdd(source, addOptions);
|
|
4671
4927
|
console.log();
|
|
4672
|
-
|
|
4673
|
-
if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skills.sh/${selected.slug}${RESET$3}`);
|
|
4674
|
-
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
4928
|
+
console.log(`${DIM$3}View the skill at${RESET$3} ${TEXT$2}https://skillhub.openeuler.org/skills/${selected.slug}${RESET$3}`);
|
|
4675
4929
|
console.log();
|
|
4676
4930
|
}
|
|
4931
|
+
function parseGetOptions(args) {
|
|
4932
|
+
const errors = [];
|
|
4933
|
+
let skillId = "";
|
|
4934
|
+
for (const arg of args) if (arg && !arg.startsWith("-")) {
|
|
4935
|
+
skillId = arg.trim();
|
|
4936
|
+
break;
|
|
4937
|
+
}
|
|
4938
|
+
if (!skillId) errors.push("Missing skill id");
|
|
4939
|
+
return {
|
|
4940
|
+
skillId: normalizeSkillId(skillId),
|
|
4941
|
+
errors
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
|
|
4945
|
+
const controller = new AbortController();
|
|
4946
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4947
|
+
try {
|
|
4948
|
+
const url = GET_URL.replace("{skill_id}", skillId);
|
|
4949
|
+
let response;
|
|
4950
|
+
try {
|
|
4951
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4952
|
+
} catch {
|
|
4953
|
+
return {
|
|
4954
|
+
status: "error",
|
|
4955
|
+
message: `无法连接详情服务: ${GET_URL}`
|
|
4956
|
+
};
|
|
4957
|
+
}
|
|
4958
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4959
|
+
if (!response.ok) return {
|
|
4960
|
+
status: "error",
|
|
4961
|
+
message: `详情服务返回 HTTP ${response.status}`
|
|
4962
|
+
};
|
|
4963
|
+
let data;
|
|
4964
|
+
try {
|
|
4965
|
+
data = await response.json();
|
|
4966
|
+
} catch {
|
|
4967
|
+
return {
|
|
4968
|
+
status: "error",
|
|
4969
|
+
message: "详情服务响应格式无效"
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4972
|
+
if (data.error) return {
|
|
4973
|
+
status: "error",
|
|
4974
|
+
message: String(data.error)
|
|
4975
|
+
};
|
|
4976
|
+
const skillData = data.data || data;
|
|
4977
|
+
return {
|
|
4978
|
+
status: "ok",
|
|
4979
|
+
data: {
|
|
4980
|
+
skill_id: skillData.skill_id ?? skillId,
|
|
4981
|
+
name: skillData.name ?? "",
|
|
4982
|
+
description: skillData.description ?? null,
|
|
4983
|
+
version: skillData.version ?? null,
|
|
4984
|
+
commit_id: skillData.commit_id ?? null,
|
|
4985
|
+
author: skillData.author ?? null,
|
|
4986
|
+
source: skillData.source ?? "",
|
|
4987
|
+
source_url: skillData.source_url ?? "",
|
|
4988
|
+
repo_url: skillData.repo_url ?? null,
|
|
4989
|
+
category: skillData.category ?? null,
|
|
4990
|
+
tags: Array.isArray(skillData.tags) ? skillData.tags : null,
|
|
4991
|
+
platform: skillData.platform ?? null,
|
|
4992
|
+
metadata: skillData.metadata ?? null,
|
|
4993
|
+
risk_score: skillData.risk_score ?? null,
|
|
4994
|
+
download_count: skillData.download_count ?? 0,
|
|
4995
|
+
period_downloads: skillData.period_downloads ?? null,
|
|
4996
|
+
rating: skillData.rating ?? null,
|
|
4997
|
+
created_at: skillData.created_at ?? null,
|
|
4998
|
+
updated_at: skillData.updated_at ?? null,
|
|
4999
|
+
last_indexed_at: skillData.last_indexed_at ?? null
|
|
5000
|
+
}
|
|
5001
|
+
};
|
|
5002
|
+
} finally {
|
|
5003
|
+
clearTimeout(timeout);
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
5006
|
+
function formatInstalls(count) {
|
|
5007
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "0 installs";
|
|
5008
|
+
if (count <= 0) return "0 installs";
|
|
5009
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
5010
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
5011
|
+
return `${count} install${count === 1 ? "" : "s"}`;
|
|
5012
|
+
}
|
|
5013
|
+
function formatRiskLevel(riskScore) {
|
|
5014
|
+
if (riskScore === void 0 || riskScore === null) return import_picocolors.default.dim("unknown risk");
|
|
5015
|
+
if (riskScore <= 20) return import_picocolors.default.green("safe risk");
|
|
5016
|
+
if (riskScore <= 50) return import_picocolors.default.green("low risk");
|
|
5017
|
+
if (riskScore <= 80) return import_picocolors.default.yellow("medium risk");
|
|
5018
|
+
return import_picocolors.default.red("high risk");
|
|
5019
|
+
}
|
|
5020
|
+
function buildGetOutput(data) {
|
|
5021
|
+
const lines = [];
|
|
5022
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(data.skill_id)}`);
|
|
5023
|
+
if (data.name) lines.push(`${import_picocolors.default.bold("Name:")} ${data.name}`);
|
|
5024
|
+
if (data.description) lines.push(`${import_picocolors.default.bold("Description:")} ${data.description}`);
|
|
5025
|
+
if (data.author) lines.push(`${import_picocolors.default.bold("Author:")} ${data.author}`);
|
|
5026
|
+
if (data.category) lines.push(`${import_picocolors.default.bold("Category:")} ${data.category}`);
|
|
5027
|
+
if (data.version) lines.push(`${import_picocolors.default.bold("Version:")} ${data.version}`);
|
|
5028
|
+
if (data.tags && data.tags.length > 0) lines.push(`${import_picocolors.default.bold("Tags:")} ${data.tags.join(", ")}`);
|
|
5029
|
+
if (data.platform) lines.push(`${import_picocolors.default.bold("Platform:")} ${data.platform}`);
|
|
5030
|
+
if (data.source) lines.push(`${import_picocolors.default.bold("Source:")} ${data.source}`);
|
|
5031
|
+
if (data.source_url) lines.push(`${import_picocolors.default.bold("Source URL:")} ${import_picocolors.default.dim(data.source_url)}`);
|
|
5032
|
+
if (data.repo_url) lines.push(`${import_picocolors.default.bold("Repo URL:")} ${import_picocolors.default.dim(data.repo_url)}`);
|
|
5033
|
+
lines.push(`${import_picocolors.default.bold("Installs:")} ${import_picocolors.default.cyan(formatInstalls(data.download_count))}`);
|
|
5034
|
+
const riskLabel = formatRiskLevel(data.risk_score);
|
|
5035
|
+
const score = data.risk_score === void 0 || data.risk_score === null ? "" : import_picocolors.default.dim(` (score ${data.risk_score})`);
|
|
5036
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel}${score}`);
|
|
5037
|
+
if (data.updated_at) lines.push(`${import_picocolors.default.bold("Updated at:")} ${import_picocolors.default.dim(data.updated_at)}`);
|
|
5038
|
+
return lines;
|
|
5039
|
+
}
|
|
5040
|
+
async function runGet(args) {
|
|
5041
|
+
const { skillId, errors } = parseGetOptions(args);
|
|
5042
|
+
if (errors.length > 0) {
|
|
5043
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
5044
|
+
console.error("Usage: wittyhub get <skill_id>");
|
|
5045
|
+
return;
|
|
5046
|
+
}
|
|
5047
|
+
const result = await fetchSkillDetail(skillId);
|
|
5048
|
+
track({
|
|
5049
|
+
event: "get",
|
|
5050
|
+
skillId,
|
|
5051
|
+
found: result.status === "ok" ? "1" : "0"
|
|
5052
|
+
});
|
|
5053
|
+
switch (result.status) {
|
|
5054
|
+
case "ok":
|
|
5055
|
+
for (const line of buildGetOutput(result.data)) console.log(line);
|
|
5056
|
+
break;
|
|
5057
|
+
case "not_found":
|
|
5058
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
5059
|
+
break;
|
|
5060
|
+
case "error":
|
|
5061
|
+
console.error(import_picocolors.default.red(result.message));
|
|
5062
|
+
break;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
4677
5065
|
const isCancelled = (value) => typeof value === "symbol";
|
|
4678
5066
|
function shortenPath$1(fullPath, cwd) {
|
|
4679
|
-
const home = homedir();
|
|
5067
|
+
const home = homedir$1();
|
|
4680
5068
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
4681
5069
|
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
4682
5070
|
return fullPath;
|
|
@@ -5031,7 +5419,7 @@ const DIM$2 = "\x1B[38;5;102m";
|
|
|
5031
5419
|
const CYAN = "\x1B[36m";
|
|
5032
5420
|
const YELLOW = "\x1B[33m";
|
|
5033
5421
|
function shortenPath(fullPath, cwd) {
|
|
5034
|
-
const home = homedir();
|
|
5422
|
+
const home = homedir$1();
|
|
5035
5423
|
if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
|
|
5036
5424
|
if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
|
|
5037
5425
|
return fullPath;
|
|
@@ -5162,6 +5550,26 @@ async function runList(args) {
|
|
|
5162
5550
|
console.log();
|
|
5163
5551
|
}
|
|
5164
5552
|
}
|
|
5553
|
+
function extractSkillName(input) {
|
|
5554
|
+
try {
|
|
5555
|
+
const parsed = parseSource(input);
|
|
5556
|
+
if (parsed.subpath) {
|
|
5557
|
+
const segments = parsed.subpath.split("/");
|
|
5558
|
+
const last = segments[segments.length - 1];
|
|
5559
|
+
if (last) return last;
|
|
5560
|
+
}
|
|
5561
|
+
if (parsed.type !== "local" && parsed.url) {
|
|
5562
|
+
const match = parsed.url.match(/\/([^/]+)\.git$/);
|
|
5563
|
+
if (match?.[1]) return match[1];
|
|
5564
|
+
}
|
|
5565
|
+
} catch {}
|
|
5566
|
+
if (input.includes("/")) {
|
|
5567
|
+
const segments = input.replace(/\.git$/, "").split("/");
|
|
5568
|
+
const last = segments[segments.length - 1];
|
|
5569
|
+
if (last) return last;
|
|
5570
|
+
}
|
|
5571
|
+
return input;
|
|
5572
|
+
}
|
|
5165
5573
|
async function removeCommand(skillNames, options) {
|
|
5166
5574
|
const agentResult = await detectAgent();
|
|
5167
5575
|
if (agentResult.isAgent) {
|
|
@@ -5207,7 +5615,10 @@ async function removeCommand(skillNames, options) {
|
|
|
5207
5615
|
let selectedSkills = [];
|
|
5208
5616
|
if (options.all) selectedSkills = installedSkills;
|
|
5209
5617
|
else if (skillNames.length > 0) {
|
|
5210
|
-
selectedSkills = installedSkills.filter((s) => skillNames.some((name) =>
|
|
5618
|
+
selectedSkills = installedSkills.filter((s) => skillNames.some((name) => {
|
|
5619
|
+
if (name.toLowerCase() === s.toLowerCase()) return true;
|
|
5620
|
+
return extractSkillName(name).toLowerCase() === s.toLowerCase();
|
|
5621
|
+
}));
|
|
5211
5622
|
if (selectedSkills.length === 0) {
|
|
5212
5623
|
M.error(`No matching skills found for: ${skillNames.join(", ")}`);
|
|
5213
5624
|
return;
|
|
@@ -5268,7 +5679,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5268
5679
|
for (const pathToCleanup of pathsToCleanup) {
|
|
5269
5680
|
if (pathToCleanup === canonicalPath) continue;
|
|
5270
5681
|
try {
|
|
5271
|
-
if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
|
|
5682
|
+
if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
|
|
5272
5683
|
recursive: true,
|
|
5273
5684
|
force: true
|
|
5274
5685
|
});
|
|
@@ -5286,7 +5697,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5286
5697
|
isStillUsed = true;
|
|
5287
5698
|
break;
|
|
5288
5699
|
}
|
|
5289
|
-
if (!isStillUsed) await rm(canonicalPath, {
|
|
5700
|
+
if (!isStillUsed) await rm$1(canonicalPath, {
|
|
5290
5701
|
recursive: true,
|
|
5291
5702
|
force: true
|
|
5292
5703
|
});
|
|
@@ -5934,7 +6345,7 @@ function buildUsePrompt(input) {
|
|
|
5934
6345
|
return sections.join("\n\n") + "\n";
|
|
5935
6346
|
}
|
|
5936
6347
|
async function materializeUseSkill(skill) {
|
|
5937
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
|
|
6348
|
+
const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
|
|
5938
6349
|
const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
|
|
5939
6350
|
if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
5940
6351
|
await mkdir(skillDir, { recursive: true });
|
|
@@ -6161,8 +6572,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
|
|
|
6161
6572
|
const fullPath = join(targetDir, filePath);
|
|
6162
6573
|
if (!isPathSafe(targetDir, fullPath)) return;
|
|
6163
6574
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
6164
|
-
if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
|
|
6165
|
-
else await writeFile(fullPath, contents);
|
|
6575
|
+
if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
|
|
6576
|
+
else await writeFile$1(fullPath, contents);
|
|
6166
6577
|
}
|
|
6167
6578
|
async function copySkillDirectory(src, dest) {
|
|
6168
6579
|
await mkdir(dest, { recursive: true });
|
|
@@ -6222,6 +6633,308 @@ function fail(message) {
|
|
|
6222
6633
|
process.exit(1);
|
|
6223
6634
|
}
|
|
6224
6635
|
var UseCommandError = class extends Error {};
|
|
6636
|
+
const CANONICAL_CATEGORIES = new Set([
|
|
6637
|
+
"Research and Design",
|
|
6638
|
+
"Development and Build",
|
|
6639
|
+
"Engineering and Compilation",
|
|
6640
|
+
"Quality and Validation",
|
|
6641
|
+
"Release and Deployment",
|
|
6642
|
+
"Monitoring and Operations",
|
|
6643
|
+
"Performance Optimization",
|
|
6644
|
+
"Security Hardening",
|
|
6645
|
+
"others"
|
|
6646
|
+
]);
|
|
6647
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
6648
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
|
|
6649
|
+
function parseValidateOptions(args) {
|
|
6650
|
+
let path = "";
|
|
6651
|
+
const options = {};
|
|
6652
|
+
const errors = [];
|
|
6653
|
+
for (let i = 0; i < args.length; i++) {
|
|
6654
|
+
const arg = args[i];
|
|
6655
|
+
if (arg === "--json") options.json = true;
|
|
6656
|
+
else if (arg === "-h" || arg === "--help") {} else if (arg && !arg.startsWith("-")) if (!path) path = arg.trim();
|
|
6657
|
+
else errors.push(`Unexpected argument: ${arg}`);
|
|
6658
|
+
else if (arg) errors.push(`Unknown option: ${arg}`);
|
|
6659
|
+
}
|
|
6660
|
+
if (!path) errors.push("Missing required argument: <path>");
|
|
6661
|
+
return {
|
|
6662
|
+
path,
|
|
6663
|
+
options,
|
|
6664
|
+
errors
|
|
6665
|
+
};
|
|
6666
|
+
}
|
|
6667
|
+
async function runValidate(args) {
|
|
6668
|
+
const { path, options, errors } = parseValidateOptions(args);
|
|
6669
|
+
if (errors.length > 0) {
|
|
6670
|
+
for (const e of errors) console.error(import_picocolors.default.red(`error: ${e}`));
|
|
6671
|
+
console.error(`\nRun ${import_picocolors.default.cyan("wittyhub validate --help")} for usage.\n`);
|
|
6672
|
+
return null;
|
|
6673
|
+
}
|
|
6674
|
+
const result = await validateSkill(resolve(path));
|
|
6675
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
6676
|
+
else printResult(result);
|
|
6677
|
+
return result;
|
|
6678
|
+
}
|
|
6679
|
+
async function validateSkill(absPath) {
|
|
6680
|
+
const errors = [];
|
|
6681
|
+
const warnings = [];
|
|
6682
|
+
let skillDir = absPath;
|
|
6683
|
+
let skillMdPath = null;
|
|
6684
|
+
let stats;
|
|
6685
|
+
try {
|
|
6686
|
+
stats = await stat(absPath);
|
|
6687
|
+
} catch {
|
|
6688
|
+
errors.push({
|
|
6689
|
+
id: "path-not-found",
|
|
6690
|
+
severity: "error",
|
|
6691
|
+
message: `Path does not exist: ${absPath}`,
|
|
6692
|
+
hint: "Pass the skill directory (e.g. ./my-skill) or the SKILL.md file path."
|
|
6693
|
+
});
|
|
6694
|
+
return {
|
|
6695
|
+
path: absPath,
|
|
6696
|
+
errors,
|
|
6697
|
+
warnings,
|
|
6698
|
+
ok: false
|
|
6699
|
+
};
|
|
6700
|
+
}
|
|
6701
|
+
if (stats.isFile()) {
|
|
6702
|
+
const filename = basename(absPath);
|
|
6703
|
+
if (filename !== "SKILL.md") {
|
|
6704
|
+
errors.push({
|
|
6705
|
+
id: "filename-must-be-skill-md",
|
|
6706
|
+
severity: "error",
|
|
6707
|
+
message: `File must be named "SKILL.md" (got "${filename}")`,
|
|
6708
|
+
hint: "Rename the file to SKILL.md (uppercase). The agent skill spec requires this exact filename."
|
|
6709
|
+
});
|
|
6710
|
+
return {
|
|
6711
|
+
path: absPath,
|
|
6712
|
+
errors,
|
|
6713
|
+
warnings,
|
|
6714
|
+
ok: false
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
skillMdPath = absPath;
|
|
6718
|
+
skillDir = dirname(absPath);
|
|
6719
|
+
} else if (stats.isDirectory()) {
|
|
6720
|
+
const expected = join(absPath, "SKILL.md");
|
|
6721
|
+
try {
|
|
6722
|
+
await stat(expected);
|
|
6723
|
+
skillMdPath = expected;
|
|
6724
|
+
} catch {
|
|
6725
|
+
errors.push({
|
|
6726
|
+
id: "missing-skill-md",
|
|
6727
|
+
severity: "error",
|
|
6728
|
+
message: `No SKILL.md found in directory: ${absPath}`,
|
|
6729
|
+
hint: "Create one with `wittyhub init <name>` or rename an existing markdown file to SKILL.md (uppercase)."
|
|
6730
|
+
});
|
|
6731
|
+
return {
|
|
6732
|
+
path: absPath,
|
|
6733
|
+
errors,
|
|
6734
|
+
warnings,
|
|
6735
|
+
ok: false
|
|
6736
|
+
};
|
|
6737
|
+
}
|
|
6738
|
+
} else {
|
|
6739
|
+
errors.push({
|
|
6740
|
+
id: "path-invalid-type",
|
|
6741
|
+
severity: "error",
|
|
6742
|
+
message: `Path is neither a file nor a directory: ${absPath}`
|
|
6743
|
+
});
|
|
6744
|
+
return {
|
|
6745
|
+
path: absPath,
|
|
6746
|
+
errors,
|
|
6747
|
+
warnings,
|
|
6748
|
+
ok: false
|
|
6749
|
+
};
|
|
6750
|
+
}
|
|
6751
|
+
try {
|
|
6752
|
+
const lowerMdFile = (await readdir(skillDir)).find((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
|
|
6753
|
+
if (lowerMdFile) warnings.push({
|
|
6754
|
+
id: "case-variant-found",
|
|
6755
|
+
severity: "warning",
|
|
6756
|
+
message: `Found a case-variant file "${lowerMdFile}" alongside SKILL.md`,
|
|
6757
|
+
hint: "Only \"SKILL.md\" (all uppercase) is recognized by the agent spec. Remove the variant to avoid confusion."
|
|
6758
|
+
});
|
|
6759
|
+
} catch {}
|
|
6760
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
6761
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
6762
|
+
if (!match) {
|
|
6763
|
+
errors.push({
|
|
6764
|
+
id: "frontmatter-missing",
|
|
6765
|
+
severity: "error",
|
|
6766
|
+
message: "SKILL.md must start with a YAML frontmatter block delimited by \"---\"",
|
|
6767
|
+
hint: "Add a frontmatter block at the top:\n\n ---\n name: my-skill\n description: what it does\n ---"
|
|
6768
|
+
});
|
|
6769
|
+
return {
|
|
6770
|
+
path: skillMdPath,
|
|
6771
|
+
errors,
|
|
6772
|
+
warnings,
|
|
6773
|
+
ok: false
|
|
6774
|
+
};
|
|
6775
|
+
}
|
|
6776
|
+
match[1];
|
|
6777
|
+
const body = match[2] ?? "";
|
|
6778
|
+
const { data: fm } = parseFrontmatter(content);
|
|
6779
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
|
|
6780
|
+
errors.push({
|
|
6781
|
+
id: "frontmatter-not-object",
|
|
6782
|
+
severity: "error",
|
|
6783
|
+
message: "Frontmatter must be a YAML mapping (key-value pairs), not a scalar or list."
|
|
6784
|
+
});
|
|
6785
|
+
return {
|
|
6786
|
+
path: skillMdPath,
|
|
6787
|
+
errors,
|
|
6788
|
+
warnings,
|
|
6789
|
+
ok: false
|
|
6790
|
+
};
|
|
6791
|
+
}
|
|
6792
|
+
if (fm.name === void 0 || fm.name === null || fm.name === "") errors.push({
|
|
6793
|
+
id: "name-required",
|
|
6794
|
+
severity: "error",
|
|
6795
|
+
message: "Missing required frontmatter field: name",
|
|
6796
|
+
hint: "Add `name: <skill-name>` to the frontmatter."
|
|
6797
|
+
});
|
|
6798
|
+
else if (typeof fm.name !== "string") errors.push({
|
|
6799
|
+
id: "name-not-string",
|
|
6800
|
+
severity: "error",
|
|
6801
|
+
message: `Frontmatter field "name" must be a string (got ${typeof fm.name})`,
|
|
6802
|
+
hint: "Wrap the value in quotes if it contains special characters."
|
|
6803
|
+
});
|
|
6804
|
+
else {
|
|
6805
|
+
if (!NAME_PATTERN.test(fm.name)) warnings.push({
|
|
6806
|
+
id: "name-not-kebab",
|
|
6807
|
+
severity: "warning",
|
|
6808
|
+
message: `Skill name "${fm.name}" is not in kebab-case (lowercase letters, digits, hyphens)`,
|
|
6809
|
+
hint: "Use a name like \"add-or-fix-type-checking\" for best compatibility."
|
|
6810
|
+
});
|
|
6811
|
+
if (basename(skillDir) !== fm.name) warnings.push({
|
|
6812
|
+
id: "name-dirname-mismatch",
|
|
6813
|
+
severity: "warning",
|
|
6814
|
+
message: `Skill name "${fm.name}" does not match directory name "${basename(skillDir)}"`,
|
|
6815
|
+
hint: "For consistency, the directory name and `name` field should match."
|
|
6816
|
+
});
|
|
6817
|
+
}
|
|
6818
|
+
if (fm.description === void 0 || fm.description === null || fm.description === "") errors.push({
|
|
6819
|
+
id: "description-required",
|
|
6820
|
+
severity: "error",
|
|
6821
|
+
message: "Missing required frontmatter field: description",
|
|
6822
|
+
hint: "Add `description: <one-line summary>` to the frontmatter."
|
|
6823
|
+
});
|
|
6824
|
+
else if (typeof fm.description !== "string") errors.push({
|
|
6825
|
+
id: "description-not-string",
|
|
6826
|
+
severity: "error",
|
|
6827
|
+
message: `Frontmatter field "description" must be a string (got ${typeof fm.description})`
|
|
6828
|
+
});
|
|
6829
|
+
else {
|
|
6830
|
+
const desc = fm.description.trim();
|
|
6831
|
+
if (desc.length < 10) warnings.push({
|
|
6832
|
+
id: "description-too-short",
|
|
6833
|
+
severity: "warning",
|
|
6834
|
+
message: `Description is very short (${desc.length} chars): "${desc}"`,
|
|
6835
|
+
hint: "Aim for at least 10-20 characters so users can quickly understand the skill."
|
|
6836
|
+
});
|
|
6837
|
+
if (desc.length > 200) warnings.push({
|
|
6838
|
+
id: "description-too-long",
|
|
6839
|
+
severity: "warning",
|
|
6840
|
+
message: `Description is very long (${desc.length} chars)`,
|
|
6841
|
+
hint: "Keep it under 200 characters for readability in listings."
|
|
6842
|
+
});
|
|
6843
|
+
}
|
|
6844
|
+
if (fm.category !== void 0 && fm.category !== null) {
|
|
6845
|
+
if (typeof fm.category !== "string") warnings.push({
|
|
6846
|
+
id: "category-not-string",
|
|
6847
|
+
severity: "warning",
|
|
6848
|
+
message: `Frontmatter field "category" should be a string (got ${typeof fm.category})`
|
|
6849
|
+
});
|
|
6850
|
+
else if (!CANONICAL_CATEGORIES.has(fm.category)) warnings.push({
|
|
6851
|
+
id: "category-not-canonical",
|
|
6852
|
+
severity: "warning",
|
|
6853
|
+
message: `Category "${fm.category}" is not in the canonical list`,
|
|
6854
|
+
hint: `Valid values: ${[...CANONICAL_CATEGORIES].join(", ")}. See https://gitcode.com/openeuler/openEuler-skills.`
|
|
6855
|
+
});
|
|
6856
|
+
}
|
|
6857
|
+
if (fm.version !== void 0 && fm.version !== null && fm.version !== "") {
|
|
6858
|
+
if (typeof fm.version !== "string") warnings.push({
|
|
6859
|
+
id: "version-not-string",
|
|
6860
|
+
severity: "warning",
|
|
6861
|
+
message: `Frontmatter field "version" should be a string (got ${typeof fm.version})`,
|
|
6862
|
+
hint: "Quote the value, e.g. `version: \"1.0.0\"`."
|
|
6863
|
+
});
|
|
6864
|
+
else if (!SEMVER_PATTERN.test(fm.version.trim())) warnings.push({
|
|
6865
|
+
id: "version-not-semver",
|
|
6866
|
+
severity: "warning",
|
|
6867
|
+
message: `Version "${fm.version}" is not a valid semver`,
|
|
6868
|
+
hint: "Use the form X.Y.Z (e.g. 1.0.0, 0.2.1-beta)."
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
if (fm.metadata !== void 0 && (typeof fm.metadata !== "object" || Array.isArray(fm.metadata))) warnings.push({
|
|
6872
|
+
id: "metadata-not-object",
|
|
6873
|
+
severity: "warning",
|
|
6874
|
+
message: `Frontmatter field "metadata" should be a mapping (got ${Array.isArray(fm.metadata) ? "array" : typeof fm.metadata})`
|
|
6875
|
+
});
|
|
6876
|
+
if (body.trim().length === 0) warnings.push({
|
|
6877
|
+
id: "body-empty",
|
|
6878
|
+
severity: "warning",
|
|
6879
|
+
message: "SKILL.md has no body content after the frontmatter",
|
|
6880
|
+
hint: "Add at least a heading and a few instructions for the agent to follow."
|
|
6881
|
+
});
|
|
6882
|
+
else if (!/^#{1,6}\s+\S/m.test(body)) warnings.push({
|
|
6883
|
+
id: "body-no-heading",
|
|
6884
|
+
severity: "warning",
|
|
6885
|
+
message: "Body content has no Markdown headings",
|
|
6886
|
+
hint: "Structure the instructions with headings like `## When to use`, `## Instructions`."
|
|
6887
|
+
});
|
|
6888
|
+
const knownKeys = new Set([
|
|
6889
|
+
"name",
|
|
6890
|
+
"description",
|
|
6891
|
+
"version",
|
|
6892
|
+
"category",
|
|
6893
|
+
"metadata"
|
|
6894
|
+
]);
|
|
6895
|
+
const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
|
|
6896
|
+
if (unknownKeys.length > 0) warnings.push({
|
|
6897
|
+
id: "unknown-frontmatter-keys",
|
|
6898
|
+
severity: "warning",
|
|
6899
|
+
message: `Unknown frontmatter keys: ${unknownKeys.join(", ")}`,
|
|
6900
|
+
hint: `Recognized keys: ${[...knownKeys].join(", ")}.`
|
|
6901
|
+
});
|
|
6902
|
+
return {
|
|
6903
|
+
path: skillMdPath,
|
|
6904
|
+
errors,
|
|
6905
|
+
warnings,
|
|
6906
|
+
ok: errors.length === 0
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6909
|
+
function printResult(result) {
|
|
6910
|
+
const { path, errors, warnings, ok } = result;
|
|
6911
|
+
console.log(`${import_picocolors.default.cyan("Path:")} ${path}`);
|
|
6912
|
+
console.log();
|
|
6913
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
6914
|
+
console.log(import_picocolors.default.green(" All checks passed."));
|
|
6915
|
+
console.log();
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
if (errors.length > 0) {
|
|
6919
|
+
console.log(import_picocolors.default.red(`${errors.length} error(s):`));
|
|
6920
|
+
for (const rule of errors) {
|
|
6921
|
+
console.log(` ${import_picocolors.default.red("✗")} [${rule.id}] ${rule.message}`);
|
|
6922
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6923
|
+
}
|
|
6924
|
+
console.log();
|
|
6925
|
+
}
|
|
6926
|
+
if (warnings.length > 0) {
|
|
6927
|
+
console.log(import_picocolors.default.yellow(`${warnings.length} warning(s):`));
|
|
6928
|
+
for (const rule of warnings) {
|
|
6929
|
+
console.log(` ${import_picocolors.default.yellow("!")} [${rule.id}] ${rule.message}`);
|
|
6930
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6931
|
+
}
|
|
6932
|
+
console.log();
|
|
6933
|
+
}
|
|
6934
|
+
if (ok) console.log(import_picocolors.default.green(" Skill is valid.") + import_picocolors.default.yellow(warnings.length > 0 ? ` (with ${warnings.length} warning${warnings.length > 1 ? "s" : ""})` : ""));
|
|
6935
|
+
else console.log(import_picocolors.default.red(" Skill is NOT valid. Fix the errors above."));
|
|
6936
|
+
console.log();
|
|
6937
|
+
}
|
|
6225
6938
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6226
6939
|
function getVersion() {
|
|
6227
6940
|
try {
|
|
@@ -6274,11 +6987,12 @@ function showBanner() {
|
|
|
6274
6987
|
console.log();
|
|
6275
6988
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
|
|
6276
6989
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
|
|
6990
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
|
|
6277
6991
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
6278
6992
|
console.log();
|
|
6279
|
-
console.log(`${DIM}try:${RESET} npx wittyhub add
|
|
6993
|
+
console.log(`${DIM}try:${RESET} npx wittyhub add gitcode:openeuler/witty-diagnosis-agent`);
|
|
6280
6994
|
console.log();
|
|
6281
|
-
console.log(`Discover more skills at ${TEXT}https://
|
|
6995
|
+
console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6282
6996
|
console.log();
|
|
6283
6997
|
}
|
|
6284
6998
|
function showHelp() {
|
|
@@ -6294,6 +7008,10 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
6294
7008
|
remove [skills] Remove installed skills
|
|
6295
7009
|
list, ls List installed skills
|
|
6296
7010
|
find [query] Search for skills interactively
|
|
7011
|
+
audit <skill_id> Show security audit result (risk level + risk signals)
|
|
7012
|
+
get <skill_id> View skill details (author/category/version/description/tags)
|
|
7013
|
+
validate <path> Validate a local skill for spec compliance (SKILL.md filename,
|
|
7014
|
+
frontmatter, required fields name/description, etc.)
|
|
6297
7015
|
|
|
6298
7016
|
${BOLD}Find Options:${RESET}
|
|
6299
7017
|
--owner <owner> Search only repositories from a GitHub owner
|
|
@@ -6308,7 +7026,7 @@ ${BOLD}Update Options:${RESET}
|
|
|
6308
7026
|
|
|
6309
7027
|
${BOLD}Project:${RESET}
|
|
6310
7028
|
experimental_install Restore skills from skills-lock.json
|
|
6311
|
-
init [name] Initialize a skill (creates <name>/SKILL.md
|
|
7029
|
+
init [name] Initialize a skill (creates <name>/SKILL.md; defaults to my-skill/)
|
|
6312
7030
|
experimental_sync Sync skills from node_modules into agent directories
|
|
6313
7031
|
|
|
6314
7032
|
${BOLD}Add Options:${RESET}
|
|
@@ -6321,6 +7039,7 @@ ${BOLD}Add Options:${RESET}
|
|
|
6321
7039
|
--subagent <names> Install to Eve subagents (use 'root' for the root agent)
|
|
6322
7040
|
--all Shorthand for --skill '*' --agent '*' -y
|
|
6323
7041
|
--full-depth Search all subdirectories even when a root SKILL.md exists
|
|
7042
|
+
-v, --version <ver> Download a specific skill version from skill_versions table
|
|
6324
7043
|
|
|
6325
7044
|
${BOLD}Use Options:${RESET}
|
|
6326
7045
|
-s, --skill <skill> Specify the skill to use
|
|
@@ -6350,12 +7069,9 @@ ${BOLD}Options:${RESET}
|
|
|
6350
7069
|
--version, -v Show version number
|
|
6351
7070
|
|
|
6352
7071
|
${BOLD}Examples:${RESET}
|
|
6353
|
-
${DIM}$${RESET} wittyhub add
|
|
6354
|
-
${DIM}$${RESET} wittyhub
|
|
6355
|
-
${DIM}$${RESET} wittyhub
|
|
6356
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
|
|
6357
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
|
|
6358
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --skill pr-review commit
|
|
7072
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis
|
|
7073
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis -g
|
|
7074
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis --agent claude-code cursor
|
|
6359
7075
|
${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
|
|
6360
7076
|
${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
|
|
6361
7077
|
${DIM}$${RESET} wittyhub rm --global frontend-design
|
|
@@ -6374,7 +7090,45 @@ ${BOLD}Examples:${RESET}
|
|
|
6374
7090
|
${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
|
|
6375
7091
|
${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
|
|
6376
7092
|
|
|
6377
|
-
Discover more skills at ${TEXT}https://
|
|
7093
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
7094
|
+
`);
|
|
7095
|
+
}
|
|
7096
|
+
function showInitHelp() {
|
|
7097
|
+
console.log(`
|
|
7098
|
+
${BOLD}Usage:${RESET} wittyhub init [name]
|
|
7099
|
+
|
|
7100
|
+
${BOLD}Description:${RESET}
|
|
7101
|
+
Initialize a new skill by creating a SKILL.md template.
|
|
7102
|
+
|
|
7103
|
+
${BOLD}Arguments:${RESET}
|
|
7104
|
+
name Optional skill name. Creates <name>/SKILL.md.
|
|
7105
|
+
If omitted, defaults to my-skill/ (never the cwd itself).
|
|
7106
|
+
|
|
7107
|
+
${BOLD}Examples:${RESET}
|
|
7108
|
+
${DIM}$${RESET} wittyhub init ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
7109
|
+
${DIM}$${RESET} wittyhub init my-skill ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
7110
|
+
${DIM}$${RESET} wittyhub init code-reviewer ${DIM}# creates code-reviewer/SKILL.md${RESET}
|
|
7111
|
+
`);
|
|
7112
|
+
}
|
|
7113
|
+
function showValidateHelp() {
|
|
7114
|
+
console.log(`
|
|
7115
|
+
${BOLD}Usage:${RESET} wittyhub validate <path> [options]
|
|
7116
|
+
|
|
7117
|
+
${BOLD}Description:${RESET}
|
|
7118
|
+
Validate a local skill for spec compliance (SKILL.md filename, frontmatter,
|
|
7119
|
+
required fields, etc.). Reports errors (must-fix) and warnings (recommended).
|
|
7120
|
+
|
|
7121
|
+
${BOLD}Arguments:${RESET}
|
|
7122
|
+
path Path to the skill directory or SKILL.md file.
|
|
7123
|
+
|
|
7124
|
+
${BOLD}Options:${RESET}
|
|
7125
|
+
--json Output result as JSON (no ANSI colors).
|
|
7126
|
+
-h, --help Show this help message.
|
|
7127
|
+
|
|
7128
|
+
${BOLD}Examples:${RESET}
|
|
7129
|
+
${DIM}$${RESET} wittyhub validate ./my-skill
|
|
7130
|
+
${DIM}$${RESET} wittyhub validate ./my-skill/SKILL.md
|
|
7131
|
+
${DIM}$${RESET} wittyhub validate ./my-skill --json
|
|
6378
7132
|
`);
|
|
6379
7133
|
}
|
|
6380
7134
|
function showRemoveHelp() {
|
|
@@ -6404,24 +7158,25 @@ ${BOLD}Examples:${RESET}
|
|
|
6404
7158
|
${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
|
|
6405
7159
|
${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
|
|
6406
7160
|
|
|
6407
|
-
Discover more skills at ${TEXT}https://
|
|
7161
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
6408
7162
|
`);
|
|
6409
7163
|
}
|
|
6410
7164
|
function runInit(args) {
|
|
6411
7165
|
const cwd = process.cwd();
|
|
6412
|
-
const skillName = args[0]
|
|
6413
|
-
const
|
|
6414
|
-
const skillDir = hasName ? join(cwd, skillName) : cwd;
|
|
7166
|
+
const skillName = args[0] ?? "my-skill";
|
|
7167
|
+
const skillDir = join(cwd, skillName);
|
|
6415
7168
|
const skillFile = join(skillDir, "SKILL.md");
|
|
6416
|
-
const displayPath =
|
|
7169
|
+
const displayPath = `${skillName}/SKILL.md`;
|
|
6417
7170
|
if (existsSync(skillFile)) {
|
|
6418
7171
|
console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
|
|
6419
7172
|
return;
|
|
6420
7173
|
}
|
|
6421
|
-
|
|
7174
|
+
mkdirSync(skillDir, { recursive: true });
|
|
6422
7175
|
writeFileSync(skillFile, `---
|
|
6423
7176
|
name: ${skillName}
|
|
6424
7177
|
description: A brief description of what this skill does
|
|
7178
|
+
version: 0.1.0 # optional; increment as needed
|
|
7179
|
+
category: others # optional;change if needed — full list at https://gitcode.com/openeuler/openEuler-skills
|
|
6425
7180
|
---
|
|
6426
7181
|
|
|
6427
7182
|
# ${skillName}
|
|
@@ -6446,12 +7201,12 @@ Describe when this skill should be used.
|
|
|
6446
7201
|
console.log(`${DIM}Next steps:${RESET}`);
|
|
6447
7202
|
console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
|
|
6448
7203
|
console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
|
|
7204
|
+
console.log(` 3. Optionally set ${TEXT}version${RESET} and ${TEXT}category${RESET} (see the comment in ${TEXT}${displayPath}${RESET} for the full list of categories)`);
|
|
6449
7205
|
console.log();
|
|
6450
7206
|
console.log(`${DIM}Publishing:${RESET}`);
|
|
6451
|
-
console.log(`
|
|
6452
|
-
console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx wittyhub add https://example.com/${displayPath}${RESET}`);
|
|
7207
|
+
console.log(` Submit your skill to ${TEXT}https://gitcode.com/openeuler/openEuler-skills${RESET}`);
|
|
6453
7208
|
console.log();
|
|
6454
|
-
console.log(`Browse existing skills for inspiration at ${TEXT}https://
|
|
7209
|
+
console.log(`Browse existing skills for inspiration at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6455
7210
|
console.log();
|
|
6456
7211
|
}
|
|
6457
7212
|
async function main() {
|
|
@@ -6473,6 +7228,10 @@ async function main() {
|
|
|
6473
7228
|
await runFind(restArgs);
|
|
6474
7229
|
break;
|
|
6475
7230
|
case "init":
|
|
7231
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7232
|
+
showInitHelp();
|
|
7233
|
+
break;
|
|
7234
|
+
}
|
|
6476
7235
|
if (!inAgent) showLogo();
|
|
6477
7236
|
console.log();
|
|
6478
7237
|
runInit(restArgs);
|
|
@@ -6515,6 +7274,24 @@ async function main() {
|
|
|
6515
7274
|
case "ls":
|
|
6516
7275
|
await runList(restArgs);
|
|
6517
7276
|
break;
|
|
7277
|
+
case "audit":
|
|
7278
|
+
if (!inAgent) showLogo();
|
|
7279
|
+
await runAudit(restArgs);
|
|
7280
|
+
break;
|
|
7281
|
+
case "get":
|
|
7282
|
+
if (!inAgent) showLogo();
|
|
7283
|
+
await runGet(restArgs);
|
|
7284
|
+
break;
|
|
7285
|
+
case "validate":
|
|
7286
|
+
case "check-skill":
|
|
7287
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7288
|
+
showValidateHelp();
|
|
7289
|
+
break;
|
|
7290
|
+
}
|
|
7291
|
+
if (!inAgent) showLogo();
|
|
7292
|
+
console.log();
|
|
7293
|
+
await runValidate(restArgs);
|
|
7294
|
+
break;
|
|
6518
7295
|
case "check":
|
|
6519
7296
|
case "update":
|
|
6520
7297
|
case "upgrade":
|