wittyhub 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/_chunks/libs/simple-git.mjs +1 -1
- package/dist/cli.mjs +1235 -625
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11,14 +11,18 @@ 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 { homedir, tmpdir } from "node:os";
|
|
18
|
+
import { dirname as dirname$1, join as join$1, sep as sep$1 } from "node:path";
|
|
19
|
+
import { homedir as homedir$1, platform, tmpdir as tmpdir$1 } from "os";
|
|
20
|
+
import { parse } from "yaml";
|
|
16
21
|
import * as readline from "readline";
|
|
17
22
|
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";
|
|
23
|
+
import { promisify as promisify$1 } from "util";
|
|
24
|
+
import { execFile as execFile$1, execSync, spawn, spawnSync } from "child_process";
|
|
25
|
+
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
26
|
import { createHash as createHash$1 } from "crypto";
|
|
23
27
|
import { gunzipSync, inflateRawSync } from "node:zlib";
|
|
24
28
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
@@ -251,6 +255,30 @@ function isWellKnownUrl(input) {
|
|
|
251
255
|
return false;
|
|
252
256
|
}
|
|
253
257
|
}
|
|
258
|
+
const CLI_CONFIG_PATH = join(homedir$1(), ".config", "wittyhub", "cli.yaml");
|
|
259
|
+
function loadCliConfig() {
|
|
260
|
+
if (!existsSync(CLI_CONFIG_PATH)) return {};
|
|
261
|
+
try {
|
|
262
|
+
const parsed = parse(readFileSync(CLI_CONFIG_PATH, "utf-8"));
|
|
263
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
264
|
+
return parsed;
|
|
265
|
+
} catch {
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const DEFAULT_API_URL = "https://skillhub.openeuler.org/api/v1";
|
|
270
|
+
const DEFAULT_SEARCH_URL = "https://skillhub.openeuler.org/api/v1/index/search";
|
|
271
|
+
const DEFAULT_TELEMETRY_URL = "https://skillhub.openeuler.org/api/v1/skills/telemetry";
|
|
272
|
+
const DEFAULT_AUDIT_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}/audit";
|
|
273
|
+
const DEFAULT_GET_URL = "https://skillhub.openeuler.org/api/v1/skills/{skill_id}";
|
|
274
|
+
const DEFAULT_LIST_URL = "https://skillhub.openeuler.org/api/v1/skills/";
|
|
275
|
+
const cliConfig = loadCliConfig();
|
|
276
|
+
const DOWNLOAD_URL = `${typeof cliConfig.api_url === "string" && cliConfig.api_url.trim() ? cliConfig.api_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1` : DEFAULT_API_URL}/skills/{skill_id}/download`;
|
|
277
|
+
const SEARCH_URL = typeof cliConfig.search_url === "string" && cliConfig.search_url.trim() ? cliConfig.search_url.trim() : process.env.SKILLS_API_URL ? `${process.env.SKILLS_API_URL.replace(/\/$/, "")}/api/v1/index/search` : DEFAULT_SEARCH_URL;
|
|
278
|
+
const TELEMETRY_URL = typeof cliConfig.telemetry_url === "string" && cliConfig.telemetry_url.trim() ? cliConfig.telemetry_url.trim() : DEFAULT_TELEMETRY_URL;
|
|
279
|
+
const AUDIT_URL = typeof cliConfig.audit_url === "string" && cliConfig.audit_url.trim() ? cliConfig.audit_url.trim() : DEFAULT_AUDIT_URL;
|
|
280
|
+
const GET_URL = typeof cliConfig.get_url === "string" && cliConfig.get_url.trim() ? cliConfig.get_url.trim() : DEFAULT_GET_URL;
|
|
281
|
+
const LIST_URL = typeof cliConfig.list_url === "string" && cliConfig.list_url.trim() ? cliConfig.list_url.trim() : GET_URL.replace(/\{skill_id\}[^/]*$/, "") || DEFAULT_LIST_URL;
|
|
254
282
|
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
255
283
|
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
256
284
|
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
@@ -453,7 +481,7 @@ const CLONE_TIMEOUT_MS = (() => {
|
|
|
453
481
|
const parsed = Number.parseInt(raw, 10);
|
|
454
482
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
455
483
|
})();
|
|
456
|
-
const execFileAsync = promisify(execFile);
|
|
484
|
+
const execFileAsync$1 = promisify$1(execFile$1);
|
|
457
485
|
var GitCloneError = class extends Error {
|
|
458
486
|
url;
|
|
459
487
|
isTimeout;
|
|
@@ -524,11 +552,12 @@ function createGitClient(extraEnv) {
|
|
|
524
552
|
"filter.lfs.smudge=",
|
|
525
553
|
"filter.lfs.clean=",
|
|
526
554
|
"filter.lfs.process="
|
|
527
|
-
]
|
|
555
|
+
],
|
|
556
|
+
unsafe: { allowUnsafeFilter: true }
|
|
528
557
|
});
|
|
529
558
|
}
|
|
530
559
|
async function resetTempDir(dir) {
|
|
531
|
-
await rm(dir, {
|
|
560
|
+
await rm$1(dir, {
|
|
532
561
|
recursive: true,
|
|
533
562
|
force: true
|
|
534
563
|
}).catch(() => {});
|
|
@@ -537,7 +566,7 @@ async function resetTempDir(dir) {
|
|
|
537
566
|
async function tryGhClone(repo, tempDir, ref) {
|
|
538
567
|
let cloneTarget = repo.slug;
|
|
539
568
|
try {
|
|
540
|
-
const { stdout, stderr } = await execFileAsync("gh", [
|
|
569
|
+
const { stdout, stderr } = await execFileAsync$1("gh", [
|
|
541
570
|
"auth",
|
|
542
571
|
"status",
|
|
543
572
|
"-h",
|
|
@@ -554,7 +583,7 @@ async function tryGhClone(repo, tempDir, ref) {
|
|
|
554
583
|
} catch {
|
|
555
584
|
return false;
|
|
556
585
|
}
|
|
557
|
-
await execFileAsync("gh", [
|
|
586
|
+
await execFileAsync$1("gh", [
|
|
558
587
|
"repo",
|
|
559
588
|
"clone",
|
|
560
589
|
cloneTarget,
|
|
@@ -580,7 +609,7 @@ function buildGitHubAuthError(url, repo, message) {
|
|
|
580
609
|
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
610
|
}
|
|
582
611
|
async function cloneRepo(url, ref) {
|
|
583
|
-
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
612
|
+
const tempDir = await mkdtemp$1(join(tmpdir$1(), "skills-"));
|
|
584
613
|
const cloneOptions = ref ? [
|
|
585
614
|
"--depth",
|
|
586
615
|
"1",
|
|
@@ -596,7 +625,7 @@ async function cloneRepo(url, ref) {
|
|
|
596
625
|
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
597
626
|
const isAuthError = isAuthFailure(errorMessage);
|
|
598
627
|
if (isTimeout) {
|
|
599
|
-
await rm(tempDir, {
|
|
628
|
+
await rm$1(tempDir, {
|
|
600
629
|
recursive: true,
|
|
601
630
|
force: true
|
|
602
631
|
}).catch(() => {});
|
|
@@ -613,7 +642,7 @@ async function cloneRepo(url, ref) {
|
|
|
613
642
|
return tempDir;
|
|
614
643
|
} catch {}
|
|
615
644
|
}
|
|
616
|
-
await rm(tempDir, {
|
|
645
|
+
await rm$1(tempDir, {
|
|
617
646
|
recursive: true,
|
|
618
647
|
force: true
|
|
619
648
|
}).catch(() => {});
|
|
@@ -623,9 +652,9 @@ async function cloneRepo(url, ref) {
|
|
|
623
652
|
}
|
|
624
653
|
async function cleanupTempDir(dir) {
|
|
625
654
|
const normalizedDir = normalize(resolve(dir));
|
|
626
|
-
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
655
|
+
const normalizedTmpDir = normalize(resolve(tmpdir$1()));
|
|
627
656
|
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
628
|
-
await rm(dir, {
|
|
657
|
+
await rm$1(dir, {
|
|
629
658
|
recursive: true,
|
|
630
659
|
force: true
|
|
631
660
|
});
|
|
@@ -731,7 +760,7 @@ async function writeLocalLock(lock, cwd) {
|
|
|
731
760
|
version: lock.version,
|
|
732
761
|
skills: sortedSkills
|
|
733
762
|
};
|
|
734
|
-
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
763
|
+
await writeFile$1(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
735
764
|
}
|
|
736
765
|
async function computeSkillFolderHash(skillDir) {
|
|
737
766
|
const files = [];
|
|
@@ -950,7 +979,7 @@ function filterSkills(skills, inputNames) {
|
|
|
950
979
|
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
951
980
|
});
|
|
952
981
|
}
|
|
953
|
-
const home = homedir();
|
|
982
|
+
const home = homedir$1();
|
|
954
983
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
955
984
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
956
985
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
@@ -1679,7 +1708,7 @@ async function isDirEntryOrSymlinkToDir(entry, entryPath) {
|
|
|
1679
1708
|
}
|
|
1680
1709
|
}
|
|
1681
1710
|
function getCanonicalSkillsDir(global, cwd) {
|
|
1682
|
-
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1711
|
+
return join(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR$1, SKILLS_SUBDIR);
|
|
1683
1712
|
}
|
|
1684
1713
|
function getEveSubagentSkillsDir(subagent, cwd) {
|
|
1685
1714
|
return join(cwd || process.cwd(), EVE_SUBAGENTS_DIR, sanitizeName(subagent), "skills");
|
|
@@ -1688,7 +1717,7 @@ function getAgentBaseDir(agentType, global, cwd, eveSubagent) {
|
|
|
1688
1717
|
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1689
1718
|
if (agentType === "eve" && eveSubagent) return getEveSubagentSkillsDir(eveSubagent, cwd);
|
|
1690
1719
|
const agent = agents[agentType];
|
|
1691
|
-
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
1720
|
+
const baseDir = global ? homedir$1() : cwd || process.cwd();
|
|
1692
1721
|
if (global) {
|
|
1693
1722
|
if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
|
|
1694
1723
|
return agent.globalSkillsDir;
|
|
@@ -1700,7 +1729,7 @@ function resolveSymlinkTarget(linkPath, linkTarget) {
|
|
|
1700
1729
|
}
|
|
1701
1730
|
async function cleanAndCreateDirectory(path) {
|
|
1702
1731
|
try {
|
|
1703
|
-
await rm(path, {
|
|
1732
|
+
await rm$1(path, {
|
|
1704
1733
|
recursive: true,
|
|
1705
1734
|
force: true
|
|
1706
1735
|
});
|
|
@@ -1727,11 +1756,11 @@ async function createSymlink(target, linkPath) {
|
|
|
1727
1756
|
try {
|
|
1728
1757
|
if ((await lstat(linkPath)).isSymbolicLink()) {
|
|
1729
1758
|
if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
|
|
1730
|
-
await rm(linkPath);
|
|
1731
|
-
} else await rm(linkPath, { recursive: true });
|
|
1759
|
+
await rm$1(linkPath);
|
|
1760
|
+
} else await rm$1(linkPath, { recursive: true });
|
|
1732
1761
|
} catch (err) {
|
|
1733
1762
|
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
|
|
1734
|
-
await rm(linkPath, { force: true });
|
|
1763
|
+
await rm$1(linkPath, { force: true });
|
|
1735
1764
|
} catch {}
|
|
1736
1765
|
}
|
|
1737
1766
|
const linkDir = dirname(linkPath);
|
|
@@ -1872,7 +1901,7 @@ async function copyDirectory(src, dest, agentType) {
|
|
|
1872
1901
|
if (entry.isDirectory()) await copyDirectory(srcPath, destPath, agentType);
|
|
1873
1902
|
else try {
|
|
1874
1903
|
if (agentType === "eve" && entry.name.toLowerCase() === "skill.md") {
|
|
1875
|
-
await writeFile(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
1904
|
+
await writeFile$1(destPath, stripIgnoredEveFrontmatter(await readFile(srcPath, "utf-8")));
|
|
1876
1905
|
return;
|
|
1877
1906
|
}
|
|
1878
1907
|
await cp(srcPath, destPath, {
|
|
@@ -1927,88 +1956,6 @@ function getCanonicalPath(skillName, options = {}) {
|
|
|
1927
1956
|
if (!isPathSafe$1(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1928
1957
|
return canonicalPath;
|
|
1929
1958
|
}
|
|
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
1959
|
async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
2013
1960
|
const agent = agents[agentType];
|
|
2014
1961
|
const isGlobal = options.global ?? false;
|
|
@@ -2033,11 +1980,11 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2033
1980
|
};
|
|
2034
1981
|
try {
|
|
2035
1982
|
await mkdir(agentBase, { recursive: true });
|
|
2036
|
-
await rm(flatSkillPath, {
|
|
1983
|
+
await rm$1(flatSkillPath, {
|
|
2037
1984
|
recursive: true,
|
|
2038
1985
|
force: true
|
|
2039
1986
|
});
|
|
2040
|
-
await writeFile(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
1987
|
+
await writeFile$1(flatSkillPath, getEveFlatSkillMarkdown(skill.files), "utf-8");
|
|
2041
1988
|
return {
|
|
2042
1989
|
success: true,
|
|
2043
1990
|
path: flatSkillPath,
|
|
@@ -2073,7 +2020,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
|
|
|
2073
2020
|
if (!isPathSafe$1(targetDir, fullPath)) continue;
|
|
2074
2021
|
const parentDir = dirname(fullPath);
|
|
2075
2022
|
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");
|
|
2023
|
+
await writeFile$1(fullPath, agentType === "eve" && basename(file.path).toLowerCase() === "skill.md" ? stripIgnoredEveFrontmatter(file.contents) : file.contents, "utf-8");
|
|
2077
2024
|
}
|
|
2078
2025
|
}
|
|
2079
2026
|
try {
|
|
@@ -2260,8 +2207,6 @@ async function listInstalledSkills(options = {}) {
|
|
|
2260
2207
|
} catch {}
|
|
2261
2208
|
return Array.from(skillsMap.values());
|
|
2262
2209
|
}
|
|
2263
|
-
const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
2264
|
-
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
2265
2210
|
let cliVersion = null;
|
|
2266
2211
|
let detectedAgentName = null;
|
|
2267
2212
|
function setDetectedAgent(agentName) {
|
|
@@ -2276,21 +2221,65 @@ function isEnabled() {
|
|
|
2276
2221
|
function setVersion(version) {
|
|
2277
2222
|
cliVersion = version;
|
|
2278
2223
|
}
|
|
2279
|
-
|
|
2224
|
+
function slugifyTelemetryValue(value) {
|
|
2225
|
+
const lowered = value.trim().toLowerCase();
|
|
2226
|
+
if (!lowered) return "";
|
|
2227
|
+
let normalized = lowered.replace(/[^a-z0-9._-]+/g, "-");
|
|
2228
|
+
normalized = normalized.replace(/-{2,}/g, "-");
|
|
2229
|
+
return normalized.replace(/^-|-$/g, "");
|
|
2230
|
+
}
|
|
2231
|
+
function buildSkillId(sourceType, ownerRepo, skillName, skillFiles) {
|
|
2232
|
+
if (![
|
|
2233
|
+
"github",
|
|
2234
|
+
"gitcode",
|
|
2235
|
+
"gitlab",
|
|
2236
|
+
"gitee"
|
|
2237
|
+
].includes(sourceType)) return null;
|
|
2238
|
+
if (!ownerRepo) return null;
|
|
2239
|
+
const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
|
|
2240
|
+
if (skillFiles) {
|
|
2241
|
+
const relativePath = skillFiles[skillName];
|
|
2242
|
+
if (relativePath) {
|
|
2243
|
+
const normalizedPath = relativePath.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2244
|
+
if (normalizedPath === "SKILL.md") return `${sourceType}/${slugifiedOwnerRepo}/${slugifiedOwnerRepo.split("/").pop()}`;
|
|
2245
|
+
if (normalizedPath.endsWith("/SKILL.md")) return `${sourceType}/${slugifiedOwnerRepo}/${normalizedPath.slice(0, -9)}`;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const skillPath = slugifyTelemetryValue(skillName);
|
|
2249
|
+
if (!skillPath) return null;
|
|
2250
|
+
return `${sourceType}/${slugifiedOwnerRepo}/${skillPath}`;
|
|
2251
|
+
}
|
|
2252
|
+
async function fetchAuditData(source, skillSlugs, sourceType = "github", skillFiles, timeoutMs = 15e3) {
|
|
2280
2253
|
if (skillSlugs.length === 0) return null;
|
|
2254
|
+
const controller = new AbortController();
|
|
2255
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2281
2256
|
try {
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2257
|
+
const results = {};
|
|
2258
|
+
const promises = skillSlugs.map(async (skillName) => {
|
|
2259
|
+
const skillId = buildSkillId(sourceType, source, skillName, skillFiles);
|
|
2260
|
+
if (!skillId) return;
|
|
2261
|
+
try {
|
|
2262
|
+
const response = await fetch(AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/")), { signal: controller.signal });
|
|
2263
|
+
if (response.ok) {
|
|
2264
|
+
const data = await response.json();
|
|
2265
|
+
if (!data.error) {
|
|
2266
|
+
const auditData = data.data || data;
|
|
2267
|
+
results[skillName] = {
|
|
2268
|
+
risk_level: auditData.risk_level ?? "unknown",
|
|
2269
|
+
risk_score: auditData.risk_score ?? null,
|
|
2270
|
+
risk_signals: auditData.risk_signals ?? [],
|
|
2271
|
+
audited_at: auditData.audited_at ?? null
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
} catch {}
|
|
2285
2276
|
});
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
|
|
2289
|
-
clearTimeout(timeout);
|
|
2290
|
-
if (!response.ok) return null;
|
|
2291
|
-
return await response.json();
|
|
2277
|
+
await Promise.all(promises);
|
|
2278
|
+
return Object.keys(results).length > 0 ? results : null;
|
|
2292
2279
|
} catch {
|
|
2293
2280
|
return null;
|
|
2281
|
+
} finally {
|
|
2282
|
+
clearTimeout(timeout);
|
|
2294
2283
|
}
|
|
2295
2284
|
}
|
|
2296
2285
|
const pendingTelemetry = [];
|
|
@@ -2810,7 +2799,7 @@ const CURRENT_VERSION = 3;
|
|
|
2810
2799
|
function getSkillLockPath() {
|
|
2811
2800
|
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2812
2801
|
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
2813
|
-
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
2802
|
+
return join(homedir$1(), AGENTS_DIR, LOCK_FILE);
|
|
2814
2803
|
}
|
|
2815
2804
|
async function readSkillLock() {
|
|
2816
2805
|
const lockPath = getSkillLockPath();
|
|
@@ -2827,7 +2816,7 @@ async function readSkillLock() {
|
|
|
2827
2816
|
async function writeSkillLock(lock) {
|
|
2828
2817
|
const lockPath = getSkillLockPath();
|
|
2829
2818
|
await mkdir(dirname(lockPath), { recursive: true });
|
|
2830
|
-
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2819
|
+
await writeFile$1(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2831
2820
|
}
|
|
2832
2821
|
let _ghWarningShown = false;
|
|
2833
2822
|
function getGitHubToken() {
|
|
@@ -3164,65 +3153,126 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
3164
3153
|
tree
|
|
3165
3154
|
};
|
|
3166
3155
|
}
|
|
3167
|
-
var version$1 = "0.0.
|
|
3156
|
+
var version$1 = "0.0.4";
|
|
3168
3157
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
3169
|
-
async function isSourcePrivate(source) {
|
|
3170
|
-
const ownerRepo = parseOwnerRepo(source);
|
|
3171
|
-
if (!ownerRepo) return false;
|
|
3172
|
-
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
|
|
3173
|
-
}
|
|
3174
3158
|
function getLockSource(parsedUrl, normalizedSource) {
|
|
3175
3159
|
return parsedUrl.startsWith("git@") || parsedUrl.startsWith("ssh://") ? parsedUrl : normalizedSource;
|
|
3176
3160
|
}
|
|
3177
3161
|
function initTelemetry(version) {
|
|
3178
3162
|
setVersion(version);
|
|
3179
3163
|
}
|
|
3180
|
-
|
|
3164
|
+
const execFileAsync = promisify(execFile);
|
|
3165
|
+
function normalizeRepoRef(input) {
|
|
3166
|
+
let s = input.trim().toLowerCase();
|
|
3167
|
+
s = s.replace(/^(https?:\/\/|git:\/\/|ssh:\/\/)/, "");
|
|
3168
|
+
s = s.replace(/^git@([^:]+):/, "$1/");
|
|
3169
|
+
s = s.replace(/\.git$/, "");
|
|
3170
|
+
s = s.replace(/\/+$/, "");
|
|
3171
|
+
return s;
|
|
3172
|
+
}
|
|
3173
|
+
function looksLikeRepoSource(source) {
|
|
3174
|
+
const s = source.trim();
|
|
3175
|
+
if (s.startsWith("http://") || s.startsWith("https://")) return true;
|
|
3176
|
+
if (s.startsWith("git@") || s.startsWith("ssh://") || s.startsWith("git://")) return true;
|
|
3177
|
+
return (s.match(/\//g) || []).length <= 1 && !s.includes(":") && !/\s/.test(s);
|
|
3178
|
+
}
|
|
3179
|
+
async function resolveSkillId$1(source, skillName) {
|
|
3180
|
+
if (!looksLikeRepoSource(source)) return source;
|
|
3181
|
+
if (!skillName || !skillName.trim()) throw new Error(`源 "${source}" 看起来是仓库地址,需要同时传入 --skill <skill_name> 才能定位具体技能。`);
|
|
3182
|
+
const searchUrl = `${SEARCH_URL}?${new URLSearchParams({
|
|
3183
|
+
q: skillName.trim(),
|
|
3184
|
+
limit: "20",
|
|
3185
|
+
mode: "text",
|
|
3186
|
+
scope: "summary"
|
|
3187
|
+
}).toString()}`;
|
|
3188
|
+
let res;
|
|
3189
|
+
try {
|
|
3190
|
+
res = await fetch(searchUrl);
|
|
3191
|
+
} catch {
|
|
3192
|
+
throw new Error(`无法连接搜索服务: ${SEARCH_URL}`);
|
|
3193
|
+
}
|
|
3194
|
+
if (!res.ok) throw new Error(`搜索服务返回 HTTP ${res.status}`);
|
|
3195
|
+
const payload = await res.json();
|
|
3196
|
+
const results = payload.data?.results ?? payload.results ?? [];
|
|
3197
|
+
if (results.length === 0) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3198
|
+
const wantedRef = normalizeRepoRef(source);
|
|
3199
|
+
const match = results.find((r) => {
|
|
3200
|
+
const ref = normalizeRepoRef(r.source_url);
|
|
3201
|
+
return ref === wantedRef || ref.startsWith(`${wantedRef}/`);
|
|
3202
|
+
}) ?? results[0];
|
|
3203
|
+
if (!match) throw new Error(`未找到名称匹配 "${skillName}" 的技能。`);
|
|
3204
|
+
return match.skill_id;
|
|
3205
|
+
}
|
|
3206
|
+
async function downloadAndExtractSkill(skillId) {
|
|
3207
|
+
const downloadUrl = DOWNLOAD_URL.replace("{skill_id}", encodeURIComponent(skillId));
|
|
3208
|
+
const response = await fetch(downloadUrl);
|
|
3209
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
3210
|
+
const tempDir = await mkdtemp(join$1(tmpdir(), "skills-"));
|
|
3211
|
+
const zipPath = join$1(tempDir, ".skill.zip");
|
|
3212
|
+
try {
|
|
3213
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()));
|
|
3214
|
+
try {
|
|
3215
|
+
await execFileAsync("unzip", [
|
|
3216
|
+
"-q",
|
|
3217
|
+
zipPath,
|
|
3218
|
+
"-d",
|
|
3219
|
+
tempDir
|
|
3220
|
+
]);
|
|
3221
|
+
} catch {
|
|
3222
|
+
throw new Error("Failed to extract skill archive. Ensure the 'unzip' command is available on PATH.");
|
|
3223
|
+
}
|
|
3224
|
+
} finally {
|
|
3225
|
+
await rm(zipPath, { force: true });
|
|
3226
|
+
}
|
|
3227
|
+
return tempDir;
|
|
3228
|
+
}
|
|
3229
|
+
function riskLabel$1(risk) {
|
|
3181
3230
|
switch (risk) {
|
|
3182
|
-
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical
|
|
3183
|
-
case "high": return import_picocolors.default.red("High
|
|
3184
|
-
case "medium": return import_picocolors.default.yellow("Med
|
|
3185
|
-
case "low": return import_picocolors.default.green("Low
|
|
3231
|
+
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical"));
|
|
3232
|
+
case "high": return import_picocolors.default.red("High");
|
|
3233
|
+
case "medium": return import_picocolors.default.yellow("Med");
|
|
3234
|
+
case "low": return import_picocolors.default.green("Low");
|
|
3186
3235
|
case "safe": return import_picocolors.default.green("Safe");
|
|
3187
|
-
default: return import_picocolors.default.dim("
|
|
3236
|
+
default: return import_picocolors.default.dim("Unknown");
|
|
3188
3237
|
}
|
|
3189
3238
|
}
|
|
3190
|
-
function socketLabel(audit) {
|
|
3191
|
-
if (!audit) return import_picocolors.default.dim("--");
|
|
3192
|
-
const count = audit.alerts ?? 0;
|
|
3193
|
-
return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
|
|
3194
|
-
}
|
|
3195
3239
|
function padEnd(str, width) {
|
|
3196
3240
|
const visible = stripTerminalEscapes(str);
|
|
3197
3241
|
const pad = Math.max(0, width - visible.length);
|
|
3198
3242
|
return str + " ".repeat(pad);
|
|
3199
3243
|
}
|
|
3200
|
-
function buildSecurityLines(auditData, skills
|
|
3244
|
+
function buildSecurityLines(auditData, skills) {
|
|
3201
3245
|
if (!auditData) return [];
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
})) return [];
|
|
3206
|
-
const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
|
|
3246
|
+
const rows = skills.filter((s) => auditData[s.slug]);
|
|
3247
|
+
if (rows.length === 0) return [];
|
|
3248
|
+
const nameWidth = Math.min(Math.max(...rows.map((s) => s.displayName.length)), 36);
|
|
3207
3249
|
const lines = [];
|
|
3208
|
-
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("
|
|
3250
|
+
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Risk"), 18) + padEnd(import_picocolors.default.dim("Score"), 8) + import_picocolors.default.dim("Signals");
|
|
3209
3251
|
lines.push(header);
|
|
3210
|
-
for (const skill of
|
|
3252
|
+
for (const skill of rows) {
|
|
3211
3253
|
const data = auditData[skill.slug];
|
|
3212
3254
|
const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
|
|
3213
|
-
const
|
|
3214
|
-
const
|
|
3215
|
-
const
|
|
3216
|
-
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(
|
|
3255
|
+
const risk = riskLabel$1(data.risk_level);
|
|
3256
|
+
const score = data.risk_score != null ? import_picocolors.default.dim(String(data.risk_score)) : import_picocolors.default.dim("--");
|
|
3257
|
+
const signals = data.risk_signals.length > 0 ? import_picocolors.default.yellow(String(data.risk_signals.length)) : import_picocolors.default.dim("0");
|
|
3258
|
+
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(risk, 18) + padEnd(score, 8) + signals);
|
|
3217
3259
|
}
|
|
3218
|
-
lines.push("");
|
|
3219
|
-
lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
|
|
3220
3260
|
return lines;
|
|
3221
3261
|
}
|
|
3262
|
+
function getHighRiskSkills(auditData, skills) {
|
|
3263
|
+
if (!auditData) return [];
|
|
3264
|
+
return skills.filter((s) => {
|
|
3265
|
+
const data = auditData[s.slug];
|
|
3266
|
+
return data && (data.risk_level === "high" || data.risk_level === "critical");
|
|
3267
|
+
}).map((s) => ({
|
|
3268
|
+
name: s.displayName,
|
|
3269
|
+
riskLevel: auditData[s.slug].risk_level
|
|
3270
|
+
}));
|
|
3271
|
+
}
|
|
3222
3272
|
function shortenPath$2(fullPath, cwd) {
|
|
3223
3273
|
const home = homedir();
|
|
3224
|
-
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
3225
|
-
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
3274
|
+
if (fullPath === home || fullPath.startsWith(home + sep$1)) return "~" + fullPath.slice(home.length);
|
|
3275
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep$1)) return "." + fullPath.slice(cwd.length);
|
|
3226
3276
|
return fullPath;
|
|
3227
3277
|
}
|
|
3228
3278
|
function computeSingleFileSkillHash(contents) {
|
|
@@ -3237,6 +3287,13 @@ function formatList$1(items, maxShow = 5) {
|
|
|
3237
3287
|
const remaining = items.length - maxShow;
|
|
3238
3288
|
return `${shown.join(", ")} +${remaining} more`;
|
|
3239
3289
|
}
|
|
3290
|
+
function buildSkillFiles(selectedSkills, tempDir, blobResult) {
|
|
3291
|
+
const skillFiles = {};
|
|
3292
|
+
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
3293
|
+
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
3294
|
+
else if (tempDir && skill.path.startsWith(tempDir + sep$1)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep$1).join("/") + "/SKILL.md";
|
|
3295
|
+
return skillFiles;
|
|
3296
|
+
}
|
|
3240
3297
|
function splitAgentsByType(agentTypes) {
|
|
3241
3298
|
const universal = [];
|
|
3242
3299
|
const symlinked = [];
|
|
@@ -3247,18 +3304,6 @@ function splitAgentsByType(agentTypes) {
|
|
|
3247
3304
|
symlinked
|
|
3248
3305
|
};
|
|
3249
3306
|
}
|
|
3250
|
-
function buildAgentSummaryLines(targetAgents, installMode) {
|
|
3251
|
-
const lines = [];
|
|
3252
|
-
const { universal, symlinked } = splitAgentsByType(targetAgents);
|
|
3253
|
-
if (installMode === "symlink") {
|
|
3254
|
-
if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
|
|
3255
|
-
if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
|
|
3256
|
-
} else {
|
|
3257
|
-
const allNames = targetAgents.map((a) => agents[a].displayName);
|
|
3258
|
-
lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
|
|
3259
|
-
}
|
|
3260
|
-
return lines;
|
|
3261
|
-
}
|
|
3262
3307
|
function targetDisplayName(target) {
|
|
3263
3308
|
const base = agents[target.agent].displayName;
|
|
3264
3309
|
return target.subagent ? `${base} (${target.subagent})` : base;
|
|
@@ -3386,285 +3431,6 @@ async function selectAgentsInteractive(options) {
|
|
|
3386
3431
|
return selected;
|
|
3387
3432
|
}
|
|
3388
3433
|
setVersion(version$1);
|
|
3389
|
-
async function handleWellKnownSkills(source, url, options, spinner) {
|
|
3390
|
-
spinner.start("Discovering skills from well-known endpoint...");
|
|
3391
|
-
const skills = await wellKnownProvider.fetchAllSkills(url);
|
|
3392
|
-
if (skills.length === 0) {
|
|
3393
|
-
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3394
|
-
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."));
|
|
3395
|
-
process.exit(1);
|
|
3396
|
-
}
|
|
3397
|
-
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3398
|
-
for (const skill of skills) {
|
|
3399
|
-
M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
|
|
3400
|
-
M.message(import_picocolors.default.dim(skill.description));
|
|
3401
|
-
if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
|
|
3402
|
-
}
|
|
3403
|
-
if (options.list) {
|
|
3404
|
-
console.log();
|
|
3405
|
-
M.step(import_picocolors.default.bold("Available Skills"));
|
|
3406
|
-
for (const skill of skills) {
|
|
3407
|
-
M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
|
|
3408
|
-
M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
3409
|
-
if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
|
|
3410
|
-
}
|
|
3411
|
-
console.log();
|
|
3412
|
-
Se("Run without --list to install");
|
|
3413
|
-
process.exit(0);
|
|
3414
|
-
}
|
|
3415
|
-
let selectedSkills;
|
|
3416
|
-
if (options.skill?.includes("*")) {
|
|
3417
|
-
selectedSkills = skills;
|
|
3418
|
-
M.info(`Installing all ${skills.length} skills`);
|
|
3419
|
-
} else if (options.skill && options.skill.length > 0) {
|
|
3420
|
-
selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
|
|
3421
|
-
if (selectedSkills.length === 0) {
|
|
3422
|
-
M.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
3423
|
-
M.info("Available skills:");
|
|
3424
|
-
for (const s of skills) M.message(` - ${s.installName}`);
|
|
3425
|
-
process.exit(1);
|
|
3426
|
-
}
|
|
3427
|
-
} else if (skills.length === 1) {
|
|
3428
|
-
selectedSkills = skills;
|
|
3429
|
-
const firstSkill = skills[0];
|
|
3430
|
-
M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
|
|
3431
|
-
} else if (options.yes) {
|
|
3432
|
-
selectedSkills = skills;
|
|
3433
|
-
M.info(`Installing all ${skills.length} skills`);
|
|
3434
|
-
} else {
|
|
3435
|
-
const selected = await multiselect({
|
|
3436
|
-
message: "Select skills to install",
|
|
3437
|
-
options: skills.map((s) => ({
|
|
3438
|
-
value: s,
|
|
3439
|
-
label: s.installName,
|
|
3440
|
-
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
3441
|
-
})),
|
|
3442
|
-
required: true
|
|
3443
|
-
});
|
|
3444
|
-
if (pD(selected)) {
|
|
3445
|
-
xe("Installation cancelled");
|
|
3446
|
-
process.exit(0);
|
|
3447
|
-
}
|
|
3448
|
-
selectedSkills = selected;
|
|
3449
|
-
}
|
|
3450
|
-
let targetAgents;
|
|
3451
|
-
const validAgents = Object.keys(agents);
|
|
3452
|
-
if (options.agent?.includes("*")) {
|
|
3453
|
-
targetAgents = validAgents;
|
|
3454
|
-
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
3455
|
-
} else if (options.agent && options.agent.length > 0) {
|
|
3456
|
-
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3457
|
-
if (invalidAgents.length > 0) {
|
|
3458
|
-
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
3459
|
-
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
3460
|
-
process.exit(1);
|
|
3461
|
-
}
|
|
3462
|
-
targetAgents = options.agent;
|
|
3463
|
-
} else {
|
|
3464
|
-
spinner.start("Loading agents...");
|
|
3465
|
-
const installedAgents = await detectInstalledAgents();
|
|
3466
|
-
const totalAgents = Object.keys(agents).length;
|
|
3467
|
-
spinner.stop(`${totalAgents} agents`);
|
|
3468
|
-
if (installedAgents.length === 0) if (options.yes) {
|
|
3469
|
-
targetAgents = validAgents;
|
|
3470
|
-
M.info("Installing to all agents");
|
|
3471
|
-
} else {
|
|
3472
|
-
M.info("Select agents to install skills to");
|
|
3473
|
-
const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
|
|
3474
|
-
value: key,
|
|
3475
|
-
label: config.displayName
|
|
3476
|
-
})));
|
|
3477
|
-
if (pD(selected)) {
|
|
3478
|
-
xe("Installation cancelled");
|
|
3479
|
-
process.exit(0);
|
|
3480
|
-
}
|
|
3481
|
-
targetAgents = selected;
|
|
3482
|
-
}
|
|
3483
|
-
else if (installedAgents.length === 1 || options.yes) {
|
|
3484
|
-
targetAgents = ensureUniversalAgents(installedAgents);
|
|
3485
|
-
if (installedAgents.length === 1) {
|
|
3486
|
-
const firstAgent = installedAgents[0];
|
|
3487
|
-
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
3488
|
-
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
3489
|
-
} else {
|
|
3490
|
-
const selected = await selectAgentsInteractive({ global: options.global });
|
|
3491
|
-
if (pD(selected)) {
|
|
3492
|
-
xe("Installation cancelled");
|
|
3493
|
-
process.exit(0);
|
|
3494
|
-
}
|
|
3495
|
-
targetAgents = selected;
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
let installGlobally = options.global ?? false;
|
|
3499
|
-
const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
|
|
3500
|
-
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
3501
|
-
const scope = await ve({
|
|
3502
|
-
message: "Installation scope",
|
|
3503
|
-
options: [{
|
|
3504
|
-
value: false,
|
|
3505
|
-
label: "Project",
|
|
3506
|
-
hint: "Install in current directory (committed with your project)"
|
|
3507
|
-
}, {
|
|
3508
|
-
value: true,
|
|
3509
|
-
label: "Global",
|
|
3510
|
-
hint: "Install in home directory (available across all projects)"
|
|
3511
|
-
}]
|
|
3512
|
-
});
|
|
3513
|
-
if (pD(scope)) {
|
|
3514
|
-
xe("Installation cancelled");
|
|
3515
|
-
process.exit(0);
|
|
3516
|
-
}
|
|
3517
|
-
installGlobally = scope;
|
|
3518
|
-
}
|
|
3519
|
-
let installMode = options.copy ? "copy" : "symlink";
|
|
3520
|
-
const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
|
|
3521
|
-
if (!options.copy && !options.yes && uniqueDirs.size > 1) {
|
|
3522
|
-
const modeChoice = await ve({
|
|
3523
|
-
message: "Installation method",
|
|
3524
|
-
options: [{
|
|
3525
|
-
value: "symlink",
|
|
3526
|
-
label: "Symlink (Recommended)",
|
|
3527
|
-
hint: "Single source of truth, easy updates"
|
|
3528
|
-
}, {
|
|
3529
|
-
value: "copy",
|
|
3530
|
-
label: "Copy to all agents",
|
|
3531
|
-
hint: "Independent copies for each agent"
|
|
3532
|
-
}]
|
|
3533
|
-
});
|
|
3534
|
-
if (pD(modeChoice)) {
|
|
3535
|
-
xe("Installation cancelled");
|
|
3536
|
-
process.exit(0);
|
|
3537
|
-
}
|
|
3538
|
-
installMode = modeChoice;
|
|
3539
|
-
} else if (uniqueDirs.size <= 1) installMode = "copy";
|
|
3540
|
-
const cwd = process.cwd();
|
|
3541
|
-
const summaryLines = [];
|
|
3542
|
-
targetAgents.map((a) => agents[a].displayName);
|
|
3543
|
-
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
3544
|
-
skillName: skill.installName,
|
|
3545
|
-
agent,
|
|
3546
|
-
installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
|
|
3547
|
-
}))));
|
|
3548
|
-
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
3549
|
-
for (const { skillName, agent, installed } of overwriteChecks) {
|
|
3550
|
-
if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
|
|
3551
|
-
overwriteStatus.get(skillName).set(agent, installed);
|
|
3552
|
-
}
|
|
3553
|
-
for (const skill of selectedSkills) {
|
|
3554
|
-
if (summaryLines.length > 0) summaryLines.push("");
|
|
3555
|
-
const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
|
|
3556
|
-
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
3557
|
-
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
3558
|
-
if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
|
|
3559
|
-
const skillOverwrites = overwriteStatus.get(skill.installName);
|
|
3560
|
-
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
3561
|
-
if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
|
|
3562
|
-
}
|
|
3563
|
-
console.log();
|
|
3564
|
-
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3565
|
-
if (!options.yes) {
|
|
3566
|
-
const confirmed = await ye({ message: "Proceed with installation?" });
|
|
3567
|
-
if (pD(confirmed) || !confirmed) {
|
|
3568
|
-
xe("Installation cancelled");
|
|
3569
|
-
process.exit(0);
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
3573
|
-
const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
|
|
3574
|
-
spinner.start("Installing skills...");
|
|
3575
|
-
const results = [];
|
|
3576
|
-
for (const skill of selectedSkills) for (const agent of targetAgents) {
|
|
3577
|
-
const result = await installWellKnownSkillForAgent(skill, agent, {
|
|
3578
|
-
global: installGlobally,
|
|
3579
|
-
mode: installMode
|
|
3580
|
-
});
|
|
3581
|
-
results.push({
|
|
3582
|
-
skill: skill.installName,
|
|
3583
|
-
agent: agents[agent].displayName,
|
|
3584
|
-
...result
|
|
3585
|
-
});
|
|
3586
|
-
}
|
|
3587
|
-
spinner.stop("Installation complete");
|
|
3588
|
-
console.log();
|
|
3589
|
-
const successful = results.filter((r) => r.success);
|
|
3590
|
-
const failed = results.filter((r) => !r.success);
|
|
3591
|
-
const skillFiles = {};
|
|
3592
|
-
for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
|
|
3593
|
-
if (await wellKnownPrivacyPromise !== true) track({
|
|
3594
|
-
event: "install",
|
|
3595
|
-
source: sourceIdentifier,
|
|
3596
|
-
skills: selectedSkills.map((s) => s.installName).join(","),
|
|
3597
|
-
agents: targetAgents.join(","),
|
|
3598
|
-
...installGlobally && { global: "1" },
|
|
3599
|
-
skillFiles: JSON.stringify(skillFiles),
|
|
3600
|
-
sourceType: "well-known"
|
|
3601
|
-
});
|
|
3602
|
-
if (successful.length > 0 && installGlobally) {
|
|
3603
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3604
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3605
|
-
await addSkillToLock(skill.installName, {
|
|
3606
|
-
source: sourceIdentifier,
|
|
3607
|
-
sourceType: "well-known",
|
|
3608
|
-
sourceUrl: skill.sourceUrl,
|
|
3609
|
-
skillFolderHash: ""
|
|
3610
|
-
});
|
|
3611
|
-
} catch {}
|
|
3612
|
-
}
|
|
3613
|
-
if (successful.length > 0 && !installGlobally) {
|
|
3614
|
-
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3615
|
-
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
3616
|
-
const matchingResult = successful.find((r) => r.skill === skill.installName);
|
|
3617
|
-
const installDir = matchingResult?.canonicalPath || matchingResult?.path;
|
|
3618
|
-
if (installDir) {
|
|
3619
|
-
const computedHash = await computeSkillFolderHash(installDir);
|
|
3620
|
-
await addSkillToLocalLock(skill.installName, {
|
|
3621
|
-
source: sourceIdentifier,
|
|
3622
|
-
sourceType: "well-known",
|
|
3623
|
-
computedHash
|
|
3624
|
-
}, cwd);
|
|
3625
|
-
}
|
|
3626
|
-
} catch {}
|
|
3627
|
-
}
|
|
3628
|
-
if (successful.length > 0) {
|
|
3629
|
-
const bySkill = /* @__PURE__ */ new Map();
|
|
3630
|
-
for (const r of successful) {
|
|
3631
|
-
const skillResults = bySkill.get(r.skill) || [];
|
|
3632
|
-
skillResults.push(r);
|
|
3633
|
-
bySkill.set(r.skill, skillResults);
|
|
3634
|
-
}
|
|
3635
|
-
const skillCount = bySkill.size;
|
|
3636
|
-
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
3637
|
-
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
3638
|
-
const resultLines = [];
|
|
3639
|
-
for (const [skillName, skillResults] of bySkill) {
|
|
3640
|
-
const firstResult = skillResults[0];
|
|
3641
|
-
if (firstResult.mode === "copy") {
|
|
3642
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
|
|
3643
|
-
resultLines.push(...buildCopyResultLines(skillResults, cwd));
|
|
3644
|
-
} else {
|
|
3645
|
-
if (firstResult.canonicalPath) {
|
|
3646
|
-
const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
|
|
3647
|
-
resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
|
|
3648
|
-
} else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
|
|
3649
|
-
resultLines.push(...buildResultLines(skillResults, targetAgents));
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3653
|
-
Me(resultLines.join("\n"), title);
|
|
3654
|
-
if (symlinkFailures.length > 0) {
|
|
3655
|
-
M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
|
|
3656
|
-
M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
if (failed.length > 0) {
|
|
3660
|
-
console.log();
|
|
3661
|
-
M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
|
|
3662
|
-
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
|
|
3663
|
-
}
|
|
3664
|
-
console.log();
|
|
3665
|
-
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3666
|
-
await promptForFindSkills(options, targetAgents);
|
|
3667
|
-
}
|
|
3668
3434
|
async function runAdd(args, options = {}) {
|
|
3669
3435
|
const source = args[0];
|
|
3670
3436
|
let installTipShown = false;
|
|
@@ -3705,17 +3471,31 @@ async function runAdd(args, options = {}) {
|
|
|
3705
3471
|
let tempDir = null;
|
|
3706
3472
|
try {
|
|
3707
3473
|
const spinner = Y();
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
})
|
|
3718
|
-
|
|
3474
|
+
const skillNameForLookup = options.skill?.[0];
|
|
3475
|
+
let skillId;
|
|
3476
|
+
try {
|
|
3477
|
+
skillId = await resolveSkillId$1(source, skillNameForLookup);
|
|
3478
|
+
} catch (e) {
|
|
3479
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3480
|
+
Se(import_picocolors.default.red(message));
|
|
3481
|
+
process.exit(1);
|
|
3482
|
+
}
|
|
3483
|
+
spinner.start(`Downloading skill ${import_picocolors.default.cyan(skillId)} from API...`);
|
|
3484
|
+
try {
|
|
3485
|
+
tempDir = await downloadAndExtractSkill(skillId);
|
|
3486
|
+
} catch (e) {
|
|
3487
|
+
spinner.stop(import_picocolors.default.red("Download failed"));
|
|
3488
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
3489
|
+
Se(import_picocolors.default.red(`Failed to download skill "${skillId}": ${message}`));
|
|
3490
|
+
process.exit(1);
|
|
3491
|
+
}
|
|
3492
|
+
spinner.stop(`Skill archive downloaded from ${import_picocolors.default.cyan("API")}`);
|
|
3493
|
+
const parsed = {
|
|
3494
|
+
type: "git",
|
|
3495
|
+
url: skillId
|
|
3496
|
+
};
|
|
3497
|
+
const repoPrivacyPromise = Promise.resolve(null);
|
|
3498
|
+
if (source.split("/")[0]?.toLowerCase() === "openclaw" && !options.dangerouslyAcceptOpenclawRisks) {
|
|
3719
3499
|
console.log();
|
|
3720
3500
|
M.warn(import_picocolors.default.yellow(import_picocolors.default.bold("⚠ OpenClaw skills are unverified community submissions.")));
|
|
3721
3501
|
M.message(import_picocolors.default.yellow("This source contains user-submitted skills that have not been reviewed for safety or quality."));
|
|
@@ -3725,80 +3505,21 @@ async function runAdd(args, options = {}) {
|
|
|
3725
3505
|
Se(import_picocolors.default.red("Installation blocked"));
|
|
3726
3506
|
process.exit(1);
|
|
3727
3507
|
}
|
|
3728
|
-
if (parsed.type === "well-known") {
|
|
3729
|
-
await handleWellKnownSkills(source, parsed.url, options, spinner);
|
|
3730
|
-
return;
|
|
3731
|
-
}
|
|
3732
|
-
if (parsed.skillFilter) {
|
|
3733
|
-
options.skill = options.skill || [];
|
|
3734
|
-
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
3735
|
-
}
|
|
3736
3508
|
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
3737
3509
|
let skills;
|
|
3738
3510
|
let blobResult = null;
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
process.exit(1);
|
|
3745
|
-
}
|
|
3746
|
-
spinner.stop("Local path validated");
|
|
3747
|
-
spinner.start("Discovering skills...");
|
|
3748
|
-
skills = await discoverSkills(parsed.localPath, parsed.subpath, {
|
|
3749
|
-
includeInternal,
|
|
3750
|
-
fullDepth: options.fullDepth
|
|
3751
|
-
});
|
|
3752
|
-
} else if (parsed.type === "github" && !options.fullDepth) {
|
|
3753
|
-
const BLOB_ALLOWED_OWNERS = [
|
|
3754
|
-
"vercel",
|
|
3755
|
-
"vercel-labs",
|
|
3756
|
-
"heygen-com"
|
|
3757
|
-
];
|
|
3758
|
-
const ownerRepo = getOwnerRepo(parsed);
|
|
3759
|
-
const owner = ownerRepo?.split("/")[0]?.toLowerCase();
|
|
3760
|
-
const isSelfHostedRepo = !!ownerRepo && Object.hasOwn(BLOB_ALLOWED_REPOS, ownerRepo.toLowerCase());
|
|
3761
|
-
if (ownerRepo && owner && (isSelfHostedRepo || BLOB_ALLOWED_OWNERS.includes(owner))) {
|
|
3762
|
-
spinner.start("Fetching skills...");
|
|
3763
|
-
blobResult = await tryBlobInstall(ownerRepo, {
|
|
3764
|
-
subpath: parsed.subpath,
|
|
3765
|
-
skillFilter: parsed.skillFilter,
|
|
3766
|
-
ref: parsed.ref,
|
|
3767
|
-
getToken: getGitHubToken,
|
|
3768
|
-
includeInternal
|
|
3769
|
-
});
|
|
3770
|
-
if (!blobResult) spinner.stop(import_picocolors.default.dim("Falling back to clone..."));
|
|
3771
|
-
}
|
|
3772
|
-
if (blobResult) {
|
|
3773
|
-
skills = blobResult.skills;
|
|
3774
|
-
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3775
|
-
} else {
|
|
3776
|
-
spinner.start("Cloning repository...");
|
|
3777
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
3778
|
-
spinner.stop("Repository cloned");
|
|
3779
|
-
spinner.start("Discovering skills...");
|
|
3780
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
3781
|
-
includeInternal,
|
|
3782
|
-
fullDepth: options.fullDepth
|
|
3783
|
-
});
|
|
3784
|
-
}
|
|
3785
|
-
} else {
|
|
3786
|
-
spinner.start("Cloning repository...");
|
|
3787
|
-
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
3788
|
-
spinner.stop("Repository cloned");
|
|
3789
|
-
spinner.start("Discovering skills...");
|
|
3790
|
-
skills = await discoverSkills(tempDir, parsed.subpath, {
|
|
3791
|
-
includeInternal,
|
|
3792
|
-
fullDepth: options.fullDepth
|
|
3793
|
-
});
|
|
3794
|
-
}
|
|
3511
|
+
spinner.start("Discovering skills...");
|
|
3512
|
+
skills = await discoverSkills(tempDir, void 0, {
|
|
3513
|
+
includeInternal,
|
|
3514
|
+
fullDepth: options.fullDepth
|
|
3515
|
+
});
|
|
3795
3516
|
if (skills.length === 0) {
|
|
3796
3517
|
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
3797
3518
|
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
3798
3519
|
await cleanup(tempDir);
|
|
3799
3520
|
process.exit(1);
|
|
3800
3521
|
}
|
|
3801
|
-
|
|
3522
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
3802
3523
|
if (options.list) {
|
|
3803
3524
|
console.log();
|
|
3804
3525
|
M.step(import_picocolors.default.bold("Available Skills"));
|
|
@@ -3896,7 +3617,8 @@ async function runAdd(args, options = {}) {
|
|
|
3896
3617
|
selectedSkills = selected;
|
|
3897
3618
|
}
|
|
3898
3619
|
const ownerRepoForAudit = getOwnerRepo(parsed);
|
|
3899
|
-
const
|
|
3620
|
+
const auditSkillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
|
|
3621
|
+
const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => s.name), parsed.type, auditSkillFiles) : Promise.resolve(null);
|
|
3900
3622
|
let targetAgents;
|
|
3901
3623
|
const validAgents = Object.keys(agents);
|
|
3902
3624
|
if (options.agent?.includes("*")) {
|
|
@@ -4097,18 +3819,28 @@ async function runAdd(args, options = {}) {
|
|
|
4097
3819
|
}
|
|
4098
3820
|
console.log();
|
|
4099
3821
|
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3822
|
+
const auditSkills = selectedSkills.map((s) => ({
|
|
3823
|
+
slug: getSkillDisplayName(s),
|
|
3824
|
+
displayName: getSkillDisplayName(s)
|
|
3825
|
+
}));
|
|
3826
|
+
let auditData = null;
|
|
4100
3827
|
try {
|
|
4101
|
-
|
|
3828
|
+
auditData = await auditPromise;
|
|
4102
3829
|
if (auditData && ownerRepoForAudit) {
|
|
4103
|
-
const securityLines = buildSecurityLines(auditData,
|
|
4104
|
-
slug: getSkillDisplayName(s),
|
|
4105
|
-
displayName: getSkillDisplayName(s)
|
|
4106
|
-
})), ownerRepoForAudit);
|
|
3830
|
+
const securityLines = buildSecurityLines(auditData, auditSkills);
|
|
4107
3831
|
if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
|
|
4108
3832
|
}
|
|
4109
3833
|
} catch {}
|
|
3834
|
+
const highRiskSkills = getHighRiskSkills(auditData, auditSkills);
|
|
3835
|
+
if (highRiskSkills.length > 0) {
|
|
3836
|
+
const list = highRiskSkills.map((s) => `${s.name} 经安全审计标记为 ${s.riskLevel} 风险`).join(",");
|
|
3837
|
+
M.warn(`${import_picocolors.default.red(import_picocolors.default.bold("⚠ 高风险警告:"))} ${list},请谨慎安装。`);
|
|
3838
|
+
}
|
|
4110
3839
|
if (!options.yes) {
|
|
4111
|
-
const confirmed = await ye({
|
|
3840
|
+
const confirmed = await ye({
|
|
3841
|
+
message: highRiskSkills.length > 0 ? `检测到以下技能存在安全风险:${highRiskSkills.map((s) => `${s.name} (${s.riskLevel})`).join(", ")}。仍要继续安装?` : "Proceed with installation?",
|
|
3842
|
+
initialValue: highRiskSkills.length === 0
|
|
3843
|
+
});
|
|
4112
3844
|
if (pD(confirmed) || !confirmed) {
|
|
4113
3845
|
xe("Installation cancelled");
|
|
4114
3846
|
await cleanup(tempDir);
|
|
@@ -4120,17 +3852,7 @@ async function runAdd(args, options = {}) {
|
|
|
4120
3852
|
for (const skill of selectedSkills) for (const target of installTargets) {
|
|
4121
3853
|
const { agent, subagent } = target;
|
|
4122
3854
|
let result;
|
|
4123
|
-
if (
|
|
4124
|
-
const blobSkill = skill;
|
|
4125
|
-
result = await installBlobSkillForAgent({
|
|
4126
|
-
installName: blobSkill.name,
|
|
4127
|
-
files: blobSkill.files
|
|
4128
|
-
}, agent, {
|
|
4129
|
-
global: installGlobally,
|
|
4130
|
-
mode: installMode,
|
|
4131
|
-
eveSubagent: subagent
|
|
4132
|
-
});
|
|
4133
|
-
} else if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
3855
|
+
if (tempDir && skill.path === tempDir && skill.rawContent) result = await installBlobSkillForAgent({
|
|
4134
3856
|
installName: skill.name,
|
|
4135
3857
|
files: [{
|
|
4136
3858
|
path: "SKILL.md",
|
|
@@ -4156,48 +3878,60 @@ async function runAdd(args, options = {}) {
|
|
|
4156
3878
|
console.log();
|
|
4157
3879
|
const successful = results.filter((r) => r.success);
|
|
4158
3880
|
const failed = results.filter((r) => !r.success);
|
|
4159
|
-
const skillFiles =
|
|
4160
|
-
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
4161
|
-
else if (tempDir && skill.path === tempDir) skillFiles[skill.name] = "SKILL.md";
|
|
4162
|
-
else if (tempDir && skill.path.startsWith(tempDir + sep)) skillFiles[skill.name] = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
4163
|
-
else continue;
|
|
3881
|
+
const skillFiles = buildSkillFiles(selectedSkills, tempDir, blobResult);
|
|
4164
3882
|
const normalizedSource = getOwnerRepo(parsed);
|
|
4165
3883
|
const lockSource = getLockSource(parsed.url, normalizedSource);
|
|
4166
3884
|
if (normalizedSource) if (parseOwnerRepo(normalizedSource)) {
|
|
4167
3885
|
const isPrivate = await repoPrivacyPromise;
|
|
4168
|
-
if (isPrivate === false || isPrivate === null && parsed.type !== "github")
|
|
3886
|
+
if (isPrivate === false || isPrivate === null && parsed.type !== "github") {
|
|
3887
|
+
console.log("[telemetry-debug-1]", JSON.stringify({
|
|
3888
|
+
isPrivate,
|
|
3889
|
+
parsedType: parsed.type,
|
|
3890
|
+
normalizedSource,
|
|
3891
|
+
sourceType: parsed.type,
|
|
3892
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3893
|
+
}));
|
|
3894
|
+
track({
|
|
3895
|
+
event: "install",
|
|
3896
|
+
source: normalizedSource,
|
|
3897
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
3898
|
+
agents: targetAgents.join(","),
|
|
3899
|
+
...installGlobally && { global: "1" },
|
|
3900
|
+
skillFiles: JSON.stringify(skillFiles),
|
|
3901
|
+
sourceType: parsed.type
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
} else {
|
|
3905
|
+
console.log("[telemetry-debug-2]", JSON.stringify({
|
|
3906
|
+
parsedType: parsed.type,
|
|
3907
|
+
normalizedSource,
|
|
3908
|
+
sourceType: parsed.type,
|
|
3909
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3910
|
+
}));
|
|
3911
|
+
track({
|
|
4169
3912
|
event: "install",
|
|
4170
3913
|
source: normalizedSource,
|
|
4171
3914
|
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4172
3915
|
agents: targetAgents.join(","),
|
|
4173
3916
|
...installGlobally && { global: "1" },
|
|
4174
|
-
skillFiles: JSON.stringify(skillFiles)
|
|
3917
|
+
skillFiles: JSON.stringify(skillFiles),
|
|
3918
|
+
sourceType: parsed.type
|
|
4175
3919
|
});
|
|
4176
|
-
}
|
|
4177
|
-
event: "install",
|
|
4178
|
-
source: normalizedSource,
|
|
4179
|
-
skills: selectedSkills.map((s) => s.name).join(","),
|
|
4180
|
-
agents: targetAgents.join(","),
|
|
4181
|
-
...installGlobally && { global: "1" },
|
|
4182
|
-
skillFiles: JSON.stringify(skillFiles)
|
|
4183
|
-
});
|
|
3920
|
+
}
|
|
4184
3921
|
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
4185
3922
|
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
4186
3923
|
let cachedTree;
|
|
4187
|
-
if (parsed.type === "github" &&
|
|
3924
|
+
if (parsed.type === "github" && true) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
|
|
4188
3925
|
for (const skill of selectedSkills) {
|
|
4189
3926
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4190
3927
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4191
3928
|
let skillFolderHash = "";
|
|
4192
3929
|
const skillPathValue = skillFiles[skill.name];
|
|
4193
|
-
if (
|
|
4194
|
-
const hash = getSkillFolderHashFromTree(blobResult.tree, skillPathValue);
|
|
4195
|
-
if (hash) skillFolderHash = hash;
|
|
4196
|
-
} else if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
3930
|
+
if (parsed.type === "github" && skillPathValue && cachedTree) {
|
|
4197
3931
|
const hash = getSkillFolderHashFromTree(cachedTree, skillPathValue);
|
|
4198
3932
|
if (hash) skillFolderHash = hash;
|
|
4199
3933
|
} else if (skillPathValue && tempDir) {
|
|
4200
|
-
const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
|
|
3934
|
+
const hash = await computeSkillFolderHash(join$1(tempDir, dirname$1(skillPathValue)));
|
|
4201
3935
|
if (hash) skillFolderHash = hash;
|
|
4202
3936
|
}
|
|
4203
3937
|
await addSkillToLock(skill.name, {
|
|
@@ -4219,7 +3953,7 @@ async function runAdd(args, options = {}) {
|
|
|
4219
3953
|
for (const skill of selectedSkills) {
|
|
4220
3954
|
const skillDisplayName = getSkillDisplayName(skill);
|
|
4221
3955
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4222
|
-
const computedHash =
|
|
3956
|
+
const computedHash = tempDir && skill.path === tempDir && skill.rawContent ? computeSingleFileSkillHash(skill.rawContent) : await computeSkillFolderHash(skill.path);
|
|
4223
3957
|
const skillPathValue = skillFiles[skill.name];
|
|
4224
3958
|
await addSkillToLocalLock(skill.name, {
|
|
4225
3959
|
source: lockSource || parsed.url,
|
|
@@ -4400,18 +4134,300 @@ function parseAddOptions(args) {
|
|
|
4400
4134
|
options
|
|
4401
4135
|
};
|
|
4402
4136
|
}
|
|
4137
|
+
const SOURCE_TYPE_PREFIXES = [
|
|
4138
|
+
"github",
|
|
4139
|
+
"gitcode",
|
|
4140
|
+
"gitlab",
|
|
4141
|
+
"gitee"
|
|
4142
|
+
];
|
|
4143
|
+
function normalizeSkillId(input) {
|
|
4144
|
+
const trimmed = input.trim();
|
|
4145
|
+
const first = trimmed.split("/")[0];
|
|
4146
|
+
if (first && SOURCE_TYPE_PREFIXES.includes(first.toLowerCase())) return trimmed;
|
|
4147
|
+
return `github/${trimmed}`;
|
|
4148
|
+
}
|
|
4149
|
+
function buildSkillIdFromSource(source, skillName) {
|
|
4150
|
+
const parsed = parseSource(source);
|
|
4151
|
+
if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return null;
|
|
4152
|
+
const ownerRepo = getOwnerRepo(parsed);
|
|
4153
|
+
if (!ownerRepo) return null;
|
|
4154
|
+
const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
|
|
4155
|
+
const skillPath = slugifyTelemetryValue(skillName);
|
|
4156
|
+
if (!skillPath) return null;
|
|
4157
|
+
return `${parsed.type}/${slugifiedOwnerRepo}/${skillPath}`;
|
|
4158
|
+
}
|
|
4159
|
+
async function listSkillsByRepo(source) {
|
|
4160
|
+
const parsed = parseSource(source);
|
|
4161
|
+
if (!SOURCE_TYPE_PREFIXES.includes(parsed.type)) return [];
|
|
4162
|
+
const ownerRepo = getOwnerRepo(parsed);
|
|
4163
|
+
if (!ownerRepo) return [];
|
|
4164
|
+
const slugifiedOwnerRepo = ownerRepo.split("/").map(slugifyTelemetryValue).join("/");
|
|
4165
|
+
if (!slugifiedOwnerRepo || slugifiedOwnerRepo.split("/").length < 2) return [];
|
|
4166
|
+
const url = `${LIST_URL}?${new URLSearchParams({
|
|
4167
|
+
source_type: parsed.type,
|
|
4168
|
+
repo: slugifiedOwnerRepo,
|
|
4169
|
+
limit: "100"
|
|
4170
|
+
}).toString()}`;
|
|
4171
|
+
try {
|
|
4172
|
+
const res = await fetch(url);
|
|
4173
|
+
if (!res.ok) return [];
|
|
4174
|
+
const raw = await res.json();
|
|
4175
|
+
return ((raw.data || raw).skills ?? []).filter((skill) => skill && typeof skill.skill_id === "string" && skill.skill_id).map((skill) => ({
|
|
4176
|
+
skill_id: skill.skill_id,
|
|
4177
|
+
name: skill.name ?? "",
|
|
4178
|
+
source_url: skill.source_url ?? ""
|
|
4179
|
+
}));
|
|
4180
|
+
} catch {
|
|
4181
|
+
return [];
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
async function resolveBySearch(source, skillName) {
|
|
4185
|
+
let results = [];
|
|
4186
|
+
try {
|
|
4187
|
+
const params = new URLSearchParams({
|
|
4188
|
+
q: skillName,
|
|
4189
|
+
limit: "10",
|
|
4190
|
+
mode: "text"
|
|
4191
|
+
});
|
|
4192
|
+
const res = await fetch(`${SEARCH_URL}?${params.toString()}`);
|
|
4193
|
+
if (res.ok) {
|
|
4194
|
+
const raw = await res.json();
|
|
4195
|
+
results = (raw.data || raw).results ?? [];
|
|
4196
|
+
}
|
|
4197
|
+
} catch {
|
|
4198
|
+
return null;
|
|
4199
|
+
}
|
|
4200
|
+
const sourceLower = source.trim().toLowerCase();
|
|
4201
|
+
const matches = results.filter((skill) => {
|
|
4202
|
+
const url = (skill.source_url || "").toLowerCase();
|
|
4203
|
+
if (!url) return false;
|
|
4204
|
+
if (url.startsWith(sourceLower)) return true;
|
|
4205
|
+
if (!sourceLower.includes("://") && sourceLower.split("/").length === 2) return url.includes(`/${sourceLower}/`);
|
|
4206
|
+
return false;
|
|
4207
|
+
});
|
|
4208
|
+
const exact = matches.find((skill) => skill.name.toLowerCase() === skillName.trim().toLowerCase());
|
|
4209
|
+
return exact ? exact.skill_id : matches.length > 0 ? matches[0].skill_id : null;
|
|
4210
|
+
}
|
|
4211
|
+
async function resolveSkillIdBySourceAndName(source, skillName) {
|
|
4212
|
+
const repoSkills = await listSkillsByRepo(source);
|
|
4213
|
+
if (repoSkills.length > 0) {
|
|
4214
|
+
const nameLower = skillName.trim().toLowerCase();
|
|
4215
|
+
const exact = repoSkills.find((skill) => skill.name.toLowerCase() === nameLower);
|
|
4216
|
+
if (exact) return exact.skill_id;
|
|
4217
|
+
const slug = slugifyTelemetryValue(skillName);
|
|
4218
|
+
if (slug) {
|
|
4219
|
+
const bySlug = repoSkills.find((skill) => {
|
|
4220
|
+
return (skill.skill_id.split("/").pop() ?? "").toLowerCase() === slug;
|
|
4221
|
+
});
|
|
4222
|
+
if (bySlug) return bySlug.skill_id;
|
|
4223
|
+
}
|
|
4224
|
+
return null;
|
|
4225
|
+
}
|
|
4226
|
+
return resolveBySearch(source, skillName);
|
|
4227
|
+
}
|
|
4228
|
+
async function resolveSkillId(source, skillName) {
|
|
4229
|
+
if (!skillName) return {
|
|
4230
|
+
skillId: normalizeSkillId(source),
|
|
4231
|
+
derived: false
|
|
4232
|
+
};
|
|
4233
|
+
const resolved = await resolveSkillIdBySourceAndName(source, skillName);
|
|
4234
|
+
if (resolved) return {
|
|
4235
|
+
skillId: resolved,
|
|
4236
|
+
derived: false
|
|
4237
|
+
};
|
|
4238
|
+
const derived = buildSkillIdFromSource(source, skillName);
|
|
4239
|
+
if (derived) return {
|
|
4240
|
+
skillId: derived,
|
|
4241
|
+
derived: true
|
|
4242
|
+
};
|
|
4243
|
+
return {
|
|
4244
|
+
skillId: null,
|
|
4245
|
+
derived: false
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
function parseAuditOptions(args) {
|
|
4249
|
+
let source = "";
|
|
4250
|
+
let skill = "";
|
|
4251
|
+
const errors = [];
|
|
4252
|
+
for (let i = 0; i < args.length; i++) {
|
|
4253
|
+
const arg = args[i];
|
|
4254
|
+
if (arg === "-s" || arg === "--skill") {
|
|
4255
|
+
i++;
|
|
4256
|
+
skill = args[i]?.trim() ?? "";
|
|
4257
|
+
} else if (arg && !arg.startsWith("-")) source = arg.trim();
|
|
4258
|
+
}
|
|
4259
|
+
if (!source) errors.push("Missing source or skill id");
|
|
4260
|
+
return {
|
|
4261
|
+
source,
|
|
4262
|
+
skill,
|
|
4263
|
+
errors
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
async function fetchSkillAudit(skillId, timeoutMs = 15e3) {
|
|
4267
|
+
const controller = new AbortController();
|
|
4268
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4269
|
+
try {
|
|
4270
|
+
const url = AUDIT_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
|
|
4271
|
+
let response;
|
|
4272
|
+
try {
|
|
4273
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4274
|
+
} catch {
|
|
4275
|
+
return {
|
|
4276
|
+
status: "error",
|
|
4277
|
+
message: `无法连接审计服务: ${AUDIT_URL}`
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4281
|
+
if (!response.ok) return {
|
|
4282
|
+
status: "error",
|
|
4283
|
+
message: `审计服务返回 HTTP ${response.status}`
|
|
4284
|
+
};
|
|
4285
|
+
let data;
|
|
4286
|
+
try {
|
|
4287
|
+
data = await response.json();
|
|
4288
|
+
} catch {
|
|
4289
|
+
return {
|
|
4290
|
+
status: "error",
|
|
4291
|
+
message: "审计服务响应格式无效"
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
if (data.error) return {
|
|
4295
|
+
status: "no_audit",
|
|
4296
|
+
message: String(data.error)
|
|
4297
|
+
};
|
|
4298
|
+
const auditData = data.data || data;
|
|
4299
|
+
return {
|
|
4300
|
+
status: "ok",
|
|
4301
|
+
data: {
|
|
4302
|
+
risk_level: auditData.risk_level ?? "unknown",
|
|
4303
|
+
risk_score: auditData.risk_score ?? null,
|
|
4304
|
+
risk_signals: auditData.risk_signals ?? [],
|
|
4305
|
+
audited_at: auditData.audited_at ?? null,
|
|
4306
|
+
audit_type: auditData.audit_type ?? null,
|
|
4307
|
+
details: auditData.details ?? null
|
|
4308
|
+
}
|
|
4309
|
+
};
|
|
4310
|
+
} finally {
|
|
4311
|
+
clearTimeout(timeout);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function riskColor(level) {
|
|
4315
|
+
switch (level) {
|
|
4316
|
+
case "critical":
|
|
4317
|
+
case "high": return import_picocolors.default.red;
|
|
4318
|
+
case "medium": return import_picocolors.default.yellow;
|
|
4319
|
+
case "low":
|
|
4320
|
+
case "safe": return import_picocolors.default.green;
|
|
4321
|
+
default: return import_picocolors.default.dim;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
const RISK_LABELS = {
|
|
4325
|
+
critical: "Critical",
|
|
4326
|
+
high: "High",
|
|
4327
|
+
medium: "Medium",
|
|
4328
|
+
low: "Low",
|
|
4329
|
+
safe: "Safe"
|
|
4330
|
+
};
|
|
4331
|
+
function riskLabel(level) {
|
|
4332
|
+
const color = riskColor(level);
|
|
4333
|
+
const label = RISK_LABELS[level] || level || "Unknown";
|
|
4334
|
+
return level === "critical" ? color(import_picocolors.default.bold(label)) : color(label);
|
|
4335
|
+
}
|
|
4336
|
+
function severityColor(severity) {
|
|
4337
|
+
const s = severity.toLowerCase();
|
|
4338
|
+
if (s === "critical" || s === "high") return import_picocolors.default.red(severity);
|
|
4339
|
+
if (s === "medium") return import_picocolors.default.yellow(severity);
|
|
4340
|
+
if (s === "low") return import_picocolors.default.green(severity);
|
|
4341
|
+
return import_picocolors.default.dim(severity);
|
|
4342
|
+
}
|
|
4343
|
+
function buildAuditOutput(skillId, data) {
|
|
4344
|
+
const lines = [];
|
|
4345
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(skillId)}`);
|
|
4346
|
+
const color = riskColor(data.risk_level);
|
|
4347
|
+
const score = data.risk_score != null ? color(`(score ${data.risk_score})`) : color("(score --)");
|
|
4348
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel(data.risk_level)} ${score}`);
|
|
4349
|
+
if (data.audited_at) lines.push(`${import_picocolors.default.bold("Audited at:")} ${import_picocolors.default.dim(data.audited_at)}`);
|
|
4350
|
+
lines.push("");
|
|
4351
|
+
if (data.risk_signals.length === 0) {
|
|
4352
|
+
lines.push(import_picocolors.default.green("No risk signals detected."));
|
|
4353
|
+
return lines;
|
|
4354
|
+
}
|
|
4355
|
+
lines.push(import_picocolors.default.bold("Risk signals:"));
|
|
4356
|
+
for (const signal of data.risk_signals) {
|
|
4357
|
+
const severity = severityColor(signal.severity || "unknown");
|
|
4358
|
+
lines.push(` ${import_picocolors.default.bold(signal.name)} ${import_picocolors.default.dim(`[${severity}]`)}`);
|
|
4359
|
+
if (signal.description) lines.push(` ${import_picocolors.default.dim(signal.description)}`);
|
|
4360
|
+
}
|
|
4361
|
+
return lines;
|
|
4362
|
+
}
|
|
4363
|
+
async function runAudit(args) {
|
|
4364
|
+
const { source, skill, errors } = parseAuditOptions(args);
|
|
4365
|
+
if (errors.length > 0) {
|
|
4366
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
4367
|
+
console.error("Usage: wittyhub audit <source> --skill <skill>");
|
|
4368
|
+
console.error(" wittyhub audit <skill_id>");
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
const { skillId, derived } = await resolveSkillId(source, skill || void 0);
|
|
4372
|
+
if (!skillId) {
|
|
4373
|
+
console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
|
|
4374
|
+
console.error("Usage: wittyhub audit <source> --skill <skill>");
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
|
|
4378
|
+
const result = await fetchSkillAudit(skillId);
|
|
4379
|
+
track({
|
|
4380
|
+
event: "audit",
|
|
4381
|
+
skillId,
|
|
4382
|
+
found: result.status === "ok" ? "1" : "0"
|
|
4383
|
+
});
|
|
4384
|
+
switch (result.status) {
|
|
4385
|
+
case "ok":
|
|
4386
|
+
for (const line of buildAuditOutput(skillId, result.data)) console.log(line);
|
|
4387
|
+
break;
|
|
4388
|
+
case "no_audit":
|
|
4389
|
+
console.log(`${import_picocolors.default.yellow("No audit result yet.")} ${import_picocolors.default.dim(result.message)}`);
|
|
4390
|
+
break;
|
|
4391
|
+
case "not_found":
|
|
4392
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
4393
|
+
break;
|
|
4394
|
+
case "error":
|
|
4395
|
+
console.error(import_picocolors.default.red(result.message));
|
|
4396
|
+
break;
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4403
4399
|
const RESET$3 = "\x1B[0m";
|
|
4404
4400
|
const BOLD$3 = "\x1B[1m";
|
|
4405
4401
|
const DIM$3 = "\x1B[38;5;102m";
|
|
4406
4402
|
const TEXT$2 = "\x1B[38;5;145m";
|
|
4407
4403
|
const CYAN$1 = "\x1B[36m";
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4410
|
-
|
|
4404
|
+
const MAGENTA = "\x1B[35m";
|
|
4405
|
+
const YELLOW$1 = "\x1B[33m";
|
|
4406
|
+
const RED = "\x1B[31m";
|
|
4407
|
+
const GREEN = "\x1B[32m";
|
|
4408
|
+
function formatInstalls$1(count) {
|
|
4409
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "";
|
|
4410
|
+
if (count <= 0) return "0 installs";
|
|
4411
4411
|
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
4412
4412
|
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
4413
4413
|
return `${count} install${count === 1 ? "" : "s"}`;
|
|
4414
4414
|
}
|
|
4415
|
+
function formatRiskLevel$1(riskScore) {
|
|
4416
|
+
if (riskScore === void 0 || riskScore === null) return "";
|
|
4417
|
+
if (riskScore <= 20) return `${GREEN}safe risk${RESET$3}`;
|
|
4418
|
+
if (riskScore <= 50) return `${GREEN}low risk${RESET$3}`;
|
|
4419
|
+
if (riskScore <= 80) return `${YELLOW$1}medium risk${RESET$3}`;
|
|
4420
|
+
return `${RED}high risk${RESET$3}`;
|
|
4421
|
+
}
|
|
4422
|
+
function escapeRegExp(s) {
|
|
4423
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4424
|
+
}
|
|
4425
|
+
function highlightMatches(text, query) {
|
|
4426
|
+
if (!text || !query) return text;
|
|
4427
|
+
const terms = query.trim().split(/\s+/).filter(Boolean).map(escapeRegExp);
|
|
4428
|
+
if (terms.length === 0) return text;
|
|
4429
|
+
return text.replace(new RegExp(`(${terms.join("|")})`, "gi"), `${MAGENTA}$1${RESET$3}`);
|
|
4430
|
+
}
|
|
4415
4431
|
const GITHUB_OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,38})$/i;
|
|
4416
4432
|
function parseFindOptions(args) {
|
|
4417
4433
|
const queryParts = [];
|
|
@@ -4424,7 +4440,7 @@ function parseFindOptions(args) {
|
|
|
4424
4440
|
if (arg === "--owner") {
|
|
4425
4441
|
const value = args[i + 1];
|
|
4426
4442
|
if (!value || value.startsWith("-")) {
|
|
4427
|
-
errors.push("--owner requires
|
|
4443
|
+
errors.push("--owner requires an owner");
|
|
4428
4444
|
continue;
|
|
4429
4445
|
}
|
|
4430
4446
|
ownerValue = value;
|
|
@@ -4432,7 +4448,7 @@ function parseFindOptions(args) {
|
|
|
4432
4448
|
} else if (arg.startsWith("--owner=")) {
|
|
4433
4449
|
ownerValue = arg.slice(8);
|
|
4434
4450
|
if (!ownerValue) {
|
|
4435
|
-
errors.push("--owner requires
|
|
4451
|
+
errors.push("--owner requires an owner");
|
|
4436
4452
|
continue;
|
|
4437
4453
|
}
|
|
4438
4454
|
} else {
|
|
@@ -4441,7 +4457,7 @@ function parseFindOptions(args) {
|
|
|
4441
4457
|
}
|
|
4442
4458
|
const owner = ownerValue.trim().toLowerCase();
|
|
4443
4459
|
if (!GITHUB_OWNER_PATTERN.test(owner)) {
|
|
4444
|
-
errors.push("--owner must be a valid
|
|
4460
|
+
errors.push("--owner must be a valid owner");
|
|
4445
4461
|
continue;
|
|
4446
4462
|
}
|
|
4447
4463
|
options.owner = owner;
|
|
@@ -4453,24 +4469,52 @@ function parseFindOptions(args) {
|
|
|
4453
4469
|
};
|
|
4454
4470
|
}
|
|
4455
4471
|
async function searchSkillsAPI(query, owner) {
|
|
4472
|
+
const url = `${SEARCH_URL}?${new URLSearchParams({
|
|
4473
|
+
q: query,
|
|
4474
|
+
limit: "10",
|
|
4475
|
+
mode: "text"
|
|
4476
|
+
}).toString()}`;
|
|
4477
|
+
let res;
|
|
4456
4478
|
try {
|
|
4457
|
-
|
|
4458
|
-
q: query,
|
|
4459
|
-
limit: "10"
|
|
4460
|
-
});
|
|
4461
|
-
if (owner) params.set("owner", owner);
|
|
4462
|
-
const url = `${SEARCH_API_BASE}/api/search?${params.toString()}`;
|
|
4463
|
-
const res = await fetch(url);
|
|
4464
|
-
if (!res.ok) return [];
|
|
4465
|
-
return (await res.json()).skills.map((skill) => ({
|
|
4466
|
-
name: sanitizeMetadata(skill.name),
|
|
4467
|
-
slug: sanitizeMetadata(skill.id),
|
|
4468
|
-
source: sanitizeMetadata(skill.source || ""),
|
|
4469
|
-
installs: skill.installs
|
|
4470
|
-
})).sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
|
4479
|
+
res = await fetch(url);
|
|
4471
4480
|
} catch {
|
|
4472
|
-
return
|
|
4481
|
+
return {
|
|
4482
|
+
skills: [],
|
|
4483
|
+
error: `无法连接搜索服务: ${SEARCH_URL}`
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
if (!res.ok) return {
|
|
4487
|
+
skills: [],
|
|
4488
|
+
error: `搜索服务返回 HTTP ${res.status}`
|
|
4489
|
+
};
|
|
4490
|
+
let data;
|
|
4491
|
+
try {
|
|
4492
|
+
data = await res.json();
|
|
4493
|
+
} catch {
|
|
4494
|
+
return {
|
|
4495
|
+
skills: [],
|
|
4496
|
+
error: "搜索服务响应格式无效"
|
|
4497
|
+
};
|
|
4473
4498
|
}
|
|
4499
|
+
let skills = (data.data?.results ?? data.results ?? []).map((skill) => ({
|
|
4500
|
+
name: sanitizeMetadata(skill.name),
|
|
4501
|
+
slug: sanitizeMetadata(skill.skill_id),
|
|
4502
|
+
source: sanitizeMetadata(skill.source || ""),
|
|
4503
|
+
installs: skill.download_count || 0,
|
|
4504
|
+
description: skill.description ? sanitizeMetadata(skill.description) : void 0,
|
|
4505
|
+
riskScore: skill.risk_score ?? void 0,
|
|
4506
|
+
sourceUrl: skill.source_url ? sanitizeMetadata(skill.source_url) : void 0
|
|
4507
|
+
}));
|
|
4508
|
+
if (owner) {
|
|
4509
|
+
const ownerLower = owner.toLowerCase();
|
|
4510
|
+
skills = skills.filter((skill) => {
|
|
4511
|
+
if (skill.sourceUrl) try {
|
|
4512
|
+
if (new URL(skill.sourceUrl).pathname.split("/").filter(Boolean)[0]?.toLowerCase() === ownerLower) return true;
|
|
4513
|
+
} catch {}
|
|
4514
|
+
return skill.slug.toLowerCase().includes(`/${ownerLower}/`);
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
return { skills: skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)) };
|
|
4474
4518
|
}
|
|
4475
4519
|
const HIDE_CURSOR = "\x1B[?25l";
|
|
4476
4520
|
const SHOW_CURSOR = "\x1B[?25h";
|
|
@@ -4482,6 +4526,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4482
4526
|
let selectedIndex = 0;
|
|
4483
4527
|
let query = initialQuery;
|
|
4484
4528
|
let loading = false;
|
|
4529
|
+
let searchError = null;
|
|
4485
4530
|
let debounceTimer = null;
|
|
4486
4531
|
let lastRenderedLines = 0;
|
|
4487
4532
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
@@ -4497,6 +4542,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4497
4542
|
lines.push("");
|
|
4498
4543
|
if (!query || query.length < 2) lines.push(`${DIM$3}Start typing to search (min 2 chars)${RESET$3}`);
|
|
4499
4544
|
else if (results.length === 0 && loading) lines.push(`${DIM$3}Searching...${RESET$3}`);
|
|
4545
|
+
else if (results.length === 0 && searchError) lines.push(`${RED}${searchError}${RESET$3}`);
|
|
4500
4546
|
else if (results.length === 0) lines.push(`${DIM$3}No skills found${RESET$3}`);
|
|
4501
4547
|
else {
|
|
4502
4548
|
const visible = results.slice(0, 8);
|
|
@@ -4504,12 +4550,23 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4504
4550
|
const skill = visible[i];
|
|
4505
4551
|
const isSelected = i === selectedIndex;
|
|
4506
4552
|
const arrow = isSelected ? `${BOLD$3}>${RESET$3}` : " ";
|
|
4507
|
-
const
|
|
4553
|
+
const hlName = highlightMatches(skill.name, query);
|
|
4554
|
+
const name = isSelected ? `${BOLD$3}${hlName}${RESET$3}` : `${TEXT$2}${hlName}${RESET$3}`;
|
|
4508
4555
|
const source = skill.source ? ` ${DIM$3}${skill.source}${RESET$3}` : "";
|
|
4509
|
-
const installs = formatInstalls(skill.installs);
|
|
4556
|
+
const installs = formatInstalls$1(skill.installs);
|
|
4510
4557
|
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
|
|
4558
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4559
|
+
const riskBadge = risk ? ` ${risk}` : "";
|
|
4511
4560
|
const loadingIndicator = loading && i === 0 ? ` ${DIM$3}...${RESET$3}` : "";
|
|
4512
|
-
lines.push(` ${arrow} ${name}${source}${
|
|
4561
|
+
lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
|
|
4562
|
+
if (installsBadge || riskBadge) {
|
|
4563
|
+
const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
|
|
4564
|
+
lines.push(` ${meta}`);
|
|
4565
|
+
}
|
|
4566
|
+
if (skill.description) {
|
|
4567
|
+
const descPlain = skill.description.length > 80 ? `${skill.description.slice(0, 77)}...` : skill.description;
|
|
4568
|
+
lines.push(` ${DIM$3}${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4569
|
+
}
|
|
4513
4570
|
}
|
|
4514
4571
|
}
|
|
4515
4572
|
lines.push("");
|
|
@@ -4525,6 +4582,7 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4525
4582
|
loading = false;
|
|
4526
4583
|
if (!q || q.length < 2) {
|
|
4527
4584
|
results = [];
|
|
4585
|
+
searchError = null;
|
|
4528
4586
|
selectedIndex = 0;
|
|
4529
4587
|
render();
|
|
4530
4588
|
return;
|
|
@@ -4534,10 +4592,13 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4534
4592
|
const debounceMs = Math.max(150, 350 - q.length * 50);
|
|
4535
4593
|
debounceTimer = setTimeout(async () => {
|
|
4536
4594
|
try {
|
|
4537
|
-
|
|
4595
|
+
const result = await searchSkillsAPI(q, owner);
|
|
4596
|
+
results = result.skills;
|
|
4597
|
+
searchError = result.error ?? null;
|
|
4538
4598
|
selectedIndex = 0;
|
|
4539
4599
|
} catch {
|
|
4540
4600
|
results = [];
|
|
4601
|
+
searchError = null;
|
|
4541
4602
|
} finally {
|
|
4542
4603
|
loading = false;
|
|
4543
4604
|
debounceTimer = null;
|
|
@@ -4596,10 +4657,26 @@ async function runSearchPrompt(initialQuery = "", owner) {
|
|
|
4596
4657
|
}
|
|
4597
4658
|
function getOwnerRepoFromString(pkg) {
|
|
4598
4659
|
const atIndex = pkg.lastIndexOf("@");
|
|
4599
|
-
const
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4660
|
+
const repoPath = atIndex > 0 ? pkg.slice(0, atIndex) : pkg;
|
|
4661
|
+
const urlMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
4662
|
+
if (urlMatch) return {
|
|
4663
|
+
owner: urlMatch[1],
|
|
4664
|
+
repo: urlMatch[2]
|
|
4665
|
+
};
|
|
4666
|
+
const blobMatch = repoPath.match(/^https?:\/\/[^/]+\/([^/]+)\/([^/]+?)\/blob\//);
|
|
4667
|
+
if (blobMatch) return {
|
|
4668
|
+
owner: blobMatch[1],
|
|
4669
|
+
repo: blobMatch[2]
|
|
4670
|
+
};
|
|
4671
|
+
const shorthandMatch = repoPath.match(/^([^/]+)\/([^/]+)$/);
|
|
4672
|
+
if (shorthandMatch) return {
|
|
4673
|
+
owner: shorthandMatch[1],
|
|
4674
|
+
repo: shorthandMatch[2]
|
|
4675
|
+
};
|
|
4676
|
+
const slugMatch = repoPath.match(/^[^/]+\/([^/]+)\/([^/]+)/);
|
|
4677
|
+
if (slugMatch) return {
|
|
4678
|
+
owner: slugMatch[1],
|
|
4679
|
+
repo: slugMatch[2]
|
|
4603
4680
|
};
|
|
4604
4681
|
return null;
|
|
4605
4682
|
}
|
|
@@ -4619,24 +4696,41 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4619
4696
|
return;
|
|
4620
4697
|
}
|
|
4621
4698
|
if (query) {
|
|
4622
|
-
const results = await searchSkillsAPI(query, owner);
|
|
4699
|
+
const { skills: results, error: searchError } = await searchSkillsAPI(query, owner);
|
|
4623
4700
|
track({
|
|
4624
4701
|
event: "find",
|
|
4625
4702
|
query,
|
|
4626
4703
|
resultCount: String(results.length)
|
|
4627
4704
|
});
|
|
4705
|
+
if (searchError) {
|
|
4706
|
+
console.error(`${RED}${searchError}${RESET$3}`);
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4628
4709
|
if (results.length === 0) {
|
|
4629
4710
|
const ownerSuffix = owner ? ` from owner "${owner}"` : "";
|
|
4630
4711
|
console.log(`${DIM$3}No skills found for "${query}"${ownerSuffix}${RESET$3}`);
|
|
4631
4712
|
return;
|
|
4632
4713
|
}
|
|
4633
|
-
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <
|
|
4714
|
+
console.log(`${DIM$3}Install with${RESET$3} npx wittyhub add <repo-url> --skill <skill>`);
|
|
4634
4715
|
console.log();
|
|
4635
4716
|
for (const skill of results.slice(0, 6)) {
|
|
4636
|
-
const
|
|
4637
|
-
const
|
|
4638
|
-
|
|
4639
|
-
|
|
4717
|
+
const raw = skill.sourceUrl || skill.source || skill.slug;
|
|
4718
|
+
const hlName = highlightMatches(skill.name, query);
|
|
4719
|
+
const installTarget = skill.sourceUrl ? `${skill.sourceUrl} --skill ${hlName}` : `${raw}@${hlName}`;
|
|
4720
|
+
const installs = formatInstalls$1(skill.installs);
|
|
4721
|
+
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$3}` : "";
|
|
4722
|
+
const risk = formatRiskLevel$1(skill.riskScore);
|
|
4723
|
+
const riskBadge = risk ? ` ${risk}` : "";
|
|
4724
|
+
console.log(`${TEXT$2}${installTarget}${RESET$3}`);
|
|
4725
|
+
if (installsBadge || riskBadge) {
|
|
4726
|
+
const meta = [installsBadge.trimStart(), riskBadge.trimStart()].filter(Boolean).join(" ");
|
|
4727
|
+
console.log(` ${meta}`);
|
|
4728
|
+
}
|
|
4729
|
+
if (skill.description) {
|
|
4730
|
+
const descPlain = skill.description.length > 100 ? `${skill.description.slice(0, 97)}...` : skill.description;
|
|
4731
|
+
console.log(`${DIM$3} ${highlightMatches(descPlain, query)}${RESET$3}`);
|
|
4732
|
+
}
|
|
4733
|
+
console.log(`${DIM$3}└ ${skill.sourceUrl || `https://skills.sh/${skill.slug}`}${RESET$3}`);
|
|
4640
4734
|
console.log();
|
|
4641
4735
|
}
|
|
4642
4736
|
return;
|
|
@@ -4659,7 +4753,7 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4659
4753
|
console.log();
|
|
4660
4754
|
return;
|
|
4661
4755
|
}
|
|
4662
|
-
const pkg = selected.source || selected.slug;
|
|
4756
|
+
const pkg = selected.sourceUrl || selected.source || selected.slug;
|
|
4663
4757
|
const skillName = selected.name;
|
|
4664
4758
|
console.log();
|
|
4665
4759
|
console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
|
|
@@ -4676,9 +4770,156 @@ ${DIM$3} 2) npx wittyhub add <owner/repo@skill>${RESET$3}`;
|
|
|
4676
4770
|
else console.log(`${DIM$3}Discover more skills at${RESET$3} ${TEXT$2}https://skills.sh${RESET$3}`);
|
|
4677
4771
|
console.log();
|
|
4678
4772
|
}
|
|
4773
|
+
function parseGetOptions(args) {
|
|
4774
|
+
let source = "";
|
|
4775
|
+
let skill = "";
|
|
4776
|
+
const errors = [];
|
|
4777
|
+
for (let i = 0; i < args.length; i++) {
|
|
4778
|
+
const arg = args[i];
|
|
4779
|
+
if (arg === "-s" || arg === "--skill") {
|
|
4780
|
+
i++;
|
|
4781
|
+
skill = args[i]?.trim() ?? "";
|
|
4782
|
+
} else if (arg && !arg.startsWith("-")) source = arg.trim();
|
|
4783
|
+
}
|
|
4784
|
+
if (!source) errors.push("Missing source or skill id");
|
|
4785
|
+
return {
|
|
4786
|
+
source,
|
|
4787
|
+
skill,
|
|
4788
|
+
errors
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
async function fetchSkillDetail(skillId, timeoutMs = 15e3) {
|
|
4792
|
+
const controller = new AbortController();
|
|
4793
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
4794
|
+
try {
|
|
4795
|
+
const url = GET_URL.replace("{skill_id}", skillId.split("/").map(encodeURIComponent).join("/"));
|
|
4796
|
+
let response;
|
|
4797
|
+
try {
|
|
4798
|
+
response = await fetch(url, { signal: controller.signal });
|
|
4799
|
+
} catch {
|
|
4800
|
+
return {
|
|
4801
|
+
status: "error",
|
|
4802
|
+
message: `无法连接详情服务: ${GET_URL}`
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
if (response.status === 404) return { status: "not_found" };
|
|
4806
|
+
if (!response.ok) return {
|
|
4807
|
+
status: "error",
|
|
4808
|
+
message: `详情服务返回 HTTP ${response.status}`
|
|
4809
|
+
};
|
|
4810
|
+
let data;
|
|
4811
|
+
try {
|
|
4812
|
+
data = await response.json();
|
|
4813
|
+
} catch {
|
|
4814
|
+
return {
|
|
4815
|
+
status: "error",
|
|
4816
|
+
message: "详情服务响应格式无效"
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
if (data.error) return {
|
|
4820
|
+
status: "error",
|
|
4821
|
+
message: String(data.error)
|
|
4822
|
+
};
|
|
4823
|
+
const skillData = data.data || data;
|
|
4824
|
+
return {
|
|
4825
|
+
status: "ok",
|
|
4826
|
+
data: {
|
|
4827
|
+
skill_id: skillData.skill_id ?? skillId,
|
|
4828
|
+
name: skillData.name ?? "",
|
|
4829
|
+
description: skillData.description ?? null,
|
|
4830
|
+
version: skillData.version ?? null,
|
|
4831
|
+
commit_id: skillData.commit_id ?? null,
|
|
4832
|
+
author: skillData.author ?? null,
|
|
4833
|
+
source: skillData.source ?? "",
|
|
4834
|
+
source_url: skillData.source_url ?? "",
|
|
4835
|
+
repo_url: skillData.repo_url ?? null,
|
|
4836
|
+
category: skillData.category ?? null,
|
|
4837
|
+
tags: Array.isArray(skillData.tags) ? skillData.tags : null,
|
|
4838
|
+
platform: skillData.platform ?? null,
|
|
4839
|
+
metadata: skillData.metadata ?? null,
|
|
4840
|
+
risk_score: skillData.risk_score ?? null,
|
|
4841
|
+
download_count: skillData.download_count ?? 0,
|
|
4842
|
+
period_downloads: skillData.period_downloads ?? null,
|
|
4843
|
+
rating: skillData.rating ?? null,
|
|
4844
|
+
created_at: skillData.created_at ?? null,
|
|
4845
|
+
updated_at: skillData.updated_at ?? null,
|
|
4846
|
+
last_indexed_at: skillData.last_indexed_at ?? null
|
|
4847
|
+
}
|
|
4848
|
+
};
|
|
4849
|
+
} finally {
|
|
4850
|
+
clearTimeout(timeout);
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
function formatInstalls(count) {
|
|
4854
|
+
if (count === void 0 || count === null || Number.isNaN(count)) return "0 installs";
|
|
4855
|
+
if (count <= 0) return "0 installs";
|
|
4856
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
4857
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
4858
|
+
return `${count} install${count === 1 ? "" : "s"}`;
|
|
4859
|
+
}
|
|
4860
|
+
function formatRiskLevel(riskScore) {
|
|
4861
|
+
if (riskScore === void 0 || riskScore === null) return import_picocolors.default.dim("unknown risk");
|
|
4862
|
+
if (riskScore <= 20) return import_picocolors.default.green("safe risk");
|
|
4863
|
+
if (riskScore <= 50) return import_picocolors.default.green("low risk");
|
|
4864
|
+
if (riskScore <= 80) return import_picocolors.default.yellow("medium risk");
|
|
4865
|
+
return import_picocolors.default.red("high risk");
|
|
4866
|
+
}
|
|
4867
|
+
function buildGetOutput(data) {
|
|
4868
|
+
const lines = [];
|
|
4869
|
+
lines.push(`${import_picocolors.default.bold("Skill:")} ${import_picocolors.default.cyan(data.skill_id)}`);
|
|
4870
|
+
if (data.name) lines.push(`${import_picocolors.default.bold("Name:")} ${data.name}`);
|
|
4871
|
+
if (data.description) lines.push(`${import_picocolors.default.bold("Description:")} ${data.description}`);
|
|
4872
|
+
if (data.author) lines.push(`${import_picocolors.default.bold("Author:")} ${data.author}`);
|
|
4873
|
+
if (data.category) lines.push(`${import_picocolors.default.bold("Category:")} ${data.category}`);
|
|
4874
|
+
if (data.version) lines.push(`${import_picocolors.default.bold("Version:")} ${data.version}`);
|
|
4875
|
+
if (data.tags && data.tags.length > 0) lines.push(`${import_picocolors.default.bold("Tags:")} ${data.tags.join(", ")}`);
|
|
4876
|
+
if (data.platform) lines.push(`${import_picocolors.default.bold("Platform:")} ${data.platform}`);
|
|
4877
|
+
if (data.source) lines.push(`${import_picocolors.default.bold("Source:")} ${data.source}`);
|
|
4878
|
+
if (data.source_url) lines.push(`${import_picocolors.default.bold("Source URL:")} ${import_picocolors.default.dim(data.source_url)}`);
|
|
4879
|
+
if (data.repo_url) lines.push(`${import_picocolors.default.bold("Repo URL:")} ${import_picocolors.default.dim(data.repo_url)}`);
|
|
4880
|
+
lines.push(`${import_picocolors.default.bold("Installs:")} ${import_picocolors.default.cyan(formatInstalls(data.download_count))}`);
|
|
4881
|
+
const riskLabel = formatRiskLevel(data.risk_score);
|
|
4882
|
+
const score = data.risk_score === void 0 || data.risk_score === null ? "" : import_picocolors.default.dim(` (score ${data.risk_score})`);
|
|
4883
|
+
lines.push(`${import_picocolors.default.bold("Risk:")} ${riskLabel}${score}`);
|
|
4884
|
+
if (data.updated_at) lines.push(`${import_picocolors.default.bold("Updated at:")} ${import_picocolors.default.dim(data.updated_at)}`);
|
|
4885
|
+
return lines;
|
|
4886
|
+
}
|
|
4887
|
+
async function runGet(args) {
|
|
4888
|
+
const { source, skill, errors } = parseGetOptions(args);
|
|
4889
|
+
if (errors.length > 0) {
|
|
4890
|
+
for (const error of errors) console.error(import_picocolors.default.red(error));
|
|
4891
|
+
console.error("Usage: wittyhub get <source> --skill <skill>");
|
|
4892
|
+
console.error(" wittyhub get <skill_id>");
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
const { skillId, derived } = await resolveSkillId(source, skill || void 0);
|
|
4896
|
+
if (!skillId) {
|
|
4897
|
+
console.error(import_picocolors.default.red(`Unable to resolve skill id from source: ${source}`));
|
|
4898
|
+
console.error("Usage: wittyhub get <source> --skill <skill>");
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
if (derived) console.error(import_picocolors.default.yellow(`Could not locate skill in repo; trying derived id: ${skillId}`));
|
|
4902
|
+
const result = await fetchSkillDetail(skillId);
|
|
4903
|
+
track({
|
|
4904
|
+
event: "get",
|
|
4905
|
+
skillId,
|
|
4906
|
+
found: result.status === "ok" ? "1" : "0"
|
|
4907
|
+
});
|
|
4908
|
+
switch (result.status) {
|
|
4909
|
+
case "ok":
|
|
4910
|
+
for (const line of buildGetOutput(result.data)) console.log(line);
|
|
4911
|
+
break;
|
|
4912
|
+
case "not_found":
|
|
4913
|
+
console.error(import_picocolors.default.red(`Skill not found: ${skillId}`));
|
|
4914
|
+
break;
|
|
4915
|
+
case "error":
|
|
4916
|
+
console.error(import_picocolors.default.red(result.message));
|
|
4917
|
+
break;
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4679
4920
|
const isCancelled = (value) => typeof value === "symbol";
|
|
4680
4921
|
function shortenPath$1(fullPath, cwd) {
|
|
4681
|
-
const home = homedir();
|
|
4922
|
+
const home = homedir$1();
|
|
4682
4923
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
4683
4924
|
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
4684
4925
|
return fullPath;
|
|
@@ -5033,7 +5274,7 @@ const DIM$2 = "\x1B[38;5;102m";
|
|
|
5033
5274
|
const CYAN = "\x1B[36m";
|
|
5034
5275
|
const YELLOW = "\x1B[33m";
|
|
5035
5276
|
function shortenPath(fullPath, cwd) {
|
|
5036
|
-
const home = homedir();
|
|
5277
|
+
const home = homedir$1();
|
|
5037
5278
|
if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
|
|
5038
5279
|
if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
|
|
5039
5280
|
return fullPath;
|
|
@@ -5270,7 +5511,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5270
5511
|
for (const pathToCleanup of pathsToCleanup) {
|
|
5271
5512
|
if (pathToCleanup === canonicalPath) continue;
|
|
5272
5513
|
try {
|
|
5273
|
-
if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
|
|
5514
|
+
if (await lstat(pathToCleanup).catch(() => null)) await rm$1(pathToCleanup, {
|
|
5274
5515
|
recursive: true,
|
|
5275
5516
|
force: true
|
|
5276
5517
|
});
|
|
@@ -5288,7 +5529,7 @@ async function removeCommand(skillNames, options) {
|
|
|
5288
5529
|
isStillUsed = true;
|
|
5289
5530
|
break;
|
|
5290
5531
|
}
|
|
5291
|
-
if (!isStillUsed) await rm(canonicalPath, {
|
|
5532
|
+
if (!isStillUsed) await rm$1(canonicalPath, {
|
|
5292
5533
|
recursive: true,
|
|
5293
5534
|
force: true
|
|
5294
5535
|
});
|
|
@@ -5936,7 +6177,7 @@ function buildUsePrompt(input) {
|
|
|
5936
6177
|
return sections.join("\n\n") + "\n";
|
|
5937
6178
|
}
|
|
5938
6179
|
async function materializeUseSkill(skill) {
|
|
5939
|
-
const tempRoot = await mkdtemp(join(tmpdir(), "skills-use-"));
|
|
6180
|
+
const tempRoot = await mkdtemp$1(join(tmpdir$1(), "skills-use-"));
|
|
5940
6181
|
const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
|
|
5941
6182
|
if (!isPathSafe(tempRoot, skillDir)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
5942
6183
|
await mkdir(skillDir, { recursive: true });
|
|
@@ -6163,8 +6404,8 @@ async function writeSafeFile(targetDir, filePath, contents) {
|
|
|
6163
6404
|
const fullPath = join(targetDir, filePath);
|
|
6164
6405
|
if (!isPathSafe(targetDir, fullPath)) return;
|
|
6165
6406
|
await mkdir(dirname(fullPath), { recursive: true });
|
|
6166
|
-
if (typeof contents === "string") await writeFile(fullPath, contents, "utf-8");
|
|
6167
|
-
else await writeFile(fullPath, contents);
|
|
6407
|
+
if (typeof contents === "string") await writeFile$1(fullPath, contents, "utf-8");
|
|
6408
|
+
else await writeFile$1(fullPath, contents);
|
|
6168
6409
|
}
|
|
6169
6410
|
async function copySkillDirectory(src, dest) {
|
|
6170
6411
|
await mkdir(dest, { recursive: true });
|
|
@@ -6224,6 +6465,308 @@ function fail(message) {
|
|
|
6224
6465
|
process.exit(1);
|
|
6225
6466
|
}
|
|
6226
6467
|
var UseCommandError = class extends Error {};
|
|
6468
|
+
const CANONICAL_CATEGORIES = new Set([
|
|
6469
|
+
"Research and Design",
|
|
6470
|
+
"Development and Build",
|
|
6471
|
+
"Engineering and Compilation",
|
|
6472
|
+
"Quality and Validation",
|
|
6473
|
+
"Release and Deployment",
|
|
6474
|
+
"Monitoring and Operations",
|
|
6475
|
+
"Performance Optimization",
|
|
6476
|
+
"Security Hardening",
|
|
6477
|
+
"others"
|
|
6478
|
+
]);
|
|
6479
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
6480
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/;
|
|
6481
|
+
function parseValidateOptions(args) {
|
|
6482
|
+
let path = "";
|
|
6483
|
+
const options = {};
|
|
6484
|
+
const errors = [];
|
|
6485
|
+
for (let i = 0; i < args.length; i++) {
|
|
6486
|
+
const arg = args[i];
|
|
6487
|
+
if (arg === "--json") options.json = true;
|
|
6488
|
+
else if (arg === "-h" || arg === "--help") {} else if (arg && !arg.startsWith("-")) if (!path) path = arg.trim();
|
|
6489
|
+
else errors.push(`Unexpected argument: ${arg}`);
|
|
6490
|
+
else if (arg) errors.push(`Unknown option: ${arg}`);
|
|
6491
|
+
}
|
|
6492
|
+
if (!path) errors.push("Missing required argument: <path>");
|
|
6493
|
+
return {
|
|
6494
|
+
path,
|
|
6495
|
+
options,
|
|
6496
|
+
errors
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
async function runValidate(args) {
|
|
6500
|
+
const { path, options, errors } = parseValidateOptions(args);
|
|
6501
|
+
if (errors.length > 0) {
|
|
6502
|
+
for (const e of errors) console.error(import_picocolors.default.red(`error: ${e}`));
|
|
6503
|
+
console.error(`\nRun ${import_picocolors.default.cyan("wittyhub validate --help")} for usage.\n`);
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6506
|
+
const result = await validateSkill(resolve(path));
|
|
6507
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
6508
|
+
else printResult(result);
|
|
6509
|
+
return result;
|
|
6510
|
+
}
|
|
6511
|
+
async function validateSkill(absPath) {
|
|
6512
|
+
const errors = [];
|
|
6513
|
+
const warnings = [];
|
|
6514
|
+
let skillDir = absPath;
|
|
6515
|
+
let skillMdPath = null;
|
|
6516
|
+
let stats;
|
|
6517
|
+
try {
|
|
6518
|
+
stats = await stat(absPath);
|
|
6519
|
+
} catch {
|
|
6520
|
+
errors.push({
|
|
6521
|
+
id: "path-not-found",
|
|
6522
|
+
severity: "error",
|
|
6523
|
+
message: `Path does not exist: ${absPath}`,
|
|
6524
|
+
hint: "Pass the skill directory (e.g. ./my-skill) or the SKILL.md file path."
|
|
6525
|
+
});
|
|
6526
|
+
return {
|
|
6527
|
+
path: absPath,
|
|
6528
|
+
errors,
|
|
6529
|
+
warnings,
|
|
6530
|
+
ok: false
|
|
6531
|
+
};
|
|
6532
|
+
}
|
|
6533
|
+
if (stats.isFile()) {
|
|
6534
|
+
const filename = basename(absPath);
|
|
6535
|
+
if (filename !== "SKILL.md") {
|
|
6536
|
+
errors.push({
|
|
6537
|
+
id: "filename-must-be-skill-md",
|
|
6538
|
+
severity: "error",
|
|
6539
|
+
message: `File must be named "SKILL.md" (got "${filename}")`,
|
|
6540
|
+
hint: "Rename the file to SKILL.md (uppercase). The agent skill spec requires this exact filename."
|
|
6541
|
+
});
|
|
6542
|
+
return {
|
|
6543
|
+
path: absPath,
|
|
6544
|
+
errors,
|
|
6545
|
+
warnings,
|
|
6546
|
+
ok: false
|
|
6547
|
+
};
|
|
6548
|
+
}
|
|
6549
|
+
skillMdPath = absPath;
|
|
6550
|
+
skillDir = dirname(absPath);
|
|
6551
|
+
} else if (stats.isDirectory()) {
|
|
6552
|
+
const expected = join(absPath, "SKILL.md");
|
|
6553
|
+
try {
|
|
6554
|
+
await stat(expected);
|
|
6555
|
+
skillMdPath = expected;
|
|
6556
|
+
} catch {
|
|
6557
|
+
errors.push({
|
|
6558
|
+
id: "missing-skill-md",
|
|
6559
|
+
severity: "error",
|
|
6560
|
+
message: `No SKILL.md found in directory: ${absPath}`,
|
|
6561
|
+
hint: "Create one with `wittyhub init <name>` or rename an existing markdown file to SKILL.md (uppercase)."
|
|
6562
|
+
});
|
|
6563
|
+
return {
|
|
6564
|
+
path: absPath,
|
|
6565
|
+
errors,
|
|
6566
|
+
warnings,
|
|
6567
|
+
ok: false
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
} else {
|
|
6571
|
+
errors.push({
|
|
6572
|
+
id: "path-invalid-type",
|
|
6573
|
+
severity: "error",
|
|
6574
|
+
message: `Path is neither a file nor a directory: ${absPath}`
|
|
6575
|
+
});
|
|
6576
|
+
return {
|
|
6577
|
+
path: absPath,
|
|
6578
|
+
errors,
|
|
6579
|
+
warnings,
|
|
6580
|
+
ok: false
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
try {
|
|
6584
|
+
const lowerMdFile = (await readdir(skillDir)).find((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
|
|
6585
|
+
if (lowerMdFile) warnings.push({
|
|
6586
|
+
id: "case-variant-found",
|
|
6587
|
+
severity: "warning",
|
|
6588
|
+
message: `Found a case-variant file "${lowerMdFile}" alongside SKILL.md`,
|
|
6589
|
+
hint: "Only \"SKILL.md\" (all uppercase) is recognized by the agent spec. Remove the variant to avoid confusion."
|
|
6590
|
+
});
|
|
6591
|
+
} catch {}
|
|
6592
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
6593
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
6594
|
+
if (!match) {
|
|
6595
|
+
errors.push({
|
|
6596
|
+
id: "frontmatter-missing",
|
|
6597
|
+
severity: "error",
|
|
6598
|
+
message: "SKILL.md must start with a YAML frontmatter block delimited by \"---\"",
|
|
6599
|
+
hint: "Add a frontmatter block at the top:\n\n ---\n name: my-skill\n description: what it does\n ---"
|
|
6600
|
+
});
|
|
6601
|
+
return {
|
|
6602
|
+
path: skillMdPath,
|
|
6603
|
+
errors,
|
|
6604
|
+
warnings,
|
|
6605
|
+
ok: false
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
6608
|
+
match[1];
|
|
6609
|
+
const body = match[2] ?? "";
|
|
6610
|
+
const { data: fm } = parseFrontmatter(content);
|
|
6611
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
|
|
6612
|
+
errors.push({
|
|
6613
|
+
id: "frontmatter-not-object",
|
|
6614
|
+
severity: "error",
|
|
6615
|
+
message: "Frontmatter must be a YAML mapping (key-value pairs), not a scalar or list."
|
|
6616
|
+
});
|
|
6617
|
+
return {
|
|
6618
|
+
path: skillMdPath,
|
|
6619
|
+
errors,
|
|
6620
|
+
warnings,
|
|
6621
|
+
ok: false
|
|
6622
|
+
};
|
|
6623
|
+
}
|
|
6624
|
+
if (fm.name === void 0 || fm.name === null || fm.name === "") errors.push({
|
|
6625
|
+
id: "name-required",
|
|
6626
|
+
severity: "error",
|
|
6627
|
+
message: "Missing required frontmatter field: name",
|
|
6628
|
+
hint: "Add `name: <skill-name>` to the frontmatter."
|
|
6629
|
+
});
|
|
6630
|
+
else if (typeof fm.name !== "string") errors.push({
|
|
6631
|
+
id: "name-not-string",
|
|
6632
|
+
severity: "error",
|
|
6633
|
+
message: `Frontmatter field "name" must be a string (got ${typeof fm.name})`,
|
|
6634
|
+
hint: "Wrap the value in quotes if it contains special characters."
|
|
6635
|
+
});
|
|
6636
|
+
else {
|
|
6637
|
+
if (!NAME_PATTERN.test(fm.name)) warnings.push({
|
|
6638
|
+
id: "name-not-kebab",
|
|
6639
|
+
severity: "warning",
|
|
6640
|
+
message: `Skill name "${fm.name}" is not in kebab-case (lowercase letters, digits, hyphens)`,
|
|
6641
|
+
hint: "Use a name like \"add-or-fix-type-checking\" for best compatibility."
|
|
6642
|
+
});
|
|
6643
|
+
if (basename(skillDir) !== fm.name) warnings.push({
|
|
6644
|
+
id: "name-dirname-mismatch",
|
|
6645
|
+
severity: "warning",
|
|
6646
|
+
message: `Skill name "${fm.name}" does not match directory name "${basename(skillDir)}"`,
|
|
6647
|
+
hint: "For consistency, the directory name and `name` field should match."
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6650
|
+
if (fm.description === void 0 || fm.description === null || fm.description === "") errors.push({
|
|
6651
|
+
id: "description-required",
|
|
6652
|
+
severity: "error",
|
|
6653
|
+
message: "Missing required frontmatter field: description",
|
|
6654
|
+
hint: "Add `description: <one-line summary>` to the frontmatter."
|
|
6655
|
+
});
|
|
6656
|
+
else if (typeof fm.description !== "string") errors.push({
|
|
6657
|
+
id: "description-not-string",
|
|
6658
|
+
severity: "error",
|
|
6659
|
+
message: `Frontmatter field "description" must be a string (got ${typeof fm.description})`
|
|
6660
|
+
});
|
|
6661
|
+
else {
|
|
6662
|
+
const desc = fm.description.trim();
|
|
6663
|
+
if (desc.length < 10) warnings.push({
|
|
6664
|
+
id: "description-too-short",
|
|
6665
|
+
severity: "warning",
|
|
6666
|
+
message: `Description is very short (${desc.length} chars): "${desc}"`,
|
|
6667
|
+
hint: "Aim for at least 10-20 characters so users can quickly understand the skill."
|
|
6668
|
+
});
|
|
6669
|
+
if (desc.length > 200) warnings.push({
|
|
6670
|
+
id: "description-too-long",
|
|
6671
|
+
severity: "warning",
|
|
6672
|
+
message: `Description is very long (${desc.length} chars)`,
|
|
6673
|
+
hint: "Keep it under 200 characters for readability in listings."
|
|
6674
|
+
});
|
|
6675
|
+
}
|
|
6676
|
+
if (fm.category !== void 0 && fm.category !== null) {
|
|
6677
|
+
if (typeof fm.category !== "string") warnings.push({
|
|
6678
|
+
id: "category-not-string",
|
|
6679
|
+
severity: "warning",
|
|
6680
|
+
message: `Frontmatter field "category" should be a string (got ${typeof fm.category})`
|
|
6681
|
+
});
|
|
6682
|
+
else if (!CANONICAL_CATEGORIES.has(fm.category)) warnings.push({
|
|
6683
|
+
id: "category-not-canonical",
|
|
6684
|
+
severity: "warning",
|
|
6685
|
+
message: `Category "${fm.category}" is not in the canonical list`,
|
|
6686
|
+
hint: `Valid values: ${[...CANONICAL_CATEGORIES].join(", ")}. See https://gitcode.com/openeuler/openEuler-skills.`
|
|
6687
|
+
});
|
|
6688
|
+
}
|
|
6689
|
+
if (fm.version !== void 0 && fm.version !== null && fm.version !== "") {
|
|
6690
|
+
if (typeof fm.version !== "string") warnings.push({
|
|
6691
|
+
id: "version-not-string",
|
|
6692
|
+
severity: "warning",
|
|
6693
|
+
message: `Frontmatter field "version" should be a string (got ${typeof fm.version})`,
|
|
6694
|
+
hint: "Quote the value, e.g. `version: \"1.0.0\"`."
|
|
6695
|
+
});
|
|
6696
|
+
else if (!SEMVER_PATTERN.test(fm.version.trim())) warnings.push({
|
|
6697
|
+
id: "version-not-semver",
|
|
6698
|
+
severity: "warning",
|
|
6699
|
+
message: `Version "${fm.version}" is not a valid semver`,
|
|
6700
|
+
hint: "Use the form X.Y.Z (e.g. 1.0.0, 0.2.1-beta)."
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
if (fm.metadata !== void 0 && (typeof fm.metadata !== "object" || Array.isArray(fm.metadata))) warnings.push({
|
|
6704
|
+
id: "metadata-not-object",
|
|
6705
|
+
severity: "warning",
|
|
6706
|
+
message: `Frontmatter field "metadata" should be a mapping (got ${Array.isArray(fm.metadata) ? "array" : typeof fm.metadata})`
|
|
6707
|
+
});
|
|
6708
|
+
if (body.trim().length === 0) warnings.push({
|
|
6709
|
+
id: "body-empty",
|
|
6710
|
+
severity: "warning",
|
|
6711
|
+
message: "SKILL.md has no body content after the frontmatter",
|
|
6712
|
+
hint: "Add at least a heading and a few instructions for the agent to follow."
|
|
6713
|
+
});
|
|
6714
|
+
else if (!/^#{1,6}\s+\S/m.test(body)) warnings.push({
|
|
6715
|
+
id: "body-no-heading",
|
|
6716
|
+
severity: "warning",
|
|
6717
|
+
message: "Body content has no Markdown headings",
|
|
6718
|
+
hint: "Structure the instructions with headings like `## When to use`, `## Instructions`."
|
|
6719
|
+
});
|
|
6720
|
+
const knownKeys = new Set([
|
|
6721
|
+
"name",
|
|
6722
|
+
"description",
|
|
6723
|
+
"version",
|
|
6724
|
+
"category",
|
|
6725
|
+
"metadata"
|
|
6726
|
+
]);
|
|
6727
|
+
const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
|
|
6728
|
+
if (unknownKeys.length > 0) warnings.push({
|
|
6729
|
+
id: "unknown-frontmatter-keys",
|
|
6730
|
+
severity: "warning",
|
|
6731
|
+
message: `Unknown frontmatter keys: ${unknownKeys.join(", ")}`,
|
|
6732
|
+
hint: `Recognized keys: ${[...knownKeys].join(", ")}.`
|
|
6733
|
+
});
|
|
6734
|
+
return {
|
|
6735
|
+
path: skillMdPath,
|
|
6736
|
+
errors,
|
|
6737
|
+
warnings,
|
|
6738
|
+
ok: errors.length === 0
|
|
6739
|
+
};
|
|
6740
|
+
}
|
|
6741
|
+
function printResult(result) {
|
|
6742
|
+
const { path, errors, warnings, ok } = result;
|
|
6743
|
+
console.log(`${import_picocolors.default.cyan("Path:")} ${path}`);
|
|
6744
|
+
console.log();
|
|
6745
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
6746
|
+
console.log(import_picocolors.default.green(" All checks passed."));
|
|
6747
|
+
console.log();
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
if (errors.length > 0) {
|
|
6751
|
+
console.log(import_picocolors.default.red(`${errors.length} error(s):`));
|
|
6752
|
+
for (const rule of errors) {
|
|
6753
|
+
console.log(` ${import_picocolors.default.red("✗")} [${rule.id}] ${rule.message}`);
|
|
6754
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6755
|
+
}
|
|
6756
|
+
console.log();
|
|
6757
|
+
}
|
|
6758
|
+
if (warnings.length > 0) {
|
|
6759
|
+
console.log(import_picocolors.default.yellow(`${warnings.length} warning(s):`));
|
|
6760
|
+
for (const rule of warnings) {
|
|
6761
|
+
console.log(` ${import_picocolors.default.yellow("!")} [${rule.id}] ${rule.message}`);
|
|
6762
|
+
if (rule.hint) console.log(` ${import_picocolors.default.dim(rule.hint)}`);
|
|
6763
|
+
}
|
|
6764
|
+
console.log();
|
|
6765
|
+
}
|
|
6766
|
+
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" : ""})` : ""));
|
|
6767
|
+
else console.log(import_picocolors.default.red(" Skill is NOT valid. Fix the errors above."));
|
|
6768
|
+
console.log();
|
|
6769
|
+
}
|
|
6227
6770
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6228
6771
|
function getVersion() {
|
|
6229
6772
|
try {
|
|
@@ -6276,11 +6819,12 @@ function showBanner() {
|
|
|
6276
6819
|
console.log();
|
|
6277
6820
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
|
|
6278
6821
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
|
|
6822
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub validate ${DIM}<path>${RESET} ${DIM}Validate a local skill${RESET}`);
|
|
6279
6823
|
console.log(` ${DIM}$${RESET} ${TEXT}npx wittyhub experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
6280
6824
|
console.log();
|
|
6281
6825
|
console.log(`${DIM}try:${RESET} npx wittyhub add vercel-labs/agent-skills`);
|
|
6282
6826
|
console.log();
|
|
6283
|
-
console.log(`Discover more skills at ${TEXT}https://
|
|
6827
|
+
console.log(`Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6284
6828
|
console.log();
|
|
6285
6829
|
}
|
|
6286
6830
|
function showHelp() {
|
|
@@ -6296,6 +6840,14 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
6296
6840
|
remove [skills] Remove installed skills
|
|
6297
6841
|
list, ls List installed skills
|
|
6298
6842
|
find [query] Search for skills interactively
|
|
6843
|
+
audit <source> --skill <skill>
|
|
6844
|
+
Show security audit result (risk level + risk signals)
|
|
6845
|
+
e.g. audit https://github.com/huggingface/transformers --skill add-or-fix-type-checking
|
|
6846
|
+
get <source> --skill <skill>
|
|
6847
|
+
View skill details (author/category/version/description/tags)
|
|
6848
|
+
e.g. get https://github.com/huggingface/transformers --skill add-or-fix-type-checking
|
|
6849
|
+
validate <path> Validate a local skill for spec compliance (SKILL.md filename,
|
|
6850
|
+
frontmatter, required fields name/description, etc.)
|
|
6299
6851
|
|
|
6300
6852
|
${BOLD}Find Options:${RESET}
|
|
6301
6853
|
--owner <owner> Search only repositories from a GitHub owner
|
|
@@ -6310,7 +6862,7 @@ ${BOLD}Update Options:${RESET}
|
|
|
6310
6862
|
|
|
6311
6863
|
${BOLD}Project:${RESET}
|
|
6312
6864
|
experimental_install Restore skills from skills-lock.json
|
|
6313
|
-
init [name] Initialize a skill (creates <name>/SKILL.md
|
|
6865
|
+
init [name] Initialize a skill (creates <name>/SKILL.md; defaults to my-skill/)
|
|
6314
6866
|
experimental_sync Sync skills from node_modules into agent directories
|
|
6315
6867
|
|
|
6316
6868
|
${BOLD}Add Options:${RESET}
|
|
@@ -6352,12 +6904,9 @@ ${BOLD}Options:${RESET}
|
|
|
6352
6904
|
--version, -v Show version number
|
|
6353
6905
|
|
|
6354
6906
|
${BOLD}Examples:${RESET}
|
|
6355
|
-
${DIM}$${RESET} wittyhub add
|
|
6356
|
-
${DIM}$${RESET} wittyhub
|
|
6357
|
-
${DIM}$${RESET} wittyhub
|
|
6358
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills -g
|
|
6359
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --agent claude-code cursor
|
|
6360
|
-
${DIM}$${RESET} wittyhub add vercel-labs/agent-skills --skill pr-review commit
|
|
6907
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis
|
|
6908
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis -g
|
|
6909
|
+
${DIM}$${RESET} wittyhub add https://gitcode.com/openeuler/witty-diagnosis-agent --skill system-resource-diagnosis --agent claude-code cursor
|
|
6361
6910
|
${DIM}$${RESET} wittyhub remove ${DIM}# interactive remove${RESET}
|
|
6362
6911
|
${DIM}$${RESET} wittyhub remove web-design ${DIM}# remove by name${RESET}
|
|
6363
6912
|
${DIM}$${RESET} wittyhub rm --global frontend-design
|
|
@@ -6376,7 +6925,45 @@ ${BOLD}Examples:${RESET}
|
|
|
6376
6925
|
${DIM}$${RESET} wittyhub experimental_sync ${DIM}# sync from node_modules${RESET}
|
|
6377
6926
|
${DIM}$${RESET} wittyhub experimental_sync -y ${DIM}# sync without prompts${RESET}
|
|
6378
6927
|
|
|
6379
|
-
Discover more skills at ${TEXT}https://
|
|
6928
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
6929
|
+
`);
|
|
6930
|
+
}
|
|
6931
|
+
function showInitHelp() {
|
|
6932
|
+
console.log(`
|
|
6933
|
+
${BOLD}Usage:${RESET} wittyhub init [name]
|
|
6934
|
+
|
|
6935
|
+
${BOLD}Description:${RESET}
|
|
6936
|
+
Initialize a new skill by creating a SKILL.md template.
|
|
6937
|
+
|
|
6938
|
+
${BOLD}Arguments:${RESET}
|
|
6939
|
+
name Optional skill name. Creates <name>/SKILL.md.
|
|
6940
|
+
If omitted, defaults to my-skill/ (never the cwd itself).
|
|
6941
|
+
|
|
6942
|
+
${BOLD}Examples:${RESET}
|
|
6943
|
+
${DIM}$${RESET} wittyhub init ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
6944
|
+
${DIM}$${RESET} wittyhub init my-skill ${DIM}# creates my-skill/SKILL.md${RESET}
|
|
6945
|
+
${DIM}$${RESET} wittyhub init code-reviewer ${DIM}# creates code-reviewer/SKILL.md${RESET}
|
|
6946
|
+
`);
|
|
6947
|
+
}
|
|
6948
|
+
function showValidateHelp() {
|
|
6949
|
+
console.log(`
|
|
6950
|
+
${BOLD}Usage:${RESET} wittyhub validate <path> [options]
|
|
6951
|
+
|
|
6952
|
+
${BOLD}Description:${RESET}
|
|
6953
|
+
Validate a local skill for spec compliance (SKILL.md filename, frontmatter,
|
|
6954
|
+
required fields, etc.). Reports errors (must-fix) and warnings (recommended).
|
|
6955
|
+
|
|
6956
|
+
${BOLD}Arguments:${RESET}
|
|
6957
|
+
path Path to the skill directory or SKILL.md file.
|
|
6958
|
+
|
|
6959
|
+
${BOLD}Options:${RESET}
|
|
6960
|
+
--json Output result as JSON (no ANSI colors).
|
|
6961
|
+
-h, --help Show this help message.
|
|
6962
|
+
|
|
6963
|
+
${BOLD}Examples:${RESET}
|
|
6964
|
+
${DIM}$${RESET} wittyhub validate ./my-skill
|
|
6965
|
+
${DIM}$${RESET} wittyhub validate ./my-skill/SKILL.md
|
|
6966
|
+
${DIM}$${RESET} wittyhub validate ./my-skill --json
|
|
6380
6967
|
`);
|
|
6381
6968
|
}
|
|
6382
6969
|
function showRemoveHelp() {
|
|
@@ -6406,24 +6993,25 @@ ${BOLD}Examples:${RESET}
|
|
|
6406
6993
|
${DIM}$${RESET} wittyhub remove --all ${DIM}# remove all skills${RESET}
|
|
6407
6994
|
${DIM}$${RESET} wittyhub remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
|
|
6408
6995
|
|
|
6409
|
-
Discover more skills at ${TEXT}https://
|
|
6996
|
+
Discover more skills at ${TEXT}https://skillhub.openeuler.org/${RESET}
|
|
6410
6997
|
`);
|
|
6411
6998
|
}
|
|
6412
6999
|
function runInit(args) {
|
|
6413
7000
|
const cwd = process.cwd();
|
|
6414
|
-
const skillName = args[0]
|
|
6415
|
-
const
|
|
6416
|
-
const skillDir = hasName ? join(cwd, skillName) : cwd;
|
|
7001
|
+
const skillName = args[0] ?? "my-skill";
|
|
7002
|
+
const skillDir = join(cwd, skillName);
|
|
6417
7003
|
const skillFile = join(skillDir, "SKILL.md");
|
|
6418
|
-
const displayPath =
|
|
7004
|
+
const displayPath = `${skillName}/SKILL.md`;
|
|
6419
7005
|
if (existsSync(skillFile)) {
|
|
6420
7006
|
console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
|
|
6421
7007
|
return;
|
|
6422
7008
|
}
|
|
6423
|
-
|
|
7009
|
+
mkdirSync(skillDir, { recursive: true });
|
|
6424
7010
|
writeFileSync(skillFile, `---
|
|
6425
7011
|
name: ${skillName}
|
|
6426
7012
|
description: A brief description of what this skill does
|
|
7013
|
+
version: 0.1.0 # optional; increment as needed
|
|
7014
|
+
category: others # optional;change if needed — full list at https://gitcode.com/openeuler/openEuler-skills
|
|
6427
7015
|
---
|
|
6428
7016
|
|
|
6429
7017
|
# ${skillName}
|
|
@@ -6448,12 +7036,12 @@ Describe when this skill should be used.
|
|
|
6448
7036
|
console.log(`${DIM}Next steps:${RESET}`);
|
|
6449
7037
|
console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
|
|
6450
7038
|
console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
|
|
7039
|
+
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)`);
|
|
6451
7040
|
console.log();
|
|
6452
7041
|
console.log(`${DIM}Publishing:${RESET}`);
|
|
6453
|
-
console.log(`
|
|
6454
|
-
console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx wittyhub add https://example.com/${displayPath}${RESET}`);
|
|
7042
|
+
console.log(` Submit your skill to ${TEXT}https://gitcode.com/openeuler/openEuler-skills${RESET}`);
|
|
6455
7043
|
console.log();
|
|
6456
|
-
console.log(`Browse existing skills for inspiration at ${TEXT}https://
|
|
7044
|
+
console.log(`Browse existing skills for inspiration at ${TEXT}https://skillhub.openeuler.org/${RESET}`);
|
|
6457
7045
|
console.log();
|
|
6458
7046
|
}
|
|
6459
7047
|
async function main() {
|
|
@@ -6475,6 +7063,10 @@ async function main() {
|
|
|
6475
7063
|
await runFind(restArgs);
|
|
6476
7064
|
break;
|
|
6477
7065
|
case "init":
|
|
7066
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7067
|
+
showInitHelp();
|
|
7068
|
+
break;
|
|
7069
|
+
}
|
|
6478
7070
|
if (!inAgent) showLogo();
|
|
6479
7071
|
console.log();
|
|
6480
7072
|
runInit(restArgs);
|
|
@@ -6517,6 +7109,24 @@ async function main() {
|
|
|
6517
7109
|
case "ls":
|
|
6518
7110
|
await runList(restArgs);
|
|
6519
7111
|
break;
|
|
7112
|
+
case "audit":
|
|
7113
|
+
if (!inAgent) showLogo();
|
|
7114
|
+
await runAudit(restArgs);
|
|
7115
|
+
break;
|
|
7116
|
+
case "get":
|
|
7117
|
+
if (!inAgent) showLogo();
|
|
7118
|
+
await runGet(restArgs);
|
|
7119
|
+
break;
|
|
7120
|
+
case "validate":
|
|
7121
|
+
case "check-skill":
|
|
7122
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
7123
|
+
showValidateHelp();
|
|
7124
|
+
break;
|
|
7125
|
+
}
|
|
7126
|
+
if (!inAgent) showLogo();
|
|
7127
|
+
console.log();
|
|
7128
|
+
await runValidate(restArgs);
|
|
7129
|
+
break;
|
|
6520
7130
|
case "check":
|
|
6521
7131
|
case "update":
|
|
6522
7132
|
case "upgrade":
|