rulesync 7.15.2 → 7.16.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.
@@ -408,6 +408,7 @@ var import_node_path5 = require("path");
408
408
 
409
409
  // src/utils/frontmatter.ts
410
410
  var import_gray_matter = __toESM(require("gray-matter"), 1);
411
+ var import_js_yaml = require("js-yaml");
411
412
  function isPlainObject(value) {
412
413
  if (value === null || typeof value !== "object") return false;
413
414
  const prototype = Object.getPrototypeOf(value);
@@ -446,8 +447,55 @@ function deepRemoveNullishObject(obj) {
446
447
  }
447
448
  return result;
448
449
  }
449
- function stringifyFrontmatter(body, frontmatter) {
450
- const cleanFrontmatter = deepRemoveNullishObject(frontmatter);
450
+ function deepFlattenStringsValue(value) {
451
+ if (value === null || value === void 0) {
452
+ return void 0;
453
+ }
454
+ if (typeof value === "string") {
455
+ return value.replace(/\n+/g, " ").trim();
456
+ }
457
+ if (Array.isArray(value)) {
458
+ const cleanedArray = value.map((item) => deepFlattenStringsValue(item)).filter((item) => item !== void 0);
459
+ return cleanedArray;
460
+ }
461
+ if (isPlainObject(value)) {
462
+ const result = {};
463
+ for (const [key, val] of Object.entries(value)) {
464
+ const cleaned = deepFlattenStringsValue(val);
465
+ if (cleaned !== void 0) {
466
+ result[key] = cleaned;
467
+ }
468
+ }
469
+ return result;
470
+ }
471
+ return value;
472
+ }
473
+ function deepFlattenStringsObject(obj) {
474
+ if (!obj || typeof obj !== "object") {
475
+ return {};
476
+ }
477
+ const result = {};
478
+ for (const [key, val] of Object.entries(obj)) {
479
+ const cleaned = deepFlattenStringsValue(val);
480
+ if (cleaned !== void 0) {
481
+ result[key] = cleaned;
482
+ }
483
+ }
484
+ return result;
485
+ }
486
+ function stringifyFrontmatter(body, frontmatter, options) {
487
+ const { avoidBlockScalars = false } = options ?? {};
488
+ const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
489
+ if (avoidBlockScalars) {
490
+ return import_gray_matter.default.stringify(body, cleanFrontmatter, {
491
+ engines: {
492
+ yaml: {
493
+ parse: (input) => (0, import_js_yaml.load)(input) ?? {},
494
+ stringify: (data) => (0, import_js_yaml.dump)(data, { lineWidth: -1 })
495
+ }
496
+ }
497
+ });
498
+ }
451
499
  return import_gray_matter.default.stringify(body, cleanFrontmatter);
452
500
  }
453
501
  function parseFrontmatter(content, filePath) {
@@ -1599,7 +1647,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1599
1647
  }
1600
1648
  super({
1601
1649
  ...rest,
1602
- fileContent: stringifyFrontmatter(body, frontmatter)
1650
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
1603
1651
  });
1604
1652
  this.frontmatter = frontmatter;
1605
1653
  this.body = body;
@@ -6025,12 +6073,26 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6025
6073
  json;
6026
6074
  constructor(params) {
6027
6075
  super(params);
6028
- this.json = this.fileContent !== void 0 ? JSON.parse(this.fileContent) : {};
6076
+ if (this.fileContent !== void 0) {
6077
+ try {
6078
+ this.json = JSON.parse(this.fileContent);
6079
+ } catch (error) {
6080
+ throw new Error(
6081
+ `Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`,
6082
+ { cause: error }
6083
+ );
6084
+ }
6085
+ } else {
6086
+ this.json = {};
6087
+ }
6029
6088
  }
6030
6089
  getJson() {
6031
6090
  return this.json;
6032
6091
  }
6033
- static getSettablePaths() {
6092
+ isDeletable() {
6093
+ return !this.global;
6094
+ }
6095
+ static getSettablePaths(_options) {
6034
6096
  return {
6035
6097
  relativeDirPath: ".cursor",
6036
6098
  relativeFilePath: "mcp.json"
@@ -6038,41 +6100,62 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6038
6100
  }
6039
6101
  static async fromFile({
6040
6102
  baseDir = process.cwd(),
6041
- validate = true
6103
+ validate = true,
6104
+ global = false
6042
6105
  }) {
6043
- const fileContent = await readFileContent(
6044
- (0, import_node_path46.join)(
6045
- baseDir,
6046
- this.getSettablePaths().relativeDirPath,
6047
- this.getSettablePaths().relativeFilePath
6048
- )
6049
- );
6106
+ const paths = this.getSettablePaths({ global });
6107
+ const filePath = (0, import_node_path46.join)(baseDir, paths.relativeDirPath, paths.relativeFilePath);
6108
+ const fileContent = await readFileContentOrNull(filePath) ?? '{"mcpServers":{}}';
6109
+ let json;
6110
+ try {
6111
+ json = JSON.parse(fileContent);
6112
+ } catch (error) {
6113
+ throw new Error(
6114
+ `Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6115
+ { cause: error }
6116
+ );
6117
+ }
6118
+ const newJson = { ...json, mcpServers: json.mcpServers ?? {} };
6050
6119
  return new _CursorMcp({
6051
6120
  baseDir,
6052
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6053
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6054
- fileContent,
6055
- validate
6121
+ relativeDirPath: paths.relativeDirPath,
6122
+ relativeFilePath: paths.relativeFilePath,
6123
+ fileContent: JSON.stringify(newJson, null, 2),
6124
+ validate,
6125
+ global
6056
6126
  });
6057
6127
  }
6058
- static fromRulesyncMcp({
6128
+ static async fromRulesyncMcp({
6059
6129
  baseDir = process.cwd(),
6060
6130
  rulesyncMcp,
6061
- validate = true
6131
+ validate = true,
6132
+ global = false
6062
6133
  }) {
6063
- const json = rulesyncMcp.getJson();
6064
- const mcpServers = isMcpServers(json.mcpServers) ? json.mcpServers : {};
6134
+ const paths = this.getSettablePaths({ global });
6135
+ const fileContent = await readOrInitializeFileContent(
6136
+ (0, import_node_path46.join)(baseDir, paths.relativeDirPath, paths.relativeFilePath),
6137
+ JSON.stringify({ mcpServers: {} }, null, 2)
6138
+ );
6139
+ let json;
6140
+ try {
6141
+ json = JSON.parse(fileContent);
6142
+ } catch (error) {
6143
+ throw new Error(
6144
+ `Failed to parse Cursor MCP config at ${(0, import_node_path46.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6145
+ { cause: error }
6146
+ );
6147
+ }
6148
+ const rulesyncJson = rulesyncMcp.getJson();
6149
+ const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
6065
6150
  const transformedServers = convertEnvToCursorFormat(mcpServers);
6066
- const cursorConfig = {
6067
- mcpServers: transformedServers
6068
- };
6069
- const fileContent = JSON.stringify(cursorConfig, null, 2);
6151
+ const cursorConfig = { ...json, mcpServers: transformedServers };
6070
6152
  return new _CursorMcp({
6071
6153
  baseDir,
6072
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6073
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6074
- fileContent,
6075
- validate
6154
+ relativeDirPath: paths.relativeDirPath,
6155
+ relativeFilePath: paths.relativeFilePath,
6156
+ fileContent: JSON.stringify(cursorConfig, null, 2),
6157
+ validate,
6158
+ global
6076
6159
  });
6077
6160
  }
6078
6161
  toRulesyncMcp() {
@@ -6096,14 +6179,16 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6096
6179
  static forDeletion({
6097
6180
  baseDir = process.cwd(),
6098
6181
  relativeDirPath,
6099
- relativeFilePath
6182
+ relativeFilePath,
6183
+ global = false
6100
6184
  }) {
6101
6185
  return new _CursorMcp({
6102
6186
  baseDir,
6103
6187
  relativeDirPath,
6104
6188
  relativeFilePath,
6105
6189
  fileContent: "{}",
6106
- validate: false
6190
+ validate: false,
6191
+ global
6107
6192
  });
6108
6193
  }
6109
6194
  };
@@ -6932,7 +7017,7 @@ var toolMcpFactories = /* @__PURE__ */ new Map([
6932
7017
  class: CursorMcp,
6933
7018
  meta: {
6934
7019
  supportsProject: true,
6935
- supportsGlobal: false,
7020
+ supportsGlobal: true,
6936
7021
  supportsEnabledTools: false,
6937
7022
  supportsDisabledTools: false
6938
7023
  }
@@ -7645,9 +7730,15 @@ var import_node_path59 = require("path");
7645
7730
  var DirFeatureProcessor = class {
7646
7731
  baseDir;
7647
7732
  dryRun;
7648
- constructor({ baseDir = process.cwd(), dryRun = false }) {
7733
+ avoidBlockScalars;
7734
+ constructor({
7735
+ baseDir = process.cwd(),
7736
+ dryRun = false,
7737
+ avoidBlockScalars = false
7738
+ }) {
7649
7739
  this.baseDir = baseDir;
7650
7740
  this.dryRun = dryRun;
7741
+ this.avoidBlockScalars = avoidBlockScalars;
7651
7742
  }
7652
7743
  /**
7653
7744
  * Return tool targets that this feature supports.
@@ -7673,7 +7764,9 @@ var DirFeatureProcessor = class {
7673
7764
  let mainFileContent;
7674
7765
  if (mainFile) {
7675
7766
  const mainFilePath = (0, import_node_path59.join)(dirPath, mainFile.name);
7676
- const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter);
7767
+ const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter, {
7768
+ avoidBlockScalars: this.avoidBlockScalars
7769
+ });
7677
7770
  mainFileContent = addTrailingNewline(content);
7678
7771
  const existingContent = await readFileContentOrNull(mainFilePath);
7679
7772
  if (existingContent !== mainFileContent) {
@@ -10412,7 +10505,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
10412
10505
  getFactory = defaultGetFactory4,
10413
10506
  dryRun = false
10414
10507
  }) {
10415
- super({ baseDir, dryRun });
10508
+ super({ baseDir, dryRun, avoidBlockScalars: toolTarget === "cursor" });
10416
10509
  const result = SkillsProcessorToolTargetSchema.safeParse(toolTarget);
10417
10510
  if (!result.success) {
10418
10511
  throw new Error(
@@ -11493,7 +11586,7 @@ var CursorSubagent = class _CursorSubagent extends ToolSubagent {
11493
11586
  ...cursorSection
11494
11587
  };
11495
11588
  const body = rulesyncSubagent.getBody();
11496
- const fileContent = stringifyFrontmatter(body, cursorFrontmatter);
11589
+ const fileContent = stringifyFrontmatter(body, cursorFrontmatter, { avoidBlockScalars: true });
11497
11590
  const paths = this.getSettablePaths({ global });
11498
11591
  return new _CursorSubagent({
11499
11592
  baseDir,
@@ -16932,13 +17025,22 @@ var import_jsonc_parser2 = require("jsonc-parser");
16932
17025
  // src/config/config.ts
16933
17026
  var import_node_path117 = require("path");
16934
17027
  var import_mini59 = require("zod/mini");
16935
- function hasControlCharacters(value) {
17028
+
17029
+ // src/utils/validation.ts
17030
+ function findControlCharacter(value) {
16936
17031
  for (let i = 0; i < value.length; i++) {
16937
17032
  const code = value.charCodeAt(i);
16938
- if (code >= 0 && code <= 31 || code === 127) return true;
17033
+ if (code >= 0 && code <= 31 || code === 127) {
17034
+ return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
17035
+ }
16939
17036
  }
16940
- return false;
17037
+ return null;
16941
17038
  }
17039
+ function hasControlCharacters(value) {
17040
+ return findControlCharacter(value) !== null;
17041
+ }
17042
+
17043
+ // src/config/config.ts
16942
17044
  var SourceEntrySchema = import_mini59.z.object({
16943
17045
  source: import_mini59.z.string().check((0, import_mini59.minLength)(1, "source must be a non-empty string")),
16944
17046
  skills: (0, import_mini59.optional)(import_mini59.z.array(import_mini59.z.string())),
@@ -18458,17 +18560,8 @@ var import_node_path122 = require("path");
18458
18560
  var import_node_util = require("util");
18459
18561
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
18460
18562
  var GIT_TIMEOUT_MS = 6e4;
18461
- var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/|[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+)/;
18563
+ var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/).+$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+$/;
18462
18564
  var INSECURE_URL_SCHEMES = /^(git:\/\/|http:\/\/)/;
18463
- function findControlCharacter(value) {
18464
- for (let i = 0; i < value.length; i++) {
18465
- const code = value.charCodeAt(i);
18466
- if (code >= 0 && code <= 31 || code === 127) {
18467
- return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
18468
- }
18469
- }
18470
- return null;
18471
- }
18472
18565
  var GitClientError = class extends Error {
18473
18566
  constructor(message, cause) {
18474
18567
  super(message, { cause });
@@ -18524,6 +18617,7 @@ async function resolveDefaultRef(url) {
18524
18617
  const ref = stdout.match(/^ref: refs\/heads\/(.+)\tHEAD$/m)?.[1];
18525
18618
  const sha = stdout.match(/^([0-9a-f]{40})\tHEAD$/m)?.[1];
18526
18619
  if (!ref || !sha) throw new GitClientError(`Could not parse default branch from: ${url}`);
18620
+ validateRef(ref);
18527
18621
  return { ref, sha };
18528
18622
  } catch (error) {
18529
18623
  if (error instanceof GitClientError) throw error;
@@ -18550,6 +18644,17 @@ async function fetchSkillFiles(params) {
18550
18644
  const { url, ref, skillsPath } = params;
18551
18645
  validateGitUrl(url);
18552
18646
  validateRef(ref);
18647
+ if (skillsPath.split(/[/\\]/).includes("..") || (0, import_node_path122.isAbsolute)(skillsPath)) {
18648
+ throw new GitClientError(
18649
+ `Invalid skillsPath "${skillsPath}": must be a relative path without ".."`
18650
+ );
18651
+ }
18652
+ const ctrl = findControlCharacter(skillsPath);
18653
+ if (ctrl) {
18654
+ throw new GitClientError(
18655
+ `skillsPath contains control character ${ctrl.hex} at position ${ctrl.position}`
18656
+ );
18657
+ }
18553
18658
  await checkGitAvailable();
18554
18659
  const tmpDir = await createTempDirectory("rulesync-git-");
18555
18660
  try {
@@ -18584,7 +18689,9 @@ async function fetchSkillFiles(params) {
18584
18689
  }
18585
18690
  }
18586
18691
  var MAX_WALK_DEPTH = 20;
18587
- async function walkDirectory(dir, baseDir, depth = 0) {
18692
+ var MAX_TOTAL_FILES = 1e4;
18693
+ var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
18694
+ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }) {
18588
18695
  if (depth > MAX_WALK_DEPTH) {
18589
18696
  throw new GitClientError(
18590
18697
  `Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
@@ -18599,7 +18706,7 @@ async function walkDirectory(dir, baseDir, depth = 0) {
18599
18706
  continue;
18600
18707
  }
18601
18708
  if (await directoryExists(fullPath)) {
18602
- results.push(...await walkDirectory(fullPath, baseDir, depth + 1));
18709
+ results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx));
18603
18710
  } else {
18604
18711
  const size = await getFileSize(fullPath);
18605
18712
  if (size > MAX_FILE_SIZE) {
@@ -18608,8 +18715,20 @@ async function walkDirectory(dir, baseDir, depth = 0) {
18608
18715
  );
18609
18716
  continue;
18610
18717
  }
18718
+ ctx.totalFiles++;
18719
+ ctx.totalSize += size;
18720
+ if (ctx.totalFiles >= MAX_TOTAL_FILES) {
18721
+ throw new GitClientError(
18722
+ `Repository exceeds max file count of ${MAX_TOTAL_FILES}. Aborting to prevent resource exhaustion.`
18723
+ );
18724
+ }
18725
+ if (ctx.totalSize >= MAX_TOTAL_SIZE) {
18726
+ throw new GitClientError(
18727
+ `Repository exceeds max total size of ${MAX_TOTAL_SIZE / 1024 / 1024}MB. Aborting to prevent resource exhaustion.`
18728
+ );
18729
+ }
18611
18730
  const content = await readFileContent(fullPath);
18612
- results.push({ relativePath: fullPath.substring(baseDir.length + 1), content, size });
18731
+ results.push({ relativePath: (0, import_node_path122.relative)(baseDir, fullPath), content, size });
18613
18732
  }
18614
18733
  }
18615
18734
  return results;
@@ -18625,7 +18744,7 @@ var LockedSkillSchema = import_mini60.z.object({
18625
18744
  });
18626
18745
  var LockedSourceSchema = import_mini60.z.object({
18627
18746
  requestedRef: (0, import_mini60.optional)(import_mini60.z.string()),
18628
- resolvedRef: import_mini60.z.string(),
18747
+ resolvedRef: import_mini60.z.string().check((0, import_mini60.refine)((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
18629
18748
  resolvedAt: (0, import_mini60.optional)(import_mini60.z.string()),
18630
18749
  skills: import_mini60.z.record(import_mini60.z.string(), LockedSkillSchema)
18631
18750
  });
@@ -18828,6 +18947,8 @@ async function resolveAndFetchSources(params) {
18828
18947
  logger.error(`Failed to fetch source "${sourceEntry.source}": ${formatError(error)}`);
18829
18948
  if (error instanceof GitHubClientError) {
18830
18949
  logGitHubAuthHints(error);
18950
+ } else if (error instanceof GitClientError) {
18951
+ logGitClientHints(error);
18831
18952
  }
18832
18953
  }
18833
18954
  }
@@ -18848,6 +18969,13 @@ async function resolveAndFetchSources(params) {
18848
18969
  }
18849
18970
  return { fetchedSkillCount: totalSkillCount, sourcesProcessed: sources.length };
18850
18971
  }
18972
+ function logGitClientHints(error) {
18973
+ if (error.message.includes("not installed")) {
18974
+ logger.info("Hint: Install git and ensure it is available on your PATH.");
18975
+ } else {
18976
+ logger.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
18977
+ }
18978
+ }
18851
18979
  async function checkLockedSkillsExist(curatedDir, skillNames) {
18852
18980
  if (skillNames.length === 0) return true;
18853
18981
  for (const name of skillNames) {
@@ -18857,9 +18985,88 @@ async function checkLockedSkillsExist(curatedDir, skillNames) {
18857
18985
  }
18858
18986
  return true;
18859
18987
  }
18988
+ async function cleanPreviousCuratedSkills(curatedDir, lockedSkillNames) {
18989
+ const resolvedCuratedDir = (0, import_node_path124.resolve)(curatedDir);
18990
+ for (const prevSkill of lockedSkillNames) {
18991
+ const prevDir = (0, import_node_path124.join)(curatedDir, prevSkill);
18992
+ if (!(0, import_node_path124.resolve)(prevDir).startsWith(resolvedCuratedDir + import_node_path124.sep)) {
18993
+ logger.warn(
18994
+ `Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
18995
+ );
18996
+ continue;
18997
+ }
18998
+ if (await directoryExists(prevDir)) {
18999
+ await removeDirectory(prevDir);
19000
+ }
19001
+ }
19002
+ }
19003
+ function shouldSkipSkill(params) {
19004
+ const { skillName, sourceKey, localSkillNames, alreadyFetchedSkillNames } = params;
19005
+ if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
19006
+ logger.warn(
19007
+ `Skipping skill with invalid name "${skillName}" from ${sourceKey}: contains path traversal characters.`
19008
+ );
19009
+ return true;
19010
+ }
19011
+ if (localSkillNames.has(skillName)) {
19012
+ logger.debug(
19013
+ `Skipping remote skill "${skillName}" from ${sourceKey}: local skill takes precedence.`
19014
+ );
19015
+ return true;
19016
+ }
19017
+ if (alreadyFetchedSkillNames.has(skillName)) {
19018
+ logger.warn(
19019
+ `Skipping duplicate skill "${skillName}" from ${sourceKey}: already fetched from another source.`
19020
+ );
19021
+ return true;
19022
+ }
19023
+ return false;
19024
+ }
19025
+ async function writeSkillAndComputeIntegrity(params) {
19026
+ const { skillName, files, curatedDir, locked, resolvedSha, sourceKey } = params;
19027
+ const written = [];
19028
+ for (const file of files) {
19029
+ checkPathTraversal({
19030
+ relativePath: file.relativePath,
19031
+ intendedRootDir: (0, import_node_path124.join)(curatedDir, skillName)
19032
+ });
19033
+ await writeFileContent((0, import_node_path124.join)(curatedDir, skillName, file.relativePath), file.content);
19034
+ written.push({ path: file.relativePath, content: file.content });
19035
+ }
19036
+ const integrity = computeSkillIntegrity(written);
19037
+ const lockedSkillEntry = locked?.skills[skillName];
19038
+ if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
19039
+ logger.warn(
19040
+ `Integrity mismatch for skill "${skillName}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
19041
+ );
19042
+ }
19043
+ return { integrity };
19044
+ }
19045
+ function buildLockUpdate(params) {
19046
+ const { lock, sourceKey, fetchedSkills, locked, requestedRef, resolvedSha } = params;
19047
+ const fetchedNames = Object.keys(fetchedSkills);
19048
+ const mergedSkills = { ...fetchedSkills };
19049
+ if (locked) {
19050
+ for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
19051
+ if (!(skillName in mergedSkills)) {
19052
+ mergedSkills[skillName] = skillEntry;
19053
+ }
19054
+ }
19055
+ }
19056
+ const updatedLock = setLockedSource(lock, sourceKey, {
19057
+ requestedRef,
19058
+ resolvedRef: resolvedSha,
19059
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19060
+ skills: mergedSkills
19061
+ });
19062
+ logger.info(
19063
+ `Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
19064
+ );
19065
+ return { updatedLock, fetchedNames };
19066
+ }
18860
19067
  async function fetchSource(params) {
18861
19068
  const { sourceEntry, client, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources } = params;
18862
- let { lock } = params;
19069
+ const { lock } = params;
18863
19070
  const parsed = parseSource(sourceEntry.source);
18864
19071
  if (parsed.provider === "gitlab") {
18865
19072
  logger.warn(`GitLab sources are not yet supported. Skipping "${sourceEntry.source}".`);
@@ -18912,37 +19119,15 @@ async function fetchSource(params) {
18912
19119
  const semaphore = new import_promise2.Semaphore(FETCH_CONCURRENCY_LIMIT);
18913
19120
  const fetchedSkills = {};
18914
19121
  if (locked) {
18915
- const resolvedCuratedDir = (0, import_node_path124.resolve)(curatedDir);
18916
- for (const prevSkill of lockedSkillNames) {
18917
- const prevDir = (0, import_node_path124.join)(curatedDir, prevSkill);
18918
- if (!(0, import_node_path124.resolve)(prevDir).startsWith(resolvedCuratedDir + import_node_path124.sep)) {
18919
- logger.warn(
18920
- `Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
18921
- );
18922
- continue;
18923
- }
18924
- if (await directoryExists(prevDir)) {
18925
- await removeDirectory(prevDir);
18926
- }
18927
- }
19122
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
18928
19123
  }
18929
19124
  for (const skillDir of filteredDirs) {
18930
- if (skillDir.name.includes("..") || skillDir.name.includes("/") || skillDir.name.includes("\\")) {
18931
- logger.warn(
18932
- `Skipping skill with invalid name "${skillDir.name}" from ${sourceKey}: contains path traversal characters.`
18933
- );
18934
- continue;
18935
- }
18936
- if (localSkillNames.has(skillDir.name)) {
18937
- logger.debug(
18938
- `Skipping remote skill "${skillDir.name}" from ${sourceKey}: local skill takes precedence.`
18939
- );
18940
- continue;
18941
- }
18942
- if (alreadyFetchedSkillNames.has(skillDir.name)) {
18943
- logger.warn(
18944
- `Skipping duplicate skill "${skillDir.name}" from ${sourceKey}: already fetched from another source.`
18945
- );
19125
+ if (shouldSkipSkill({
19126
+ skillName: skillDir.name,
19127
+ sourceKey,
19128
+ localSkillNames,
19129
+ alreadyFetchedSkillNames
19130
+ })) {
18946
19131
  continue;
18947
19132
  }
18948
19133
  const allFiles = await listDirectoryRecursive({
@@ -18965,55 +19150,39 @@ async function fetchSource(params) {
18965
19150
  const skillFiles = [];
18966
19151
  for (const file of files) {
18967
19152
  const relativeToSkill = file.path.substring(skillDir.path.length + 1);
18968
- const localFilePath = (0, import_node_path124.join)(curatedDir, skillDir.name, relativeToSkill);
18969
- checkPathTraversal({
18970
- relativePath: relativeToSkill,
18971
- intendedRootDir: (0, import_node_path124.join)(curatedDir, skillDir.name)
18972
- });
18973
19153
  const content = await withSemaphore(
18974
19154
  semaphore,
18975
19155
  () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
18976
19156
  );
18977
- await writeFileContent(localFilePath, content);
18978
- skillFiles.push({ path: relativeToSkill, content });
19157
+ skillFiles.push({ relativePath: relativeToSkill, content });
18979
19158
  }
18980
- const integrity = computeSkillIntegrity(skillFiles);
18981
- const lockedSkillEntry = locked?.skills[skillDir.name];
18982
- if (lockedSkillEntry && lockedSkillEntry.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
18983
- logger.warn(
18984
- `Integrity mismatch for skill "${skillDir.name}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
18985
- );
18986
- }
18987
- fetchedSkills[skillDir.name] = { integrity };
19159
+ fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
19160
+ skillName: skillDir.name,
19161
+ files: skillFiles,
19162
+ curatedDir,
19163
+ locked,
19164
+ resolvedSha,
19165
+ sourceKey
19166
+ });
18988
19167
  logger.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
18989
19168
  }
18990
- const fetchedNames = Object.keys(fetchedSkills);
18991
- const mergedSkills = { ...fetchedSkills };
18992
- if (locked) {
18993
- for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
18994
- if (!(skillName in mergedSkills)) {
18995
- mergedSkills[skillName] = skillEntry;
18996
- }
18997
- }
18998
- }
18999
- lock = setLockedSource(lock, sourceKey, {
19169
+ const result = buildLockUpdate({
19170
+ lock,
19171
+ sourceKey,
19172
+ fetchedSkills,
19173
+ locked,
19000
19174
  requestedRef,
19001
- resolvedRef: resolvedSha,
19002
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19003
- skills: mergedSkills
19175
+ resolvedSha
19004
19176
  });
19005
- logger.info(
19006
- `Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
19007
- );
19008
19177
  return {
19009
- skillCount: fetchedNames.length,
19010
- fetchedSkillNames: fetchedNames,
19011
- updatedLock: lock
19178
+ skillCount: result.fetchedNames.length,
19179
+ fetchedSkillNames: result.fetchedNames,
19180
+ updatedLock: result.updatedLock
19012
19181
  };
19013
19182
  }
19014
19183
  async function fetchSourceViaGit(params) {
19015
19184
  const { sourceEntry, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources, frozen } = params;
19016
- let { lock } = params;
19185
+ const { lock } = params;
19017
19186
  const url = sourceEntry.source;
19018
19187
  const locked = getLockedSource(lock, url);
19019
19188
  const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
@@ -19069,68 +19238,35 @@ async function fetchSourceViaGit(params) {
19069
19238
  const allNames = [...skillFileMap.keys()];
19070
19239
  const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
19071
19240
  if (locked) {
19072
- const base = (0, import_node_path124.resolve)(curatedDir);
19073
- for (const prev of lockedSkillNames) {
19074
- const dir = (0, import_node_path124.join)(curatedDir, prev);
19075
- if ((0, import_node_path124.resolve)(dir).startsWith(base + import_node_path124.sep) && await directoryExists(dir)) {
19076
- await removeDirectory(dir);
19077
- }
19078
- }
19241
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
19079
19242
  }
19080
19243
  const fetchedSkills = {};
19081
19244
  for (const skillName of filteredNames) {
19082
- if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
19083
- logger.warn(
19084
- `Skipping skill with invalid name "${skillName}" from ${url}: contains path traversal characters.`
19085
- );
19245
+ if (shouldSkipSkill({ skillName, sourceKey: url, localSkillNames, alreadyFetchedSkillNames })) {
19086
19246
  continue;
19087
19247
  }
19088
- if (localSkillNames.has(skillName)) {
19089
- logger.debug(
19090
- `Skipping remote skill "${skillName}" from ${url}: local skill takes precedence.`
19091
- );
19092
- continue;
19093
- }
19094
- if (alreadyFetchedSkillNames.has(skillName)) {
19095
- logger.warn(
19096
- `Skipping duplicate skill "${skillName}" from ${url}: already fetched from another source.`
19097
- );
19098
- continue;
19099
- }
19100
- const files = skillFileMap.get(skillName) ?? [];
19101
- const written = [];
19102
- for (const file of files) {
19103
- checkPathTraversal({
19104
- relativePath: file.relativePath,
19105
- intendedRootDir: (0, import_node_path124.join)(curatedDir, skillName)
19106
- });
19107
- await writeFileContent((0, import_node_path124.join)(curatedDir, skillName, file.relativePath), file.content);
19108
- written.push({ path: file.relativePath, content: file.content });
19109
- }
19110
- const integrity = computeSkillIntegrity(written);
19111
- const lockedSkillEntry = locked?.skills[skillName];
19112
- if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
19113
- logger.warn(`Integrity mismatch for skill "${skillName}" from ${url}.`);
19114
- }
19115
- fetchedSkills[skillName] = { integrity };
19116
- }
19117
- const fetchedNames = Object.keys(fetchedSkills);
19118
- const mergedSkills = { ...fetchedSkills };
19119
- if (locked) {
19120
- for (const [k, v] of Object.entries(locked.skills)) {
19121
- if (!(k in mergedSkills)) mergedSkills[k] = v;
19122
- }
19248
+ fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
19249
+ skillName,
19250
+ files: skillFileMap.get(skillName) ?? [],
19251
+ curatedDir,
19252
+ locked,
19253
+ resolvedSha,
19254
+ sourceKey: url
19255
+ });
19123
19256
  }
19124
- lock = setLockedSource(lock, url, {
19257
+ const result = buildLockUpdate({
19258
+ lock,
19259
+ sourceKey: url,
19260
+ fetchedSkills,
19261
+ locked,
19125
19262
  requestedRef,
19126
- resolvedRef: resolvedSha,
19127
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19128
- skills: mergedSkills
19263
+ resolvedSha
19129
19264
  });
19130
- logger.info(
19131
- `Fetched ${fetchedNames.length} skill(s) from ${url}: ${fetchedNames.join(", ") || "(none)"}`
19132
- );
19133
- return { skillCount: fetchedNames.length, fetchedSkillNames: fetchedNames, updatedLock: lock };
19265
+ return {
19266
+ skillCount: result.fetchedNames.length,
19267
+ fetchedSkillNames: result.fetchedNames,
19268
+ updatedLock: result.updatedLock
19269
+ };
19134
19270
  }
19135
19271
 
19136
19272
  // src/cli/commands/install.ts
@@ -20946,7 +21082,7 @@ async function updateCommand(currentVersion, options) {
20946
21082
  }
20947
21083
 
20948
21084
  // src/cli/index.ts
20949
- var getVersion = () => "7.15.2";
21085
+ var getVersion = () => "7.16.0";
20950
21086
  var main = async () => {
20951
21087
  const program = new import_commander.Command();
20952
21088
  const version = getVersion();