@bud-fe/skills 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 CHANGED
@@ -1,19 +1,28 @@
1
1
  # skills-x
2
2
 
3
- The CLI for the open agent skills, commands ecosystem.
3
+ Forked from [vercel-labs/skills](https://github.com/vercel-labs/skills).
4
+
5
+ ## Changes based on upstream
6
+
7
+ ### ADDED
4
8
 
5
- Forked from [vercel-labs/skills](https://github.com/vercel-labs/skills), **recent spec changes**:
9
+ - `add-c` 命令:支持按名称从默认源的 `commands/` 目录安装 slash command 文件
6
10
 
7
- - `skills add` now supports implicit default-registry source resolution when flags are provided without a source (for example `--list` and `--all`).
8
- - `skills add` maps simple identifiers (for example `code-review`) to the curated default registry, while explicit sources (`owner/repo`, URLs, local paths) keep their existing behavior.
9
- - `skills add-c` installs command markdown files from the default registry `commands/` directory by command name.
10
- - `skills find` is provider-scoped:
11
- - default mode searches the curated registry
12
- - `-g` / `--global` searches [skills.sh](https://skills.sh)
13
- - the CLI does not silently fall back to another provider if the selected provider fails
11
+ ### MODIFIED
12
+
13
+ - `add` 命令
14
+ - 支持在未提供源但携带标志(如 `--list` `--all`)时,自动解析至默认源
15
+ - 将简单标识符(如 `code-review`)映射到默认源,而显式源(`owner/repo`、URL、本地路径)保持原有行为
16
+ - `find` 命令
17
+ - 无参数时默认从默认源搜索
18
+ - `-g` / `--global` 搜索 [skills.sh](https://skills.sh)(即原逻辑)
19
+ - `check` 和 `update`
20
+ - 支持非 GitHub 源(如 GitLab、通用 Git),采用基于内容的内容哈希(SHA-256)进行更新检测
14
21
 
15
22
  ---
16
23
 
24
+ The CLI for the open agent skills, commands ecosystem.
25
+
17
26
  <!-- agent-list:start -->
18
27
 
19
28
  Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [37 more](#available-agents).
@@ -1,5 +1,5 @@
1
- import path from "path";
2
1
  import os from "os";
2
+ import path from "path";
3
3
  const homeDirectory = os.homedir();
4
4
  const { env } = process;
5
5
  const xdgData = env.XDG_DATA_HOME || (homeDirectory ? path.join(homeDirectory, ".local", "share") : void 0);
package/dist/cli.mjs CHANGED
@@ -9,17 +9,17 @@ import "./_chunks/libs/esprima.mjs";
9
9
  import "./_chunks/libs/@kwsites/file-exists.mjs";
10
10
  import "./_chunks/libs/@kwsites/promise-deferred.mjs";
11
11
  import { t as esm_default } from "./_chunks/libs/simple-git.mjs";
12
- import { execSync, spawn, spawnSync } from "child_process";
12
+ import { execSync, spawnSync } from "child_process";
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
- import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
15
14
  import { homedir, platform, tmpdir } from "os";
16
- import { createHash } from "crypto";
15
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
17
16
  import { fileURLToPath } from "url";
18
17
  import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
18
+ import { createHash } from "crypto";
19
19
  import * as readline from "readline";
20
20
  import { Writable } from "stream";
21
21
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
22
- var version$1 = "0.0.2";
22
+ var version$1 = "0.0.4";
23
23
  const home = homedir();
24
24
  const configHome = xdgConfig ?? join(home, ".config");
25
25
  const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
@@ -104,6 +104,8 @@ const agents = {
104
104
  displayName: "Codex",
105
105
  skillsDir: ".agents/skills",
106
106
  globalSkillsDir: join(codexHome, "skills"),
107
+ commandsDir: ".codex/prompts",
108
+ globalCommandsDir: join(codexHome, "prompts"),
107
109
  detectInstalled: async () => {
108
110
  return existsSync(codexHome) || existsSync("/etc/codex");
109
111
  }
@@ -1295,6 +1297,72 @@ async function listInstalledSkills(options = {}) {
1295
1297
  } catch {}
1296
1298
  return Array.from(skillsMap.values());
1297
1299
  }
1300
+ const LOCAL_LOCK_FILE = "skills-lock.json";
1301
+ const CURRENT_VERSION$1 = 1;
1302
+ function getLocalLockPath(cwd) {
1303
+ return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
1304
+ }
1305
+ async function readLocalLock(cwd) {
1306
+ const lockPath = getLocalLockPath(cwd);
1307
+ try {
1308
+ const content = await readFile(lockPath, "utf-8");
1309
+ const parsed = JSON.parse(content);
1310
+ if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
1311
+ if (parsed.version < CURRENT_VERSION$1) return createEmptyLocalLock();
1312
+ return parsed;
1313
+ } catch {
1314
+ return createEmptyLocalLock();
1315
+ }
1316
+ }
1317
+ async function writeLocalLock(lock, cwd) {
1318
+ const lockPath = getLocalLockPath(cwd);
1319
+ const sortedSkills = {};
1320
+ for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
1321
+ const sorted = {
1322
+ version: lock.version,
1323
+ skills: sortedSkills
1324
+ };
1325
+ await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
1326
+ }
1327
+ async function computeSkillFolderHash(skillDir) {
1328
+ const files = [];
1329
+ await collectFiles(skillDir, skillDir, files);
1330
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1331
+ const hash = createHash("sha256");
1332
+ for (const file of files) {
1333
+ hash.update(file.relativePath);
1334
+ hash.update(file.content);
1335
+ }
1336
+ return hash.digest("hex");
1337
+ }
1338
+ async function collectFiles(baseDir, currentDir, results) {
1339
+ const entries = await readdir(currentDir, { withFileTypes: true });
1340
+ await Promise.all(entries.map(async (entry) => {
1341
+ const fullPath = join(currentDir, entry.name);
1342
+ if (entry.isDirectory()) {
1343
+ if (entry.name === ".git" || entry.name === "node_modules") return;
1344
+ await collectFiles(baseDir, fullPath, results);
1345
+ } else if (entry.isFile()) {
1346
+ const content = await readFile(fullPath);
1347
+ const relativePath = relative(baseDir, fullPath).split("\\").join("/");
1348
+ results.push({
1349
+ relativePath,
1350
+ content
1351
+ });
1352
+ }
1353
+ }));
1354
+ }
1355
+ async function addSkillToLocalLock(skillName, entry, cwd) {
1356
+ const lock = await readLocalLock(cwd);
1357
+ lock.skills[skillName] = entry;
1358
+ await writeLocalLock(lock, cwd);
1359
+ }
1360
+ function createEmptyLocalLock() {
1361
+ return {
1362
+ version: CURRENT_VERSION$1,
1363
+ skills: {}
1364
+ };
1365
+ }
1298
1366
  async function fetchMintlifySkill(url) {
1299
1367
  try {
1300
1368
  const response = await fetch(url, { signal: AbortSignal.timeout(3e4) });
@@ -1770,7 +1838,7 @@ registerProvider(mintlifyProvider);
1770
1838
  registerProvider(huggingFaceProvider);
1771
1839
  const AGENTS_DIR$1 = ".agents";
1772
1840
  const LOCK_FILE$1 = ".skill-lock.json";
1773
- const CURRENT_VERSION$1 = 3;
1841
+ const CURRENT_VERSION = 3;
1774
1842
  function getSkillLockPath$1() {
1775
1843
  return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
1776
1844
  }
@@ -1780,7 +1848,7 @@ async function readSkillLock$1() {
1780
1848
  const content = await readFile(lockPath, "utf-8");
1781
1849
  const parsed = JSON.parse(content);
1782
1850
  if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
1783
- if (parsed.version < CURRENT_VERSION$1) return createEmptyLockFile();
1851
+ if (parsed.version < CURRENT_VERSION) return createEmptyLockFile();
1784
1852
  return parsed;
1785
1853
  } catch (error) {
1786
1854
  return createEmptyLockFile();
@@ -1853,7 +1921,7 @@ async function getSkillFromLock(skillName) {
1853
1921
  }
1854
1922
  function createEmptyLockFile() {
1855
1923
  return {
1856
- version: CURRENT_VERSION$1,
1924
+ version: CURRENT_VERSION,
1857
1925
  skills: {},
1858
1926
  dismissed: {}
1859
1927
  };
@@ -1875,72 +1943,6 @@ async function saveSelectedAgents(agents) {
1875
1943
  lock.lastSelectedAgents = agents;
1876
1944
  await writeSkillLock(lock);
1877
1945
  }
1878
- const LOCAL_LOCK_FILE = "skills-lock.json";
1879
- const CURRENT_VERSION = 1;
1880
- function getLocalLockPath(cwd) {
1881
- return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
1882
- }
1883
- async function readLocalLock(cwd) {
1884
- const lockPath = getLocalLockPath(cwd);
1885
- try {
1886
- const content = await readFile(lockPath, "utf-8");
1887
- const parsed = JSON.parse(content);
1888
- if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
1889
- if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
1890
- return parsed;
1891
- } catch {
1892
- return createEmptyLocalLock();
1893
- }
1894
- }
1895
- async function writeLocalLock(lock, cwd) {
1896
- const lockPath = getLocalLockPath(cwd);
1897
- const sortedSkills = {};
1898
- for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
1899
- const sorted = {
1900
- version: lock.version,
1901
- skills: sortedSkills
1902
- };
1903
- await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
1904
- }
1905
- async function computeSkillFolderHash(skillDir) {
1906
- const files = [];
1907
- await collectFiles(skillDir, skillDir, files);
1908
- files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1909
- const hash = createHash("sha256");
1910
- for (const file of files) {
1911
- hash.update(file.relativePath);
1912
- hash.update(file.content);
1913
- }
1914
- return hash.digest("hex");
1915
- }
1916
- async function collectFiles(baseDir, currentDir, results) {
1917
- const entries = await readdir(currentDir, { withFileTypes: true });
1918
- await Promise.all(entries.map(async (entry) => {
1919
- const fullPath = join(currentDir, entry.name);
1920
- if (entry.isDirectory()) {
1921
- if (entry.name === ".git" || entry.name === "node_modules") return;
1922
- await collectFiles(baseDir, fullPath, results);
1923
- } else if (entry.isFile()) {
1924
- const content = await readFile(fullPath);
1925
- const relativePath = relative(baseDir, fullPath).split("\\").join("/");
1926
- results.push({
1927
- relativePath,
1928
- content
1929
- });
1930
- }
1931
- }));
1932
- }
1933
- async function addSkillToLocalLock(skillName, entry, cwd) {
1934
- const lock = await readLocalLock(cwd);
1935
- lock.skills[skillName] = entry;
1936
- await writeLocalLock(lock, cwd);
1937
- }
1938
- function createEmptyLocalLock() {
1939
- return {
1940
- version: CURRENT_VERSION,
1941
- skills: {}
1942
- };
1943
- }
1944
1946
  function getOwnerRepo(parsed) {
1945
1947
  if (parsed.type === "local") return null;
1946
1948
  if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null;
@@ -2073,8 +2075,7 @@ function parseSource(input) {
2073
2075
  url: input
2074
2076
  };
2075
2077
  if (input.match(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i) && !input.includes("/") && !input.includes(":") && !input.startsWith(".")) return {
2076
- type: "github",
2077
- url: DEFAULT_REGISTRY_URL,
2078
+ ...parseSource(DEFAULT_REGISTRY_URL),
2078
2079
  subpath: DEFAULT_REGISTRY_SUBPATH,
2079
2080
  skillFilter: input
2080
2081
  };
@@ -2322,6 +2323,33 @@ async function selectAgentsInteractive(options) {
2322
2323
  return selected;
2323
2324
  }
2324
2325
  setVersion(version$1);
2326
+ async function resolveGlobalLockHashData(params, deps = {
2327
+ fetchGitHubHash: fetchSkillFolderHash,
2328
+ computeFolderHash: computeSkillFolderHash
2329
+ }) {
2330
+ if (params.sourceType === "github") {
2331
+ let skillFolderHash = "";
2332
+ if (params.normalizedSource && params.skillPathValue) try {
2333
+ const hash = await deps.fetchGitHubHash(params.normalizedSource, params.skillPathValue);
2334
+ if (hash) skillFolderHash = hash;
2335
+ } catch {}
2336
+ return {
2337
+ skillFolderHash,
2338
+ hashMethod: "github-tree-sha"
2339
+ };
2340
+ }
2341
+ if (params.sourceType === "git" || params.sourceType === "gitlab") {
2342
+ let skillFolderHash = "";
2343
+ if (params.installDir) try {
2344
+ skillFolderHash = await deps.computeFolderHash(params.installDir);
2345
+ } catch {}
2346
+ return {
2347
+ skillFolderHash,
2348
+ hashMethod: "content-sha256"
2349
+ };
2350
+ }
2351
+ return { skillFolderHash: "" };
2352
+ }
2325
2353
  async function handleRemoteSkill(source, url, options, spinner) {
2326
2354
  const provider = findProvider(url);
2327
2355
  if (!provider) {
@@ -2494,7 +2522,9 @@ async function handleRemoteSkill(source, url, options, spinner) {
2494
2522
  });
2495
2523
  if (successful.length > 0 && installGlobally) try {
2496
2524
  let skillFolderHash = "";
2525
+ let hashMethod;
2497
2526
  if (remoteSkill.providerId === "github") {
2527
+ hashMethod = "github-tree-sha";
2498
2528
  const hash = await fetchSkillFolderHash(remoteSkill.sourceIdentifier, url);
2499
2529
  if (hash) skillFolderHash = hash;
2500
2530
  }
@@ -2502,7 +2532,8 @@ async function handleRemoteSkill(source, url, options, spinner) {
2502
2532
  source: remoteSkill.sourceIdentifier,
2503
2533
  sourceType: remoteSkill.providerId,
2504
2534
  sourceUrl: url,
2505
- skillFolderHash
2535
+ skillFolderHash,
2536
+ hashMethod
2506
2537
  });
2507
2538
  } catch {}
2508
2539
  if (successful.length > 0 && !installGlobally) try {
@@ -3025,12 +3056,12 @@ async function runAdd(args, options = {}) {
3025
3056
  console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
3026
3057
  console.log();
3027
3058
  console.log(import_picocolors.default.dim(" Usage:"));
3028
- console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
3029
- console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("[options]")} ${import_picocolors.default.dim("(uses default registry)")}`);
3059
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
3060
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add")} ${import_picocolors.default.yellow("[options]")} ${import_picocolors.default.dim("(uses default registry)")}`);
3030
3061
  console.log();
3031
3062
  console.log(import_picocolors.default.dim(" Example:"));
3032
- console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
3033
- console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("--all")}`);
3063
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
3064
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add")} ${import_picocolors.default.yellow("--all")}`);
3034
3065
  console.log();
3035
3066
  process.exit(1);
3036
3067
  }
@@ -3329,18 +3360,22 @@ async function runAdd(args, options = {}) {
3329
3360
  for (const skill of selectedSkills) {
3330
3361
  const skillDisplayName = getSkillDisplayName(skill);
3331
3362
  if (successfulSkillNames.has(skillDisplayName)) try {
3332
- let skillFolderHash = "";
3333
3363
  const skillPathValue = skillFiles[skill.name];
3334
- if (parsed.type === "github" && skillPathValue) {
3335
- const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue);
3336
- if (hash) skillFolderHash = hash;
3337
- }
3364
+ const matchingResult = successful.find((r) => r.skill === skillDisplayName);
3365
+ const installDir = matchingResult?.canonicalPath || matchingResult?.path;
3366
+ const { skillFolderHash, hashMethod } = await resolveGlobalLockHashData({
3367
+ sourceType: parsed.type,
3368
+ normalizedSource,
3369
+ skillPathValue,
3370
+ installDir
3371
+ });
3338
3372
  await addSkillToLock(skill.name, {
3339
3373
  source: normalizedSource,
3340
3374
  sourceType: parsed.type,
3341
3375
  sourceUrl: parsed.url,
3342
3376
  skillPath: skillPathValue,
3343
- skillFolderHash
3377
+ skillFolderHash,
3378
+ hashMethod
3344
3379
  });
3345
3380
  } catch {}
3346
3381
  }
@@ -3446,12 +3481,12 @@ async function runAddCommand(args, options = {}) {
3446
3481
  console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: command"));
3447
3482
  console.log();
3448
3483
  console.log(import_picocolors.default.dim(" Usage:"));
3449
- console.log(` ${import_picocolors.default.cyan("npx skills add-c")} ${import_picocolors.default.yellow("<command>")} ${import_picocolors.default.dim("[options]")}`);
3450
- console.log(` ${import_picocolors.default.cyan("npx skills add-c")} ${import_picocolors.default.yellow("[options]")} ${import_picocolors.default.dim("(uses default registry)")}`);
3484
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add-c")} ${import_picocolors.default.yellow("<command>")} ${import_picocolors.default.dim("[options]")}`);
3485
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add-c")} ${import_picocolors.default.yellow("[options]")} ${import_picocolors.default.dim("(uses default registry)")}`);
3451
3486
  console.log();
3452
3487
  console.log(import_picocolors.default.dim(" Example:"));
3453
- console.log(` ${import_picocolors.default.cyan("npx skills add-c")} ${import_picocolors.default.yellow("my-command")}`);
3454
- console.log(` ${import_picocolors.default.cyan("npx skills add-c")} ${import_picocolors.default.yellow("--list")}`);
3488
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add-c")} ${import_picocolors.default.yellow("my-command")}`);
3489
+ console.log(` ${import_picocolors.default.cyan("npx @bud-fe/skills add-c")} ${import_picocolors.default.yellow("--list")}`);
3455
3490
  console.log();
3456
3491
  process.exit(1);
3457
3492
  }
@@ -3795,11 +3830,11 @@ async function promptForFindSkills(options, targetAgents) {
3795
3830
  });
3796
3831
  } catch {
3797
3832
  M.warn("Failed to install find-skills. You can try again with:");
3798
- M.message(import_picocolors.default.dim(" npx skills add vercel-labs/skills@find-skills -g -y --all"));
3833
+ M.message(import_picocolors.default.dim(" npx @bud-fe/skills add vercel-labs/skills@find-skills -g -y --all"));
3799
3834
  }
3800
3835
  } else {
3801
3836
  await dismissPrompt("findSkillsPrompt");
3802
- M.message(import_picocolors.default.dim("You can install it later with: npx skills add vercel-labs/skills@find-skills"));
3837
+ M.message(import_picocolors.default.dim("You can install it later with: npx @bud-fe/skills add vercel-labs/skills@find-skills"));
3803
3838
  }
3804
3839
  } catch {}
3805
3840
  }
@@ -3983,7 +4018,7 @@ function formatNonInteractiveResults(mode, defaultResults, apiResults) {
3983
4018
  const hasDefault = mode === "default" && defaultResults.length > 0;
3984
4019
  const hasApi = mode === "global" && apiResults.length > 0;
3985
4020
  if (hasDefault) {
3986
- lines.push(`${DIM$2}Install with${RESET$2} npx skills add <name>${DIM$2} (From default registry)${RESET$2}`);
4021
+ lines.push(`${DIM$2}Install with${RESET$2} npx @bud-fe/skills add <name>${DIM$2} (From default registry)${RESET$2}`);
3987
4022
  lines.push("");
3988
4023
  for (const skill of defaultResults) {
3989
4024
  lines.push(`${TEXT$1}${skill.name}${RESET$2}`);
@@ -3992,7 +4027,7 @@ function formatNonInteractiveResults(mode, defaultResults, apiResults) {
3992
4027
  }
3993
4028
  }
3994
4029
  if (hasApi) {
3995
- lines.push(`${DIM$2}Install with${RESET$2} npx skills add <owner/repo@skill>${DIM$2} (From https://skills.sh)${RESET$2}`);
4030
+ lines.push(`${DIM$2}Install with${RESET$2} npx @bud-fe/skills add <owner/repo@skill>${DIM$2} (From https://skills.sh)${RESET$2}`);
3996
4031
  lines.push("");
3997
4032
  for (const skill of apiResults.slice(0, 6)) {
3998
4033
  const pkg = skill.source || skill.slug;
@@ -4177,9 +4212,9 @@ async function runFind(args) {
4177
4212
  const { mode, query } = parseFindArgs(args);
4178
4213
  const isNonInteractive = !process.stdin.isTTY;
4179
4214
  const agentTip = `${DIM$2}Tip: if running in a coding agent, follow these steps:${RESET$2}
4180
- ${DIM$2} 1) npx skills find [query]${RESET$2}
4181
- ${DIM$2} 2) npx skills find -g [query]${RESET$2}
4182
- ${DIM$2} 3) npx skills add <owner/repo@skill>${RESET$2}`;
4215
+ ${DIM$2} 1) npx @bud-fe/skills find [query]${RESET$2}
4216
+ ${DIM$2} 2) npx @bud-fe/skills find -g [query]${RESET$2}
4217
+ ${DIM$2} 3) npx @bud-fe/skills add <owner/repo@skill>${RESET$2}`;
4183
4218
  if (query) {
4184
4219
  const { defaultResults, apiResults } = await searchSkillsByMode(query, mode);
4185
4220
  const totalResults = defaultResults.length + apiResults.length;
@@ -4526,7 +4561,7 @@ async function runInstallFromLock(args) {
4526
4561
  const skillEntries = Object.entries(lock.skills);
4527
4562
  if (skillEntries.length === 0) {
4528
4563
  M.warn("No project skills found in skills-lock.json");
4529
- M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx skills add <package>")} (without ${import_picocolors.default.cyan("-g")})`);
4564
+ M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx @bud-fe/skills add <package>")} (without ${import_picocolors.default.cyan("-g")})`);
4530
4565
  return;
4531
4566
  }
4532
4567
  const universalAgentNames = getUniversalAgents();
@@ -4826,6 +4861,142 @@ function parseRemoveOptions(args) {
4826
4861
  options
4827
4862
  };
4828
4863
  }
4864
+ const DEFAULT_DEPS = {
4865
+ fetchGitHubHash: fetchSkillFolderHash,
4866
+ cloneRepo,
4867
+ cleanupTempDir,
4868
+ computeFolderHash: computeSkillFolderHash,
4869
+ pathExists: existsSync
4870
+ };
4871
+ function normalizeSkillFolderPath(skillPath) {
4872
+ let folderPath = skillPath.replace(/\\/g, "/");
4873
+ if (folderPath.endsWith("/SKILL.md")) folderPath = folderPath.slice(0, -9);
4874
+ else if (folderPath.endsWith("SKILL.md")) folderPath = folderPath.slice(0, -8);
4875
+ if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
4876
+ return folderPath;
4877
+ }
4878
+ function resolveHashMethod(entry) {
4879
+ if (entry.hashMethod === "github-tree-sha" || entry.hashMethod === "content-sha256") return entry.hashMethod;
4880
+ if (entry.sourceType === "github") return "github-tree-sha";
4881
+ if ((entry.sourceType === "git" || entry.sourceType === "gitlab") && !!entry.skillFolderHash) return "content-sha256";
4882
+ return null;
4883
+ }
4884
+ async function detectSkillUpdates(lock, options = {}) {
4885
+ const deps = {
4886
+ ...DEFAULT_DEPS,
4887
+ ...options.deps || {}
4888
+ };
4889
+ const githubSkillsBySource = /* @__PURE__ */ new Map();
4890
+ const contentSkillsBySourceUrl = /* @__PURE__ */ new Map();
4891
+ let skippedMissingMetadata = 0;
4892
+ for (const [name, entry] of Object.entries(lock.skills)) {
4893
+ const hashMethod = resolveHashMethod(entry);
4894
+ if (!hashMethod || !entry.skillPath || !entry.skillFolderHash) {
4895
+ skippedMissingMetadata++;
4896
+ continue;
4897
+ }
4898
+ if (hashMethod === "github-tree-sha") {
4899
+ const bucket = githubSkillsBySource.get(entry.source) || [];
4900
+ bucket.push({
4901
+ name,
4902
+ entry
4903
+ });
4904
+ githubSkillsBySource.set(entry.source, bucket);
4905
+ continue;
4906
+ }
4907
+ if (!entry.sourceUrl) {
4908
+ skippedMissingMetadata++;
4909
+ continue;
4910
+ }
4911
+ const bucket = contentSkillsBySourceUrl.get(entry.sourceUrl) || [];
4912
+ bucket.push({
4913
+ name,
4914
+ entry
4915
+ });
4916
+ contentSkillsBySourceUrl.set(entry.sourceUrl, bucket);
4917
+ }
4918
+ const updates = [];
4919
+ const errors = [];
4920
+ let checkedCount = 0;
4921
+ for (const [source, skills] of githubSkillsBySource) for (const { name, entry } of skills) {
4922
+ checkedCount++;
4923
+ try {
4924
+ const latestHash = await deps.fetchGitHubHash(source, entry.skillPath, options.githubToken);
4925
+ if (!latestHash) {
4926
+ errors.push({
4927
+ name,
4928
+ source,
4929
+ error: "Could not fetch from GitHub"
4930
+ });
4931
+ continue;
4932
+ }
4933
+ if (latestHash !== entry.skillFolderHash) updates.push({
4934
+ name,
4935
+ source,
4936
+ entry
4937
+ });
4938
+ } catch (err) {
4939
+ errors.push({
4940
+ name,
4941
+ source,
4942
+ error: err instanceof Error ? err.message : "Unknown error"
4943
+ });
4944
+ }
4945
+ }
4946
+ for (const [sourceUrl, skills] of contentSkillsBySourceUrl) {
4947
+ let tempDir = null;
4948
+ try {
4949
+ tempDir = await deps.cloneRepo(sourceUrl);
4950
+ } catch (err) {
4951
+ const message = err instanceof Error ? err.message : "Failed to clone source repository";
4952
+ for (const { name, entry } of skills) {
4953
+ checkedCount++;
4954
+ errors.push({
4955
+ name,
4956
+ source: entry.source,
4957
+ error: message
4958
+ });
4959
+ }
4960
+ continue;
4961
+ }
4962
+ try {
4963
+ for (const { name, entry } of skills) {
4964
+ checkedCount++;
4965
+ try {
4966
+ const folderPath = normalizeSkillFolderPath(entry.skillPath);
4967
+ const targetDir = folderPath ? join(tempDir, folderPath) : tempDir;
4968
+ if (!deps.pathExists(targetDir)) {
4969
+ errors.push({
4970
+ name,
4971
+ source: entry.source,
4972
+ error: `Skill path not found: ${folderPath}`
4973
+ });
4974
+ continue;
4975
+ }
4976
+ if (await deps.computeFolderHash(targetDir) !== entry.skillFolderHash) updates.push({
4977
+ name,
4978
+ source: entry.source,
4979
+ entry
4980
+ });
4981
+ } catch (err) {
4982
+ errors.push({
4983
+ name,
4984
+ source: entry.source,
4985
+ error: err instanceof Error ? err.message : "Unknown error"
4986
+ });
4987
+ }
4988
+ }
4989
+ } finally {
4990
+ await deps.cleanupTempDir(tempDir).catch(() => {});
4991
+ }
4992
+ }
4993
+ return {
4994
+ checkedCount,
4995
+ skippedMissingMetadata,
4996
+ updates,
4997
+ errors
4998
+ };
4999
+ }
4829
5000
  const __dirname = dirname(fileURLToPath(import.meta.url));
4830
5001
  function getVersion() {
4831
5002
  try {
@@ -4866,20 +5037,20 @@ function showBanner() {
4866
5037
  console.log();
4867
5038
  console.log(`${DIM}The open agent skills ecosystem${RESET}`);
4868
5039
  console.log();
4869
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
4870
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills add-c ${DIM}<command>${RESET} ${DIM}Install a command${RESET}`);
4871
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
4872
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills list${RESET} ${DIM}List installed skills${RESET}`);
4873
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills find ${DIM}[query] [-g]${RESET} ${DIM}Search skills (default registry or global)${RESET}`);
5040
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
5041
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills add-c ${DIM}<command>${RESET} ${DIM}Install a command${RESET}`);
5042
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
5043
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills list${RESET} ${DIM}List installed skills${RESET}`);
5044
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills find ${DIM}[query] [-g]${RESET} ${DIM}Search skills (default registry or global)${RESET}`);
4874
5045
  console.log();
4875
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills check${RESET} ${DIM}Check for updates${RESET}`);
4876
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills update${RESET} ${DIM}Update all skills${RESET}`);
5046
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills check${RESET} ${DIM}Check for updates${RESET}`);
5047
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills update${RESET} ${DIM}Update all skills${RESET}`);
4877
5048
  console.log();
