rulesync 7.15.2 → 7.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -14,12 +14,14 @@ import {
14
14
  RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
15
15
  RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
16
16
  RULESYNC_CONFIG_RELATIVE_FILE_PATH,
17
+ RULESYNC_CONFIG_SCHEMA_URL,
17
18
  RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH,
18
19
  RULESYNC_HOOKS_FILE_NAME,
19
20
  RULESYNC_HOOKS_RELATIVE_FILE_PATH,
20
21
  RULESYNC_IGNORE_RELATIVE_FILE_PATH,
21
22
  RULESYNC_MCP_FILE_NAME,
22
23
  RULESYNC_MCP_RELATIVE_FILE_PATH,
24
+ RULESYNC_MCP_SCHEMA_URL,
23
25
  RULESYNC_OVERVIEW_FILE_NAME,
24
26
  RULESYNC_RELATIVE_DIR_PATH,
25
27
  RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -47,6 +49,7 @@ import {
47
49
  directoryExists,
48
50
  ensureDir,
49
51
  fileExists,
52
+ findControlCharacter,
50
53
  findFilesByGlobs,
51
54
  formatError,
52
55
  generate,
@@ -62,7 +65,7 @@ import {
62
65
  removeTempDirectory,
63
66
  stringifyFrontmatter,
64
67
  writeFileContent
65
- } from "../chunk-WY325EI7.js";
68
+ } from "../chunk-ZQUEAGJI.js";
66
69
 
67
70
  // src/cli/index.ts
68
71
  import { Command } from "commander";
@@ -1383,6 +1386,7 @@ async function createConfigFile() {
1383
1386
  path2,
1384
1387
  JSON.stringify(
1385
1388
  {
1389
+ $schema: RULESYNC_CONFIG_SCHEMA_URL,
1386
1390
  targets: ["copilot", "cursor", "claudecode", "codexcli"],
1387
1391
  features: ["rules", "ignore", "mcp", "commands", "subagents", "skills", "hooks"],
1388
1392
  baseDirs: ["."],
@@ -1440,6 +1444,7 @@ globs: ["**/*"]
1440
1444
  const sampleMcpFile = {
1441
1445
  filename: "mcp.json",
1442
1446
  content: `{
1447
+ "$schema": "${RULESYNC_MCP_SCHEMA_URL}",
1443
1448
  "mcpServers": {
1444
1449
  "serena": {
1445
1450
  "type": "stdio",
@@ -1618,21 +1623,12 @@ import { Semaphore as Semaphore2 } from "es-toolkit/promise";
1618
1623
 
1619
1624
  // src/lib/git-client.ts
1620
1625
  import { execFile } from "child_process";
1621
- import { join as join4 } from "path";
1626
+ import { isAbsolute, join as join4, relative } from "path";
1622
1627
  import { promisify } from "util";
1623
1628
  var execFileAsync = promisify(execFile);
1624
1629
  var GIT_TIMEOUT_MS = 6e4;
1625
- var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/|[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+)/;
1630
+ var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/).+$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+$/;
1626
1631
  var INSECURE_URL_SCHEMES = /^(git:\/\/|http:\/\/)/;
1627
- function findControlCharacter(value) {
1628
- for (let i = 0; i < value.length; i++) {
1629
- const code = value.charCodeAt(i);
1630
- if (code >= 0 && code <= 31 || code === 127) {
1631
- return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
1632
- }
1633
- }
1634
- return null;
1635
- }
1636
1632
  var GitClientError = class extends Error {
1637
1633
  constructor(message, cause) {
1638
1634
  super(message, { cause });
@@ -1688,6 +1684,7 @@ async function resolveDefaultRef(url) {
1688
1684
  const ref = stdout.match(/^ref: refs\/heads\/(.+)\tHEAD$/m)?.[1];
1689
1685
  const sha = stdout.match(/^([0-9a-f]{40})\tHEAD$/m)?.[1];
1690
1686
  if (!ref || !sha) throw new GitClientError(`Could not parse default branch from: ${url}`);
1687
+ validateRef(ref);
1691
1688
  return { ref, sha };
1692
1689
  } catch (error) {
1693
1690
  if (error instanceof GitClientError) throw error;
@@ -1714,6 +1711,17 @@ async function fetchSkillFiles(params) {
1714
1711
  const { url, ref, skillsPath } = params;
1715
1712
  validateGitUrl(url);
1716
1713
  validateRef(ref);
1714
+ if (skillsPath.split(/[/\\]/).includes("..") || isAbsolute(skillsPath)) {
1715
+ throw new GitClientError(
1716
+ `Invalid skillsPath "${skillsPath}": must be a relative path without ".."`
1717
+ );
1718
+ }
1719
+ const ctrl = findControlCharacter(skillsPath);
1720
+ if (ctrl) {
1721
+ throw new GitClientError(
1722
+ `skillsPath contains control character ${ctrl.hex} at position ${ctrl.position}`
1723
+ );
1724
+ }
1717
1725
  await checkGitAvailable();
1718
1726
  const tmpDir = await createTempDirectory("rulesync-git-");
1719
1727
  try {
@@ -1748,7 +1756,9 @@ async function fetchSkillFiles(params) {
1748
1756
  }
1749
1757
  }
1750
1758
  var MAX_WALK_DEPTH = 20;
1751
- async function walkDirectory(dir, baseDir, depth = 0) {
1759
+ var MAX_TOTAL_FILES = 1e4;
1760
+ var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
1761
+ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }) {
1752
1762
  if (depth > MAX_WALK_DEPTH) {
1753
1763
  throw new GitClientError(
1754
1764
  `Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
@@ -1763,7 +1773,7 @@ async function walkDirectory(dir, baseDir, depth = 0) {
1763
1773
  continue;
1764
1774
  }
1765
1775
  if (await directoryExists(fullPath)) {
1766
- results.push(...await walkDirectory(fullPath, baseDir, depth + 1));
1776
+ results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx));
1767
1777
  } else {
1768
1778
  const size = await getFileSize(fullPath);
1769
1779
  if (size > MAX_FILE_SIZE) {
@@ -1772,8 +1782,20 @@ async function walkDirectory(dir, baseDir, depth = 0) {
1772
1782
  );
1773
1783
  continue;
1774
1784
  }
1785
+ ctx.totalFiles++;
1786
+ ctx.totalSize += size;
1787
+ if (ctx.totalFiles >= MAX_TOTAL_FILES) {
1788
+ throw new GitClientError(
1789
+ `Repository exceeds max file count of ${MAX_TOTAL_FILES}. Aborting to prevent resource exhaustion.`
1790
+ );
1791
+ }
1792
+ if (ctx.totalSize >= MAX_TOTAL_SIZE) {
1793
+ throw new GitClientError(
1794
+ `Repository exceeds max total size of ${MAX_TOTAL_SIZE / 1024 / 1024}MB. Aborting to prevent resource exhaustion.`
1795
+ );
1796
+ }
1775
1797
  const content = await readFileContent(fullPath);
1776
- results.push({ relativePath: fullPath.substring(baseDir.length + 1), content, size });
1798
+ results.push({ relativePath: relative(baseDir, fullPath), content, size });
1777
1799
  }
1778
1800
  }
1779
1801
  return results;
@@ -1782,14 +1804,14 @@ async function walkDirectory(dir, baseDir, depth = 0) {
1782
1804
  // src/lib/sources-lock.ts
1783
1805
  import { createHash } from "crypto";
1784
1806
  import { join as join5 } from "path";
1785
- import { optional, z as z4 } from "zod/mini";
1807
+ import { optional, refine, z as z4 } from "zod/mini";
1786
1808
  var LOCKFILE_VERSION = 1;
1787
1809
  var LockedSkillSchema = z4.object({
1788
1810
  integrity: z4.string()
1789
1811
  });
1790
1812
  var LockedSourceSchema = z4.object({
1791
1813
  requestedRef: optional(z4.string()),
1792
- resolvedRef: z4.string(),
1814
+ resolvedRef: z4.string().check(refine((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
1793
1815
  resolvedAt: optional(z4.string()),
1794
1816
  skills: z4.record(z4.string(), LockedSkillSchema)
1795
1817
  });
@@ -1992,6 +2014,8 @@ async function resolveAndFetchSources(params) {
1992
2014
  logger.error(`Failed to fetch source "${sourceEntry.source}": ${formatError(error)}`);
1993
2015
  if (error instanceof GitHubClientError) {
1994
2016
  logGitHubAuthHints(error);
2017
+ } else if (error instanceof GitClientError) {
2018
+ logGitClientHints(error);
1995
2019
  }
1996
2020
  }
1997
2021
  }
@@ -2012,6 +2036,13 @@ async function resolveAndFetchSources(params) {
2012
2036
  }
2013
2037
  return { fetchedSkillCount: totalSkillCount, sourcesProcessed: sources.length };
2014
2038
  }
2039
+ function logGitClientHints(error) {
2040
+ if (error.message.includes("not installed")) {
2041
+ logger.info("Hint: Install git and ensure it is available on your PATH.");
2042
+ } else {
2043
+ logger.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
2044
+ }
2045
+ }
2015
2046
  async function checkLockedSkillsExist(curatedDir, skillNames) {
2016
2047
  if (skillNames.length === 0) return true;
2017
2048
  for (const name of skillNames) {
@@ -2021,9 +2052,88 @@ async function checkLockedSkillsExist(curatedDir, skillNames) {
2021
2052
  }
2022
2053
  return true;
2023
2054
  }
2055
+ async function cleanPreviousCuratedSkills(curatedDir, lockedSkillNames) {
2056
+ const resolvedCuratedDir = resolve(curatedDir);
2057
+ for (const prevSkill of lockedSkillNames) {
2058
+ const prevDir = join6(curatedDir, prevSkill);
2059
+ if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
2060
+ logger.warn(
2061
+ `Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
2062
+ );
2063
+ continue;
2064
+ }
2065
+ if (await directoryExists(prevDir)) {
2066
+ await removeDirectory(prevDir);
2067
+ }
2068
+ }
2069
+ }
2070
+ function shouldSkipSkill(params) {
2071
+ const { skillName, sourceKey, localSkillNames, alreadyFetchedSkillNames } = params;
2072
+ if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
2073
+ logger.warn(
2074
+ `Skipping skill with invalid name "${skillName}" from ${sourceKey}: contains path traversal characters.`
2075
+ );
2076
+ return true;
2077
+ }
2078
+ if (localSkillNames.has(skillName)) {
2079
+ logger.debug(
2080
+ `Skipping remote skill "${skillName}" from ${sourceKey}: local skill takes precedence.`
2081
+ );
2082
+ return true;
2083
+ }
2084
+ if (alreadyFetchedSkillNames.has(skillName)) {
2085
+ logger.warn(
2086
+ `Skipping duplicate skill "${skillName}" from ${sourceKey}: already fetched from another source.`
2087
+ );
2088
+ return true;
2089
+ }
2090
+ return false;
2091
+ }
2092
+ async function writeSkillAndComputeIntegrity(params) {
2093
+ const { skillName, files, curatedDir, locked, resolvedSha, sourceKey } = params;
2094
+ const written = [];
2095
+ for (const file of files) {
2096
+ checkPathTraversal({
2097
+ relativePath: file.relativePath,
2098
+ intendedRootDir: join6(curatedDir, skillName)
2099
+ });
2100
+ await writeFileContent(join6(curatedDir, skillName, file.relativePath), file.content);
2101
+ written.push({ path: file.relativePath, content: file.content });
2102
+ }
2103
+ const integrity = computeSkillIntegrity(written);
2104
+ const lockedSkillEntry = locked?.skills[skillName];
2105
+ if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
2106
+ logger.warn(
2107
+ `Integrity mismatch for skill "${skillName}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
2108
+ );
2109
+ }
2110
+ return { integrity };
2111
+ }
2112
+ function buildLockUpdate(params) {
2113
+ const { lock, sourceKey, fetchedSkills, locked, requestedRef, resolvedSha } = params;
2114
+ const fetchedNames = Object.keys(fetchedSkills);
2115
+ const mergedSkills = { ...fetchedSkills };
2116
+ if (locked) {
2117
+ for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
2118
+ if (!(skillName in mergedSkills)) {
2119
+ mergedSkills[skillName] = skillEntry;
2120
+ }
2121
+ }
2122
+ }
2123
+ const updatedLock = setLockedSource(lock, sourceKey, {
2124
+ requestedRef,
2125
+ resolvedRef: resolvedSha,
2126
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
2127
+ skills: mergedSkills
2128
+ });
2129
+ logger.info(
2130
+ `Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
2131
+ );
2132
+ return { updatedLock, fetchedNames };
2133
+ }
2024
2134
  async function fetchSource(params) {
2025
2135
  const { sourceEntry, client, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources } = params;
2026
- let { lock } = params;
2136
+ const { lock } = params;
2027
2137
  const parsed = parseSource(sourceEntry.source);
2028
2138
  if (parsed.provider === "gitlab") {
2029
2139
  logger.warn(`GitLab sources are not yet supported. Skipping "${sourceEntry.source}".`);
@@ -2076,37 +2186,15 @@ async function fetchSource(params) {
2076
2186
  const semaphore = new Semaphore2(FETCH_CONCURRENCY_LIMIT);
2077
2187
  const fetchedSkills = {};
2078
2188
  if (locked) {
2079
- const resolvedCuratedDir = resolve(curatedDir);
2080
- for (const prevSkill of lockedSkillNames) {
2081
- const prevDir = join6(curatedDir, prevSkill);
2082
- if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
2083
- logger.warn(
2084
- `Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
2085
- );
2086
- continue;
2087
- }
2088
- if (await directoryExists(prevDir)) {
2089
- await removeDirectory(prevDir);
2090
- }
2091
- }
2189
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
2092
2190
  }
2093
2191
  for (const skillDir of filteredDirs) {
2094
- if (skillDir.name.includes("..") || skillDir.name.includes("/") || skillDir.name.includes("\\")) {
2095
- logger.warn(
2096
- `Skipping skill with invalid name "${skillDir.name}" from ${sourceKey}: contains path traversal characters.`
2097
- );
2098
- continue;
2099
- }
2100
- if (localSkillNames.has(skillDir.name)) {
2101
- logger.debug(
2102
- `Skipping remote skill "${skillDir.name}" from ${sourceKey}: local skill takes precedence.`
2103
- );
2104
- continue;
2105
- }
2106
- if (alreadyFetchedSkillNames.has(skillDir.name)) {
2107
- logger.warn(
2108
- `Skipping duplicate skill "${skillDir.name}" from ${sourceKey}: already fetched from another source.`
2109
- );
2192
+ if (shouldSkipSkill({
2193
+ skillName: skillDir.name,
2194
+ sourceKey,
2195
+ localSkillNames,
2196
+ alreadyFetchedSkillNames
2197
+ })) {
2110
2198
  continue;
2111
2199
  }
2112
2200
  const allFiles = await listDirectoryRecursive({
@@ -2129,55 +2217,39 @@ async function fetchSource(params) {
2129
2217
  const skillFiles = [];
2130
2218
  for (const file of files) {
2131
2219
  const relativeToSkill = file.path.substring(skillDir.path.length + 1);
2132
- const localFilePath = join6(curatedDir, skillDir.name, relativeToSkill);
2133
- checkPathTraversal({
2134
- relativePath: relativeToSkill,
2135
- intendedRootDir: join6(curatedDir, skillDir.name)
2136
- });
2137
2220
  const content = await withSemaphore(
2138
2221
  semaphore,
2139
2222
  () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
2140
2223
  );
2141
- await writeFileContent(localFilePath, content);
2142
- skillFiles.push({ path: relativeToSkill, content });
2143
- }
2144
- const integrity = computeSkillIntegrity(skillFiles);
2145
- const lockedSkillEntry = locked?.skills[skillDir.name];
2146
- if (lockedSkillEntry && lockedSkillEntry.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
2147
- logger.warn(
2148
- `Integrity mismatch for skill "${skillDir.name}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
2149
- );
2150
- }
2151
- fetchedSkills[skillDir.name] = { integrity };
2224
+ skillFiles.push({ relativePath: relativeToSkill, content });
2225
+ }
2226
+ fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
2227
+ skillName: skillDir.name,
2228
+ files: skillFiles,
2229
+ curatedDir,
2230
+ locked,
2231
+ resolvedSha,
2232
+ sourceKey
2233
+ });
2152
2234
  logger.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
2153
2235
  }
2154
- const fetchedNames = Object.keys(fetchedSkills);
2155
- const mergedSkills = { ...fetchedSkills };
2156
- if (locked) {
2157
- for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
2158
- if (!(skillName in mergedSkills)) {
2159
- mergedSkills[skillName] = skillEntry;
2160
- }
2161
- }
2162
- }
2163
- lock = setLockedSource(lock, sourceKey, {
2236
+ const result = buildLockUpdate({
2237
+ lock,
2238
+ sourceKey,
2239
+ fetchedSkills,
2240
+ locked,
2164
2241
  requestedRef,
2165
- resolvedRef: resolvedSha,
2166
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
2167
- skills: mergedSkills
2242
+ resolvedSha
2168
2243
  });
2169
- logger.info(
2170
- `Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
2171
- );
2172
2244
  return {
2173
- skillCount: fetchedNames.length,
2174
- fetchedSkillNames: fetchedNames,
2175
- updatedLock: lock
2245
+ skillCount: result.fetchedNames.length,
2246
+ fetchedSkillNames: result.fetchedNames,
2247
+ updatedLock: result.updatedLock
2176
2248
  };
2177
2249
  }
2178
2250
  async function fetchSourceViaGit(params) {
2179
2251
  const { sourceEntry, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources, frozen } = params;
2180
- let { lock } = params;
2252
+ const { lock } = params;
2181
2253
  const url = sourceEntry.source;
2182
2254
  const locked = getLockedSource(lock, url);
2183
2255
  const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
@@ -2233,68 +2305,35 @@ async function fetchSourceViaGit(params) {
2233
2305
  const allNames = [...skillFileMap.keys()];
2234
2306
  const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
2235
2307
  if (locked) {
2236
- const base = resolve(curatedDir);
2237
- for (const prev of lockedSkillNames) {
2238
- const dir = join6(curatedDir, prev);
2239
- if (resolve(dir).startsWith(base + sep) && await directoryExists(dir)) {
2240
- await removeDirectory(dir);
2241
- }
2242
- }
2308
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
2243
2309
  }
2244
2310
  const fetchedSkills = {};
2245
2311
  for (const skillName of filteredNames) {
2246
- if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
2247
- logger.warn(
2248
- `Skipping skill with invalid name "${skillName}" from ${url}: contains path traversal characters.`
2249
- );
2250
- continue;
2251
- }
2252
- if (localSkillNames.has(skillName)) {
2253
- logger.debug(
2254
- `Skipping remote skill "${skillName}" from ${url}: local skill takes precedence.`
2255
- );
2256
- continue;
2257
- }
2258
- if (alreadyFetchedSkillNames.has(skillName)) {
2259
- logger.warn(
2260
- `Skipping duplicate skill "${skillName}" from ${url}: already fetched from another source.`
2261
- );
2312
+ if (shouldSkipSkill({ skillName, sourceKey: url, localSkillNames, alreadyFetchedSkillNames })) {
2262
2313
  continue;
2263
2314
  }
2264
- const files = skillFileMap.get(skillName) ?? [];
2265
- const written = [];
2266
- for (const file of files) {
2267
- checkPathTraversal({
2268
- relativePath: file.relativePath,
2269
- intendedRootDir: join6(curatedDir, skillName)
2270
- });
2271
- await writeFileContent(join6(curatedDir, skillName, file.relativePath), file.content);
2272
- written.push({ path: file.relativePath, content: file.content });
2273
- }
2274
- const integrity = computeSkillIntegrity(written);
2275
- const lockedSkillEntry = locked?.skills[skillName];
2276
- if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
2277
- logger.warn(`Integrity mismatch for skill "${skillName}" from ${url}.`);
2278
- }
2279
- fetchedSkills[skillName] = { integrity };
2280
- }
2281
- const fetchedNames = Object.keys(fetchedSkills);
2282
- const mergedSkills = { ...fetchedSkills };
2283
- if (locked) {
2284
- for (const [k, v] of Object.entries(locked.skills)) {
2285
- if (!(k in mergedSkills)) mergedSkills[k] = v;
2286
- }
2315
+ fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
2316
+ skillName,
2317
+ files: skillFileMap.get(skillName) ?? [],
2318
+ curatedDir,
2319
+ locked,
2320
+ resolvedSha,
2321
+ sourceKey: url
2322
+ });
2287
2323
  }
2288
- lock = setLockedSource(lock, url, {
2324
+ const result = buildLockUpdate({
2325
+ lock,
2326
+ sourceKey: url,
2327
+ fetchedSkills,
2328
+ locked,
2289
2329
  requestedRef,
2290
- resolvedRef: resolvedSha,
2291
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
2292
- skills: mergedSkills
2330
+ resolvedSha
2293
2331
  });
2294
- logger.info(
2295
- `Fetched ${fetchedNames.length} skill(s) from ${url}: ${fetchedNames.join(", ") || "(none)"}`
2296
- );
2297
- return { skillCount: fetchedNames.length, fetchedSkillNames: fetchedNames, updatedLock: lock };
2332
+ return {
2333
+ skillCount: result.fetchedNames.length,
2334
+ fetchedSkillNames: result.fetchedNames,
2335
+ updatedLock: result.updatedLock
2336
+ };
2298
2337
  }
2299
2338
 
2300
2339
  // src/cli/commands/install.ts
@@ -4110,7 +4149,7 @@ async function updateCommand(currentVersion, options) {
4110
4149
  }
4111
4150
 
4112
4151
  // src/cli/index.ts
4113
- var getVersion = () => "7.15.2";
4152
+ var getVersion = () => "7.17.0";
4114
4153
  var main = async () => {
4115
4154
  const program = new Command();
4116
4155
  const version = getVersion();