rulesync 7.15.1 → 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;
@@ -5516,7 +5564,10 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
5516
5564
  stripMcpServerFields(fields) {
5517
5565
  if (fields.length === 0) return this;
5518
5566
  const filteredServers = Object.fromEntries(
5519
- Object.entries(this.json.mcpServers).map(([name, config]) => [name, (0, import_object.omit)(config, fields)])
5567
+ Object.entries(this.json.mcpServers).map(([name, config]) => [
5568
+ name,
5569
+ Object.fromEntries(Object.entries(config).filter(([key]) => !fields.includes(key)))
5570
+ ])
5520
5571
  );
5521
5572
  return new _RulesyncMcp({
5522
5573
  baseDir: this.baseDir,
@@ -6022,12 +6073,26 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6022
6073
  json;
6023
6074
  constructor(params) {
6024
6075
  super(params);
6025
- 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
+ }
6026
6088
  }
6027
6089
  getJson() {
6028
6090
  return this.json;
6029
6091
  }
6030
- static getSettablePaths() {
6092
+ isDeletable() {
6093
+ return !this.global;
6094
+ }
6095
+ static getSettablePaths(_options) {
6031
6096
  return {
6032
6097
  relativeDirPath: ".cursor",
6033
6098
  relativeFilePath: "mcp.json"
@@ -6035,41 +6100,62 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6035
6100
  }
6036
6101
  static async fromFile({
6037
6102
  baseDir = process.cwd(),
6038
- validate = true
6103
+ validate = true,
6104
+ global = false
6039
6105
  }) {
6040
- const fileContent = await readFileContent(
6041
- (0, import_node_path46.join)(
6042
- baseDir,
6043
- this.getSettablePaths().relativeDirPath,
6044
- this.getSettablePaths().relativeFilePath
6045
- )
6046
- );
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 ?? {} };
6047
6119
  return new _CursorMcp({
6048
6120
  baseDir,
6049
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6050
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6051
- fileContent,
6052
- validate
6121
+ relativeDirPath: paths.relativeDirPath,
6122
+ relativeFilePath: paths.relativeFilePath,
6123
+ fileContent: JSON.stringify(newJson, null, 2),
6124
+ validate,
6125
+ global
6053
6126
  });
6054
6127
  }
6055
- static fromRulesyncMcp({
6128
+ static async fromRulesyncMcp({
6056
6129
  baseDir = process.cwd(),
6057
6130
  rulesyncMcp,
6058
- validate = true
6131
+ validate = true,
6132
+ global = false
6059
6133
  }) {
6060
- const json = rulesyncMcp.getJson();
6061
- 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 : {};
6062
6150
  const transformedServers = convertEnvToCursorFormat(mcpServers);
6063
- const cursorConfig = {
6064
- mcpServers: transformedServers
6065
- };
6066
- const fileContent = JSON.stringify(cursorConfig, null, 2);
6151
+ const cursorConfig = { ...json, mcpServers: transformedServers };
6067
6152
  return new _CursorMcp({
6068
6153
  baseDir,
6069
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6070
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6071
- fileContent,
6072
- validate
6154
+ relativeDirPath: paths.relativeDirPath,
6155
+ relativeFilePath: paths.relativeFilePath,
6156
+ fileContent: JSON.stringify(cursorConfig, null, 2),
6157
+ validate,
6158
+ global
6073
6159
  });
6074
6160
  }
6075
6161
  toRulesyncMcp() {
@@ -6093,14 +6179,16 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6093
6179
  static forDeletion({
6094
6180
  baseDir = process.cwd(),
6095
6181
  relativeDirPath,
6096
- relativeFilePath
6182
+ relativeFilePath,
6183
+ global = false
6097
6184
  }) {
6098
6185
  return new _CursorMcp({
6099
6186
  baseDir,
6100
6187
  relativeDirPath,
6101
6188
  relativeFilePath,
6102
6189
  fileContent: "{}",
6103
- validate: false
6190
+ validate: false,
6191
+ global
6104
6192
  });
6105
6193
  }
6106
6194
  };
@@ -6929,7 +7017,7 @@ var toolMcpFactories = /* @__PURE__ */ new Map([
6929
7017
  class: CursorMcp,
6930
7018
  meta: {
6931
7019
  supportsProject: true,
6932
- supportsGlobal: false,
7020
+ supportsGlobal: true,
6933
7021
  supportsEnabledTools: false,
6934
7022
  supportsDisabledTools: false
6935
7023
  }
@@ -7642,9 +7730,15 @@ var import_node_path59 = require("path");
7642
7730
  var DirFeatureProcessor = class {
7643
7731
  baseDir;
7644
7732
  dryRun;
7645
- constructor({ baseDir = process.cwd(), dryRun = false }) {
7733
+ avoidBlockScalars;
7734
+ constructor({
7735
+ baseDir = process.cwd(),
7736
+ dryRun = false,
7737
+ avoidBlockScalars = false
7738
+ }) {
7646
7739
  this.baseDir = baseDir;
7647
7740
  this.dryRun = dryRun;
7741
+ this.avoidBlockScalars = avoidBlockScalars;
7648
7742
  }
7649
7743
  /**
7650
7744
  * Return tool targets that this feature supports.
@@ -7670,7 +7764,9 @@ var DirFeatureProcessor = class {
7670
7764
  let mainFileContent;
7671
7765
  if (mainFile) {
7672
7766
  const mainFilePath = (0, import_node_path59.join)(dirPath, mainFile.name);
7673
- const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter);
7767
+ const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter, {
7768
+ avoidBlockScalars: this.avoidBlockScalars
7769
+ });
7674
7770
  mainFileContent = addTrailingNewline(content);
7675
7771
  const existingContent = await readFileContentOrNull(mainFilePath);
7676
7772
  if (existingContent !== mainFileContent) {
@@ -10409,7 +10505,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
10409
10505
  getFactory = defaultGetFactory4,
10410
10506
  dryRun = false
10411
10507
  }) {
10412
- super({ baseDir, dryRun });
10508
+ super({ baseDir, dryRun, avoidBlockScalars: toolTarget === "cursor" });
10413
10509
  const result = SkillsProcessorToolTargetSchema.safeParse(toolTarget);
10414
10510
  if (!result.success) {
10415
10511
  throw new Error(
@@ -11490,7 +11586,7 @@ var CursorSubagent = class _CursorSubagent extends ToolSubagent {
11490
11586
  ...cursorSection
11491
11587
  };
11492
11588
  const body = rulesyncSubagent.getBody();
11493
- const fileContent = stringifyFrontmatter(body, cursorFrontmatter);
11589
+ const fileContent = stringifyFrontmatter(body, cursorFrontmatter, { avoidBlockScalars: true });
11494
11590
  const paths = this.getSettablePaths({ global });
11495
11591
  return new _CursorSubagent({
11496
11592
  baseDir,
@@ -16929,13 +17025,22 @@ var import_jsonc_parser2 = require("jsonc-parser");
16929
17025
  // src/config/config.ts
16930
17026
  var import_node_path117 = require("path");
16931
17027
  var import_mini59 = require("zod/mini");
16932
- function hasControlCharacters(value) {
17028
+
17029
+ // src/utils/validation.ts
17030
+ function findControlCharacter(value) {
16933
17031
  for (let i = 0; i < value.length; i++) {
16934
17032
  const code = value.charCodeAt(i);
16935
- 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
+ }
16936
17036
  }
16937
- return false;
17037
+ return null;
16938
17038
  }
17039
+ function hasControlCharacters(value) {
17040
+ return findControlCharacter(value) !== null;
17041
+ }
17042
+
17043
+ // src/config/config.ts
16939
17044
  var SourceEntrySchema = import_mini59.z.object({
16940
17045
  source: import_mini59.z.string().check((0, import_mini59.minLength)(1, "source must be a non-empty string")),
16941
17046
  skills: (0, import_mini59.optional)(import_mini59.z.array(import_mini59.z.string())),
@@ -18455,17 +18560,8 @@ var import_node_path122 = require("path");
18455
18560
  var import_node_util = require("util");
18456
18561
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
18457
18562
  var GIT_TIMEOUT_MS = 6e4;
18458
- 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_.+/~-]+$/;
18459
18564
  var INSECURE_URL_SCHEMES = /^(git:\/\/|http:\/\/)/;
18460
- function findControlCharacter(value) {
18461
- for (let i = 0; i < value.length; i++) {
18462
- const code = value.charCodeAt(i);
18463
- if (code >= 0 && code <= 31 || code === 127) {
18464
- return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
18465
- }
18466
- }
18467
- return null;
18468
- }
18469
18565
  var GitClientError = class extends Error {
18470
18566
  constructor(message, cause) {
18471
18567
  super(message, { cause });
@@ -18521,6 +18617,7 @@ async function resolveDefaultRef(url) {
18521
18617
  const ref = stdout.match(/^ref: refs\/heads\/(.+)\tHEAD$/m)?.[1];
18522
18618
  const sha = stdout.match(/^([0-9a-f]{40})\tHEAD$/m)?.[1];
18523
18619
  if (!ref || !sha) throw new GitClientError(`Could not parse default branch from: ${url}`);
18620
+ validateRef(ref);
18524
18621
  return { ref, sha };
18525
18622
  } catch (error) {
18526
18623
  if (error instanceof GitClientError) throw error;
@@ -18547,6 +18644,17 @@ async function fetchSkillFiles(params) {
18547
18644
  const { url, ref, skillsPath } = params;
18548
18645
  validateGitUrl(url);
18549
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
+ }
18550
18658
  await checkGitAvailable();
18551
18659
  const tmpDir = await createTempDirectory("rulesync-git-");
18552
18660
  try {
@@ -18581,7 +18689,9 @@ async function fetchSkillFiles(params) {
18581
18689
  }
18582
18690
  }
18583
18691
  var MAX_WALK_DEPTH = 20;
18584
- 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 }) {
18585
18695
  if (depth > MAX_WALK_DEPTH) {
18586
18696
  throw new GitClientError(
18587
18697
  `Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
@@ -18596,7 +18706,7 @@ async function walkDirectory(dir, baseDir, depth = 0) {
18596
18706
  continue;
18597
18707
  }
18598
18708
  if (await directoryExists(fullPath)) {
18599
- results.push(...await walkDirectory(fullPath, baseDir, depth + 1));
18709
+ results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx));
18600
18710
  } else {
18601
18711
  const size = await getFileSize(fullPath);
18602
18712
  if (size > MAX_FILE_SIZE) {
@@ -18605,8 +18715,20 @@ async function walkDirectory(dir, baseDir, depth = 0) {
18605
18715
  );
18606
18716
  continue;
18607
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
+ }
18608
18730
  const content = await readFileContent(fullPath);
18609
- results.push({ relativePath: fullPath.substring(baseDir.length + 1), content, size });
18731
+ results.push({ relativePath: (0, import_node_path122.relative)(baseDir, fullPath), content, size });
18610
18732
  }
18611
18733
  }
18612
18734
  return results;
@@ -18622,7 +18744,7 @@ var LockedSkillSchema = import_mini60.z.object({
18622
18744
  });
18623
18745
  var LockedSourceSchema = import_mini60.z.object({
18624
18746
  requestedRef: (0, import_mini60.optional)(import_mini60.z.string()),
18625
- 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")),
18626
18748
  resolvedAt: (0, import_mini60.optional)(import_mini60.z.string()),
18627
18749
  skills: import_mini60.z.record(import_mini60.z.string(), LockedSkillSchema)
18628
18750
  });
@@ -18825,6 +18947,8 @@ async function resolveAndFetchSources(params) {
18825
18947
  logger.error(`Failed to fetch source "${sourceEntry.source}": ${formatError(error)}`);
18826
18948
  if (error instanceof GitHubClientError) {
18827
18949
  logGitHubAuthHints(error);
18950
+ } else if (error instanceof GitClientError) {
18951
+ logGitClientHints(error);
18828
18952
  }
18829
18953
  }
18830
18954
  }
@@ -18845,6 +18969,13 @@ async function resolveAndFetchSources(params) {
18845
18969
  }
18846
18970
  return { fetchedSkillCount: totalSkillCount, sourcesProcessed: sources.length };
18847
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
+ }
18848
18979
  async function checkLockedSkillsExist(curatedDir, skillNames) {
18849
18980
  if (skillNames.length === 0) return true;
18850
18981
  for (const name of skillNames) {
@@ -18854,9 +18985,88 @@ async function checkLockedSkillsExist(curatedDir, skillNames) {
18854
18985
  }
18855
18986
  return true;
18856
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
+ }
18857
19067
  async function fetchSource(params) {
18858
19068
  const { sourceEntry, client, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources } = params;
18859
- let { lock } = params;
19069
+ const { lock } = params;
18860
19070
  const parsed = parseSource(sourceEntry.source);
18861
19071
  if (parsed.provider === "gitlab") {
18862
19072
  logger.warn(`GitLab sources are not yet supported. Skipping "${sourceEntry.source}".`);
@@ -18909,37 +19119,15 @@ async function fetchSource(params) {
18909
19119
  const semaphore = new import_promise2.Semaphore(FETCH_CONCURRENCY_LIMIT);
18910
19120
  const fetchedSkills = {};
18911
19121
  if (locked) {
18912
- const resolvedCuratedDir = (0, import_node_path124.resolve)(curatedDir);
18913
- for (const prevSkill of lockedSkillNames) {
18914
- const prevDir = (0, import_node_path124.join)(curatedDir, prevSkill);
18915
- if (!(0, import_node_path124.resolve)(prevDir).startsWith(resolvedCuratedDir + import_node_path124.sep)) {
18916
- logger.warn(
18917
- `Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
18918
- );
18919
- continue;
18920
- }
18921
- if (await directoryExists(prevDir)) {
18922
- await removeDirectory(prevDir);
18923
- }
18924
- }
19122
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
18925
19123
  }
18926
19124
  for (const skillDir of filteredDirs) {
18927
- if (skillDir.name.includes("..") || skillDir.name.includes("/") || skillDir.name.includes("\\")) {
18928
- logger.warn(
18929
- `Skipping skill with invalid name "${skillDir.name}" from ${sourceKey}: contains path traversal characters.`
18930
- );
18931
- continue;
18932
- }
18933
- if (localSkillNames.has(skillDir.name)) {
18934
- logger.debug(
18935
- `Skipping remote skill "${skillDir.name}" from ${sourceKey}: local skill takes precedence.`
18936
- );
18937
- continue;
18938
- }
18939
- if (alreadyFetchedSkillNames.has(skillDir.name)) {
18940
- logger.warn(
18941
- `Skipping duplicate skill "${skillDir.name}" from ${sourceKey}: already fetched from another source.`
18942
- );
19125
+ if (shouldSkipSkill({
19126
+ skillName: skillDir.name,
19127
+ sourceKey,
19128
+ localSkillNames,
19129
+ alreadyFetchedSkillNames
19130
+ })) {
18943
19131
  continue;
18944
19132
  }
18945
19133
  const allFiles = await listDirectoryRecursive({
@@ -18962,55 +19150,39 @@ async function fetchSource(params) {
18962
19150
  const skillFiles = [];
18963
19151
  for (const file of files) {
18964
19152
  const relativeToSkill = file.path.substring(skillDir.path.length + 1);
18965
- const localFilePath = (0, import_node_path124.join)(curatedDir, skillDir.name, relativeToSkill);
18966
- checkPathTraversal({
18967
- relativePath: relativeToSkill,
18968
- intendedRootDir: (0, import_node_path124.join)(curatedDir, skillDir.name)
18969
- });
18970
19153
  const content = await withSemaphore(
18971
19154
  semaphore,
18972
19155
  () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
18973
19156
  );
18974
- await writeFileContent(localFilePath, content);
18975
- skillFiles.push({ path: relativeToSkill, content });
19157
+ skillFiles.push({ relativePath: relativeToSkill, content });
18976
19158
  }
18977
- const integrity = computeSkillIntegrity(skillFiles);
18978
- const lockedSkillEntry = locked?.skills[skillDir.name];
18979
- if (lockedSkillEntry && lockedSkillEntry.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
18980
- logger.warn(
18981
- `Integrity mismatch for skill "${skillDir.name}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
18982
- );
18983
- }
18984
- 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
+ });
18985
19167
  logger.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
18986
19168
  }
18987
- const fetchedNames = Object.keys(fetchedSkills);
18988
- const mergedSkills = { ...fetchedSkills };
18989
- if (locked) {
18990
- for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
18991
- if (!(skillName in mergedSkills)) {
18992
- mergedSkills[skillName] = skillEntry;
18993
- }
18994
- }
18995
- }
18996
- lock = setLockedSource(lock, sourceKey, {
19169
+ const result = buildLockUpdate({
19170
+ lock,
19171
+ sourceKey,
19172
+ fetchedSkills,
19173
+ locked,
18997
19174
  requestedRef,
18998
- resolvedRef: resolvedSha,
18999
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19000
- skills: mergedSkills
19175
+ resolvedSha
19001
19176
  });
19002
- logger.info(
19003
- `Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
19004
- );
19005
19177
  return {
19006
- skillCount: fetchedNames.length,
19007
- fetchedSkillNames: fetchedNames,
19008
- updatedLock: lock
19178
+ skillCount: result.fetchedNames.length,
19179
+ fetchedSkillNames: result.fetchedNames,
19180
+ updatedLock: result.updatedLock
19009
19181
  };
19010
19182
  }
19011
19183
  async function fetchSourceViaGit(params) {
19012
19184
  const { sourceEntry, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources, frozen } = params;
19013
- let { lock } = params;
19185
+ const { lock } = params;
19014
19186
  const url = sourceEntry.source;
19015
19187
  const locked = getLockedSource(lock, url);
19016
19188
  const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
@@ -19066,68 +19238,35 @@ async function fetchSourceViaGit(params) {
19066
19238
  const allNames = [...skillFileMap.keys()];
19067
19239
  const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
19068
19240
  if (locked) {
19069
- const base = (0, import_node_path124.resolve)(curatedDir);
19070
- for (const prev of lockedSkillNames) {
19071
- const dir = (0, import_node_path124.join)(curatedDir, prev);
19072
- if ((0, import_node_path124.resolve)(dir).startsWith(base + import_node_path124.sep) && await directoryExists(dir)) {
19073
- await removeDirectory(dir);
19074
- }
19075
- }
19241
+ await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
19076
19242
  }
19077
19243
  const fetchedSkills = {};
19078
19244
  for (const skillName of filteredNames) {
19079
- if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
19080
- logger.warn(
19081
- `Skipping skill with invalid name "${skillName}" from ${url}: contains path traversal characters.`
19082
- );
19245
+ if (shouldSkipSkill({ skillName, sourceKey: url, localSkillNames, alreadyFetchedSkillNames })) {
19083
19246
  continue;
19084
19247
  }
19085
- if (localSkillNames.has(skillName)) {
19086
- logger.debug(
19087
- `Skipping remote skill "${skillName}" from ${url}: local skill takes precedence.`
19088
- );
19089
- continue;
19090
- }
19091
- if (alreadyFetchedSkillNames.has(skillName)) {
19092
- logger.warn(
19093
- `Skipping duplicate skill "${skillName}" from ${url}: already fetched from another source.`
19094
- );
19095
- continue;
19096
- }
19097
- const files = skillFileMap.get(skillName) ?? [];
19098
- const written = [];
19099
- for (const file of files) {
19100
- checkPathTraversal({
19101
- relativePath: file.relativePath,
19102
- intendedRootDir: (0, import_node_path124.join)(curatedDir, skillName)
19103
- });
19104
- await writeFileContent((0, import_node_path124.join)(curatedDir, skillName, file.relativePath), file.content);
19105
- written.push({ path: file.relativePath, content: file.content });
19106
- }
19107
- const integrity = computeSkillIntegrity(written);
19108
- const lockedSkillEntry = locked?.skills[skillName];
19109
- if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
19110
- logger.warn(`Integrity mismatch for skill "${skillName}" from ${url}.`);
19111
- }
19112
- fetchedSkills[skillName] = { integrity };
19113
- }
19114
- const fetchedNames = Object.keys(fetchedSkills);
19115
- const mergedSkills = { ...fetchedSkills };
19116
- if (locked) {
19117
- for (const [k, v] of Object.entries(locked.skills)) {
19118
- if (!(k in mergedSkills)) mergedSkills[k] = v;
19119
- }
19248
+ fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
19249
+ skillName,
19250
+ files: skillFileMap.get(skillName) ?? [],
19251
+ curatedDir,
19252
+ locked,
19253
+ resolvedSha,
19254
+ sourceKey: url
19255
+ });
19120
19256
  }
19121
- lock = setLockedSource(lock, url, {
19257
+ const result = buildLockUpdate({
19258
+ lock,
19259
+ sourceKey: url,
19260
+ fetchedSkills,
19261
+ locked,
19122
19262
  requestedRef,
19123
- resolvedRef: resolvedSha,
19124
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19125
- skills: mergedSkills
19263
+ resolvedSha
19126
19264
  });
19127
- logger.info(
19128
- `Fetched ${fetchedNames.length} skill(s) from ${url}: ${fetchedNames.join(", ") || "(none)"}`
19129
- );
19130
- return { skillCount: fetchedNames.length, fetchedSkillNames: fetchedNames, updatedLock: lock };
19265
+ return {
19266
+ skillCount: result.fetchedNames.length,
19267
+ fetchedSkillNames: result.fetchedNames,
19268
+ updatedLock: result.updatedLock
19269
+ };
19131
19270
  }
19132
19271
 
19133
19272
  // src/cli/commands/install.ts
@@ -20943,7 +21082,7 @@ async function updateCommand(currentVersion, options) {
20943
21082
  }
20944
21083
 
20945
21084
  // src/cli/index.ts
20946
- var getVersion = () => "7.15.1";
21085
+ var getVersion = () => "7.16.0";
20947
21086
  var main = async () => {
20948
21087
  const program = new import_commander.Command();
20949
21088
  const version = getVersion();