4878
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
4879
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
4880
- console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
5049
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
5050
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
5051
+ console.log(` ${DIM}$${RESET} ${TEXT}npx @bud-fe/skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
4881
5052
  console.log();
4882
- console.log(`${DIM}try:${RESET} npx skills add vercel-labs/agent-skills`);
5053
+ console.log(`${DIM}try:${RESET} npx @bud-fe/skills add vercel-labs/agent-skills`);
4883
5054
  console.log();
4884
5055
  console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
4885
5056
  console.log();
@@ -5043,8 +5214,8 @@ Describe when this skill should be used.
5043
5214
  console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
5044
5215
  console.log();
5045
5216
  console.log(`${DIM}Publishing:${RESET}`);
5046
- console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx skills add <owner>/<repo>${RESET}`);
5047
- console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx skills add https://example.com/${displayPath}${RESET}`);
5217
+ console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx @bud-fe/skills add <owner>/<repo>${RESET}`);
5218
+ console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx @bud-fe/skills add https://example.com/${displayPath}${RESET}`);
5048
5219
  console.log();
5049
5220
  console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
5050
5221
  console.log();
@@ -5080,58 +5251,21 @@ async function runCheck(args = []) {
5080
5251
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
5081
5252
  console.log();
5082
5253
  const lock = readSkillLock();
5083
- const skillNames = Object.keys(lock.skills);
5084
- if (skillNames.length === 0) {
5254
+ if (Object.keys(lock.skills).length === 0) {
5085
5255
  console.log(`${DIM}No skills tracked in lock file.${RESET}`);
5086
- console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
5256
+ console.log(`${DIM}Install skills with${RESET} ${TEXT}npx @bud-fe/skills add <package>${RESET}`);
5087
5257
  return;
5088
5258
  }
5089
- const token = getGitHubToken();
5090
- const skillsBySource = /* @__PURE__ */ new Map();
5091
- let skippedCount = 0;
5092
- for (const skillName of skillNames) {
5093
- const entry = lock.skills[skillName];
5094
- if (!entry) continue;
5095
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) {
5096
- skippedCount++;
5097
- continue;
5098
- }
5099
- const existing = skillsBySource.get(entry.source) || [];
5100
- existing.push({
5101
- name: skillName,
5102
- entry
5103
- });
5104
- skillsBySource.set(entry.source, existing);
5105
- }
5106
- const totalSkills = skillNames.length - skippedCount;
5259
+ const result = await detectSkillUpdates(lock, { githubToken: getGitHubToken() });
5260
+ const totalSkills = result.checkedCount;
5107
5261
  if (totalSkills === 0) {
5108
- console.log(`${DIM}No GitHub skills to check.${RESET}`);
5262
+ console.log(`${DIM}No skills with update metadata to check.${RESET}`);
5263
+ if (result.skippedMissingMetadata > 0) console.log(`${DIM}${result.skippedMissingMetadata} skill(s) are missing hash metadata (run${RESET} ${TEXT}npx @bud-fe/skills add <source> -g${RESET} ${DIM}to reinstall and track updates).${RESET}`);
5109
5264
  return;
5110
5265
  }
5111
5266
  console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
5112
- const updates = [];
5113
- const errors = [];
5114
- for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
5115
- const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
5116
- if (!latestHash) {
5117
- errors.push({
5118
- name,
5119
- source,
5120
- error: "Could not fetch from GitHub"
5121
- });
5122
- continue;
5123
- }
5124
- if (latestHash !== entry.skillFolderHash) updates.push({
5125
- name,
5126
- source
5127
- });
5128
- } catch (err) {
5129
- errors.push({
5130
- name,
5131
- source,
5132
- error: err instanceof Error ? err.message : "Unknown error"
5133
- });
5134
- }
5267
+ const updates = result.updates;
5268
+ const errors = result.errors;
5135
5269
  console.log();
5136
5270
  if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
5137
5271
  else {
@@ -5142,12 +5276,16 @@ async function runCheck(args = []) {
5142
5276
  console.log(` ${DIM}source: ${update.source}${RESET}`);
5143
5277
  }
5144
5278
  console.log();
5145
- console.log(`${DIM}Run${RESET} ${TEXT}npx skills update${RESET} ${DIM}to update all skills${RESET}`);
5279
+ console.log(`${DIM}Run${RESET} ${TEXT}npx @bud-fe/skills update${RESET} ${DIM}to update all skills${RESET}`);
5146
5280
  }
5147
5281
  if (errors.length > 0) {
5148
5282
  console.log();
5149
5283
  console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
5150
5284
  }
5285
+ if (result.skippedMissingMetadata > 0) {
5286
+ console.log();
5287
+ console.log(`${DIM}Skipped ${result.skippedMissingMetadata} skill(s) missing hash metadata (reinstall to enable update tracking).${RESET}`);
5288
+ }
5151
5289
  track({
5152
5290
  event: "check",
5153
5291
  skillCount: String(totalSkills),
@@ -5159,31 +5297,16 @@ async function runUpdate() {
5159
5297
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
5160
5298
  console.log();
5161
5299
  const lock = readSkillLock();
5162
- const skillNames = Object.keys(lock.skills);
5163
- if (skillNames.length === 0) {
5300
+ if (Object.keys(lock.skills).length === 0) {
5164
5301
  console.log(`${DIM}No skills tracked in lock file.${RESET}`);
5165
- console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
5302
+ console.log(`${DIM}Install skills with${RESET} ${TEXT}npx @bud-fe/skills add <package>${RESET}`);
5166
5303
  return;
5167
5304
  }
5168
- const token = getGitHubToken();
5169
- const updates = [];
5170
- let checkedCount = 0;
5171
- for (const skillName of skillNames) {
5172
- const entry = lock.skills[skillName];
5173
- if (!entry) continue;
5174
- if (entry.sourceType !== "github" || !entry.skillFolderHash || !entry.skillPath) continue;
5175
- checkedCount++;
5176
- try {
5177
- const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
5178
- if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
5179
- name: skillName,
5180
- source: entry.source,
5181
- entry
5182
- });
5183
- } catch {}
5184
- }
5185
- if (checkedCount === 0) {
5186
- console.log(`${DIM}No skills to check.${RESET}`);
5305
+ const result = await detectSkillUpdates(lock, { githubToken: getGitHubToken() });
5306
+ const updates = result.updates;
5307
+ if (result.checkedCount === 0) {
5308
+ console.log(`${DIM}No skills with update metadata to check.${RESET}`);
5309
+ if (result.skippedMissingMetadata > 0) console.log(`${DIM}${result.skippedMissingMetadata} skill(s) are missing hash metadata (reinstall to enable update tracking).${RESET}`);
5187
5310
  return;
5188
5311
  }
5189
5312
  if (updates.length === 0) {
@@ -5198,22 +5321,21 @@ async function runUpdate() {
5198
5321
  for (const update of updates) {
5199
5322
  console.log(`${TEXT}Updating ${update.name}...${RESET}`);
5200
5323
  let installUrl = update.entry.sourceUrl;
5201
- if (update.entry.skillPath) {
5324
+ const addArgs = [
5325
+ "-y",
5326
+ "skills",
5327
+ "add"
5328
+ ];
5329
+ if (update.entry.sourceType === "github" && update.entry.skillPath) {
5202
5330
  let skillFolder = update.entry.skillPath;
5203
5331
  if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
5204
5332
  else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
5205
5333
  if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
5206
5334
  installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
5207
5335
  installUrl = `${installUrl}/tree/main/${skillFolder}`;
5208
- }
5209
- if (spawnSync("npx", [
5210
- "-y",
5211
- "skills",
5212
- "add",
5213
- installUrl,
5214
- "-g",
5215
- "-y"
5216
- ], { stdio: [
5336
+ addArgs.push(installUrl, "-g", "-y");
5337
+ } else addArgs.push(installUrl, "--skill", update.name, "-g", "-y");
5338
+ if (spawnSync("npx", addArgs, { stdio: [
5217
5339
  "inherit",
5218
5340
  "pipe",
5219
5341
  "pipe"
@@ -5228,6 +5350,7 @@ async function runUpdate() {
5228
5350
  console.log();
5229
5351
  if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
5230
5352
  if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
5353
+ if (result.skippedMissingMetadata > 0) console.log(`${DIM}Skipped ${result.skippedMissingMetadata} skill(s) missing hash metadata (reinstall to enable update tracking).${RESET}`);
5231
5354
  track({
5232
5355
  event: "update",
5233
5356
  skillCount: String(updates.length),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bud-fe/skills",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {