rulesync 12.0.0 → 14.0.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_import = require("../import-C0N0Lo_F.cjs");
2
+ const require_import = require("../import-Dh9PCQKq.cjs");
3
3
  let commander = require("commander");
4
4
  let zod_mini = require("zod/mini");
5
5
  let node_path = require("node:path");
@@ -8,12 +8,13 @@ let node_os = require("node:os");
8
8
  node_os = require_import.__toESM(node_os, 1);
9
9
  let node_util = require("node:util");
10
10
  let js_yaml = require("js-yaml");
11
+ let node_crypto = require("node:crypto");
12
+ node_crypto = require_import.__toESM(node_crypto, 1);
11
13
  let es_toolkit_promise = require("es-toolkit/promise");
12
14
  let _octokit_request_error = require("@octokit/request-error");
13
15
  let _octokit_rest = require("@octokit/rest");
14
- let node_crypto = require("node:crypto");
15
- node_crypto = require_import.__toESM(node_crypto, 1);
16
16
  let node_child_process = require("node:child_process");
17
+ let node_zlib = require("node:zlib");
17
18
  let fastmcp = require("fastmcp");
18
19
  let node_fs = require("node:fs");
19
20
  node_fs = require_import.__toESM(node_fs, 1);
@@ -36,7 +37,7 @@ const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim())
36
37
  * Calculate the total count from a result object
37
38
  */
38
39
  function calculateTotalCount(result) {
39
- return result.rulesCount + result.ignoreCount + result.mcpCount + result.commandsCount + result.subagentsCount + result.skillsCount + result.hooksCount + result.permissionsCount;
40
+ return result.rulesCount + result.ignoreCount + result.mcpCount + result.commandsCount + result.subagentsCount + result.skillsCount + result.hooksCount + result.permissionsCount + result.checksCount;
40
41
  }
41
42
  //#endregion
42
43
  //#region src/cli/commands/convert.ts
@@ -82,7 +83,8 @@ async function convertCommand(logger, options) {
82
83
  subagents: { count: result.subagentsCount },
83
84
  skills: { count: result.skillsCount },
84
85
  hooks: { count: result.hooksCount },
85
- permissions: { count: result.permissionsCount }
86
+ permissions: { count: result.permissionsCount },
87
+ checks: { count: result.checksCount }
86
88
  });
87
89
  logger.captureData("totalFiles", totalConverted);
88
90
  }
@@ -95,6 +97,7 @@ async function convertCommand(logger, options) {
95
97
  if (result.skillsCount > 0) parts.push(`${result.skillsCount} skills`);
96
98
  if (result.hooksCount > 0) parts.push(`${result.hooksCount} hooks`);
97
99
  if (result.permissionsCount > 0) parts.push(`${result.permissionsCount} permissions`);
100
+ if (result.checksCount > 0) parts.push(`${result.checksCount} checks`);
98
101
  const summary = `${modePrefix}${isPreview ? "Would convert" : "Converted"} ${totalConverted} file(s) total from ${fromTool} to ${toTools.join(", ")} (${parts.join(" + ")})`;
99
102
  if (isPreview) logger.info(summary);
100
103
  else logger.success(summary);
@@ -541,6 +544,7 @@ const FEATURE_PATHS = {
541
544
  commands: ["commands"],
542
545
  subagents: ["subagents"],
543
546
  skills: ["skills"],
547
+ checks: ["checks"],
544
548
  ignore: [require_import.RULESYNC_AIIGNORE_FILE_NAME],
545
549
  mcp: [require_import.RULESYNC_MCP_FILE_NAME, require_import.RULESYNC_MCP_JSONC_FILE_NAME],
546
550
  hooks: [require_import.RULESYNC_HOOKS_FILE_NAME, require_import.RULESYNC_HOOKS_JSONC_FILE_NAME],
@@ -626,6 +630,16 @@ async function convertFetchedFilesToRulesync(params) {
626
630
  logger
627
631
  })
628
632
  },
633
+ {
634
+ feature: "checks",
635
+ getTargets: () => require_import.ChecksProcessor.getToolTargets({ global: false }),
636
+ createProcessor: () => new require_import.ChecksProcessor({
637
+ outputRoot: tempDir,
638
+ toolTarget: target,
639
+ global: false,
640
+ logger
641
+ })
642
+ },
629
643
  {
630
644
  feature: "ignore",
631
645
  getTargets: () => require_import.IgnoreProcessor.getToolTargets(),
@@ -949,6 +963,10 @@ function getToolPathMapping(target) {
949
963
  const factory = require_import.SkillsProcessor.getFactory(target);
950
964
  if (factory) mapping.skills = factory.class.getSettablePaths({ global: false }).relativeDirPath;
951
965
  }
966
+ if (require_import.ChecksProcessor.getToolTargets({ global: false }).includes(target)) {
967
+ const factory = require_import.ChecksProcessor.getFactory(target);
968
+ if (factory) mapping.checks = factory.class.getSettablePaths({ global: false }).relativeDirPath;
969
+ }
952
970
  return mapping;
953
971
  }
954
972
  /**
@@ -972,6 +990,10 @@ function mapToToolPath(relativePath, toolPaths) {
972
990
  const restPath = relativePath.substring(7);
973
991
  if (toolPaths.skills) return (0, node_path.join)(toolPaths.skills, restPath);
974
992
  }
993
+ if (relativePath.startsWith("checks/")) {
994
+ const restPath = relativePath.substring(7);
995
+ if (toolPaths.checks) return (0, node_path.join)(toolPaths.checks, restPath);
996
+ }
975
997
  return relativePath;
976
998
  }
977
999
  /**
@@ -1047,6 +1069,7 @@ const FEATURE_DEBUG_MESSAGES = {
1047
1069
  subagents: "Generating subagent files...",
1048
1070
  skills: "Generating skill files...",
1049
1071
  hooks: "Generating hooks...",
1072
+ checks: "Generating check files...",
1050
1073
  rules: "Generating rule files..."
1051
1074
  };
1052
1075
  const FEATURE_DEBUG_ORDER = [
@@ -1056,6 +1079,7 @@ const FEATURE_DEBUG_ORDER = [
1056
1079
  "subagents",
1057
1080
  "skills",
1058
1081
  "hooks",
1082
+ "checks",
1059
1083
  "rules"
1060
1084
  ];
1061
1085
  function logFeatureDebugMessages(logger, features) {
@@ -1099,6 +1123,10 @@ function buildSummaryParts(result) {
1099
1123
  {
1100
1124
  count: result.permissionsCount,
1101
1125
  label: "permissions"
1126
+ },
1127
+ {
1128
+ count: result.checksCount,
1129
+ label: "checks"
1102
1130
  }
1103
1131
  ];
1104
1132
  const parts = [];
@@ -1149,6 +1177,10 @@ async function generateCommand(logger, options) {
1149
1177
  count: result.permissionsCount,
1150
1178
  paths: result.permissionsPaths
1151
1179
  },
1180
+ checks: {
1181
+ count: result.checksCount,
1182
+ paths: result.checksPaths
1183
+ },
1152
1184
  rules: {
1153
1185
  count: result.rulesCount,
1154
1186
  paths: result.rulesPaths
@@ -1162,7 +1194,8 @@ async function generateCommand(logger, options) {
1162
1194
  subagents: (count) => `${count === 1 ? "subagent" : "subagents"}`,
1163
1195
  skills: (count) => `${count === 1 ? "skill" : "skills"}`,
1164
1196
  hooks: (count) => `${count === 1 ? "hooks file" : "hooks files"}`,
1165
- permissions: (count) => `${count === 1 ? "permissions file" : "permissions files"}`
1197
+ permissions: (count) => `${count === 1 ? "permissions file" : "permissions files"}`,
1198
+ checks: (count) => `${count === 1 ? "check" : "checks"}`
1166
1199
  };
1167
1200
  for (const [feature, data] of Object.entries(featureResults)) logFeatureResult(logger, {
1168
1201
  count: data.count,
@@ -1210,6 +1243,7 @@ const DERIVED_PATHS_NOT_GITIGNORED = /* @__PURE__ */ new Set([
1210
1243
  "**/.grok/config.toml",
1211
1244
  "**/.vibe/config.toml",
1212
1245
  "**/reasonix.toml",
1246
+ "**/.vscode/settings.json",
1213
1247
  "**/.zed/settings.json",
1214
1248
  "**/kilo.json",
1215
1249
  "**/kilo.jsonc",
@@ -1263,13 +1297,16 @@ const deriveRulesEntries = () => {
1263
1297
  for (const root of [paths.root, ...paths.alternativeRoots ?? []]) if (root) pushEntry(entries, target, "rules", fileToGlob(root.relativeDirPath, root.relativeFilePath));
1264
1298
  const nonRootDir = paths.nonRoot?.relativeDirPath;
1265
1299
  if (nonRootDir && nonRootDir !== ".") pushEntry(entries, target, "rules", dirToGlob(nonRootDir));
1300
+ const classWithExtraFiles = factory.class;
1301
+ if (classWithExtraFiles.getExtraFixedFiles) for (const file of classWithExtraFiles.getExtraFixedFiles({ global: false })) pushEntry(entries, target, "rules", fileToGlob(file.relativeDirPath, file.relativeFilePath));
1266
1302
  }
1267
1303
  return entries;
1268
1304
  };
1269
1305
  const DIR_FEATURES = /* @__PURE__ */ new Set([
1270
1306
  "commands",
1271
1307
  "skills",
1272
- "subagents"
1308
+ "subagents",
1309
+ "checks"
1273
1310
  ]);
1274
1311
  const FILE_FEATURES = /* @__PURE__ */ new Set([
1275
1312
  "mcp",
@@ -1292,7 +1329,8 @@ const DERIVED_FEATURES = [
1292
1329
  "mcp",
1293
1330
  "hooks",
1294
1331
  "permissions",
1295
- "ignore"
1332
+ "ignore",
1333
+ "checks"
1296
1334
  ];
1297
1335
  const deriveAllGitignoreEntriesUnfiltered = () => DERIVED_FEATURES.flatMap((feature) => deriveFeatureGitignoreEntries(feature));
1298
1336
  const deriveAllGitignoreEntries = () => deriveAllGitignoreEntriesUnfiltered().filter((tag) => !DERIVED_PATHS_NOT_GITIGNORED.has(tag.entry));
@@ -1383,6 +1421,16 @@ const GITIGNORE_ENTRY_REGISTRY = [
1383
1421
  feature: "rules",
1384
1422
  entry: "**/.augment-guidelines"
1385
1423
  },
1424
+ {
1425
+ target: "devin",
1426
+ feature: "commands",
1427
+ entry: "**/.devin/workflows/"
1428
+ },
1429
+ {
1430
+ target: "junie",
1431
+ feature: "rules",
1432
+ entry: "**/.junie/memories/"
1433
+ },
1386
1434
  {
1387
1435
  target: "rovodev",
1388
1436
  feature: "skills",
@@ -1431,7 +1479,7 @@ const GITIGNORE_ENTRY_REGISTRY = [
1431
1479
  {
1432
1480
  target: "codexcli",
1433
1481
  feature: "permissions",
1434
- entry: `**/${require_import.CODEXCLI_DIR}/rules/`
1482
+ entry: `**/${require_import.CODEXCLI_DIR}/rules/${require_import.CODEXCLI_BASH_RULES_FILE_NAME}`
1435
1483
  }
1436
1484
  ],
1437
1485
  ...deriveAllGitignoreEntries(),
@@ -1740,7 +1788,8 @@ async function importCommand(logger, options) {
1740
1788
  subagents: { count: result.subagentsCount },
1741
1789
  skills: { count: result.skillsCount },
1742
1790
  hooks: { count: result.hooksCount },
1743
- permissions: { count: result.permissionsCount }
1791
+ permissions: { count: result.permissionsCount },
1792
+ checks: { count: result.checksCount }
1744
1793
  });
1745
1794
  logger.captureData("totalFiles", totalImported);
1746
1795
  }
@@ -1753,6 +1802,7 @@ async function importCommand(logger, options) {
1753
1802
  if (result.skillsCount > 0) parts.push(`${result.skillsCount} skills`);
1754
1803
  if (result.hooksCount > 0) parts.push(`${result.hooksCount} hooks`);
1755
1804
  if (result.permissionsCount > 0) parts.push(`${result.permissionsCount} permissions`);
1805
+ if (result.checksCount > 0) parts.push(`${result.checksCount} checks`);
1756
1806
  logger.success(`Imported ${totalImported} file(s) total (${parts.join(" + ")})`);
1757
1807
  }
1758
1808
  //#endregion
@@ -3500,6 +3550,516 @@ async function walkDirectory(dir, outputRoot, depth = 0, ctx = {
3500
3550
  }
3501
3551
  return results;
3502
3552
  }
3553
+ const DEFAULT_NPM_TOKEN_ENV = "NPM_TOKEN";
3554
+ /** Abbreviated packument media type (install metadata only). */
3555
+ const PACKUMENT_ACCEPT_HEADER = "application/vnd.npm.install-v1+json";
3556
+ /** Timeout for registry HTTP requests (60 seconds). */
3557
+ const NPM_FETCH_TIMEOUT_MS = 6e4;
3558
+ /** Maximum accepted tarball size (compressed), aligned with the extraction cap. */
3559
+ const MAX_TARBALL_SIZE = 100 * 1024 * 1024;
3560
+ /**
3561
+ * npm package name rules (scoped or unscoped, URL-safe characters only).
3562
+ * This also guards against URL path injection into registry requests.
3563
+ */
3564
+ const NPM_PACKAGE_NAME_REGEX = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i;
3565
+ const MAX_NPM_PACKAGE_NAME_LENGTH = 214;
3566
+ const INTEGRITY_ALGORITHM_PREFERENCE = [
3567
+ "sha512",
3568
+ "sha384",
3569
+ "sha256",
3570
+ "sha1"
3571
+ ];
3572
+ var NpmClientError = class extends Error {
3573
+ statusCode;
3574
+ constructor(message, options) {
3575
+ super(message, { cause: options?.cause });
3576
+ this.name = "NpmClientError";
3577
+ this.statusCode = options?.statusCode;
3578
+ }
3579
+ };
3580
+ function validateNpmPackageName(name) {
3581
+ if (name.length > MAX_NPM_PACKAGE_NAME_LENGTH || !NPM_PACKAGE_NAME_REGEX.test(name)) throw new NpmClientError(`Invalid npm package name: "${name}". Expected "name" or "@scope/name".`);
3582
+ }
3583
+ function validateNpmRegistryUrl(url, options) {
3584
+ const ctrl = require_import.findControlCharacter(url);
3585
+ if (ctrl) throw new NpmClientError(`Registry URL contains control character ${ctrl.hex} at position ${ctrl.position}`);
3586
+ if (!url.startsWith("https://") && !url.startsWith("http://")) throw new NpmClientError(`Unsupported registry URL: "${url}". Use https:// (or http://).`);
3587
+ if (url.startsWith("http://")) options?.logger?.warn(`Registry URL "${url}" uses an unencrypted protocol. Consider using https:// instead.`);
3588
+ }
3589
+ /**
3590
+ * Resolve the registry token from the environment. When `tokenEnv` is set it
3591
+ * must name an existing environment variable; otherwise `NPM_TOKEN` is used
3592
+ * when present. The token value itself is never logged.
3593
+ */
3594
+ function resolveNpmToken(params) {
3595
+ const { tokenEnv } = params;
3596
+ if (tokenEnv !== void 0) {
3597
+ const value = process.env[tokenEnv];
3598
+ if (value === void 0 || value === "") throw new NpmClientError(`Environment variable "${tokenEnv}" (from tokenEnv) is not set. Export it or remove the tokenEnv field.`);
3599
+ return value;
3600
+ }
3601
+ const fallback = process.env[DEFAULT_NPM_TOKEN_ENV];
3602
+ return fallback === void 0 || fallback === "" ? void 0 : fallback;
3603
+ }
3604
+ /** Build the packument URL for a (possibly scoped) package on a registry. */
3605
+ function buildPackumentUrl(params) {
3606
+ const { registryUrl, packageName } = params;
3607
+ validateNpmPackageName(packageName);
3608
+ const base = registryUrl.endsWith("/") ? registryUrl : `${registryUrl}/`;
3609
+ const encodedName = packageName.replaceAll("/", "%2F");
3610
+ return new URL(encodedName, base).toString();
3611
+ }
3612
+ async function fetchWithTimeout(url, headers) {
3613
+ try {
3614
+ return await fetch(url, {
3615
+ headers,
3616
+ redirect: "follow",
3617
+ signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS)
3618
+ });
3619
+ } catch (error) {
3620
+ throw new NpmClientError(`Network error while requesting ${url}`, { cause: error });
3621
+ }
3622
+ }
3623
+ /**
3624
+ * Fetch the (abbreviated) packument for a package from a registry.
3625
+ */
3626
+ async function fetchPackument(params) {
3627
+ const { registryUrl, packageName, token } = params;
3628
+ validateNpmPackageName(packageName);
3629
+ const url = buildPackumentUrl({
3630
+ registryUrl,
3631
+ packageName
3632
+ });
3633
+ const headers = { Accept: PACKUMENT_ACCEPT_HEADER };
3634
+ if (token) headers.Authorization = `Bearer ${token}`;
3635
+ const response = await fetchWithTimeout(url, headers);
3636
+ if (!response.ok) throw new NpmClientError(`Failed to fetch package metadata for "${packageName}" from ${registryUrl}: HTTP ${response.status}`, { statusCode: response.status });
3637
+ try {
3638
+ return await response.json();
3639
+ } catch (error) {
3640
+ throw new NpmClientError(`Failed to parse package metadata for "${packageName}" from ${registryUrl}`, { cause: error });
3641
+ }
3642
+ }
3643
+ /**
3644
+ * Resolve a requested version or dist-tag against a packument. Only exact
3645
+ * versions and dist-tags are supported — semver ranges are intentionally out
3646
+ * of scope (no semver dependency).
3647
+ */
3648
+ function resolvePackumentVersion(params) {
3649
+ const { packument, packageName, requested } = params;
3650
+ const versions = packument.versions ?? {};
3651
+ if (Object.prototype.hasOwnProperty.call(versions, requested)) return requested;
3652
+ const distTags = packument["dist-tags"] ?? {};
3653
+ const tagged = Object.prototype.hasOwnProperty.call(distTags, requested) ? distTags[requested] : void 0;
3654
+ if (tagged !== void 0 && Object.prototype.hasOwnProperty.call(versions, tagged)) return tagged;
3655
+ throw new NpmClientError(`Could not resolve "${packageName}@${requested}": not an exact published version or dist-tag. Note: semver ranges are not supported by the npm transport.`);
3656
+ }
3657
+ /** Get the dist metadata (tarball URL, integrity) for a resolved version. */
3658
+ function getPackumentVersionDist(params) {
3659
+ const { packument, packageName, version } = params;
3660
+ const versions = packument.versions ?? {};
3661
+ const dist = (Object.prototype.hasOwnProperty.call(versions, version) ? versions[version] : void 0)?.dist;
3662
+ if (!dist?.tarball) throw new NpmClientError(`Registry metadata for "${packageName}@${version}" is missing the dist.tarball URL.`);
3663
+ return dist;
3664
+ }
3665
+ /**
3666
+ * Download a package tarball. The Authorization header is only attached when
3667
+ * the tarball is hosted on the same origin (scheme + host) as the registry,
3668
+ * so the token never leaks to third-party CDNs or plaintext downgrades.
3669
+ */
3670
+ async function fetchTarball(params) {
3671
+ const { tarballUrl, registryUrl, token } = params;
3672
+ const maxSize = params.maxSize ?? MAX_TARBALL_SIZE;
3673
+ if (!tarballUrl.startsWith("https://") && !tarballUrl.startsWith("http://")) throw new NpmClientError(`Unsupported tarball URL: "${tarballUrl}". Use https:// (or http://).`);
3674
+ const headers = {};
3675
+ if (token && isSameOrigin(tarballUrl, registryUrl)) headers.Authorization = `Bearer ${token}`;
3676
+ const response = await fetchWithTimeout(tarballUrl, headers);
3677
+ if (!response.ok) throw new NpmClientError(`Failed to download tarball ${tarballUrl}: HTTP ${response.status}`, { statusCode: response.status });
3678
+ const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
3679
+ if (Number.isFinite(contentLength) && contentLength > maxSize) throw new NpmClientError(oversizedTarballMessage(tarballUrl, maxSize));
3680
+ return await readBodyWithLimit({
3681
+ response,
3682
+ tarballUrl,
3683
+ maxSize
3684
+ });
3685
+ }
3686
+ function oversizedTarballMessage(tarballUrl, maxSize) {
3687
+ return `Tarball ${tarballUrl} exceeds max size of ${maxSize / 1024 / 1024}MB.`;
3688
+ }
3689
+ /**
3690
+ * Read a response body incrementally, aborting as soon as the size cap is
3691
+ * exceeded. content-length can be absent or forged, so the streaming check is
3692
+ * the actual enforcement of the cap.
3693
+ */
3694
+ async function readBodyWithLimit(params) {
3695
+ const { response, tarballUrl, maxSize } = params;
3696
+ const reader = response.body?.getReader();
3697
+ if (!reader) {
3698
+ const arrayBuffer = await response.arrayBuffer();
3699
+ if (arrayBuffer.byteLength > maxSize) throw new NpmClientError(oversizedTarballMessage(tarballUrl, maxSize));
3700
+ return Buffer.from(arrayBuffer);
3701
+ }
3702
+ const chunks = [];
3703
+ let totalBytes = 0;
3704
+ while (true) {
3705
+ const { done, value } = await reader.read();
3706
+ if (done) break;
3707
+ totalBytes += value.byteLength;
3708
+ if (totalBytes > maxSize) {
3709
+ await reader.cancel();
3710
+ throw new NpmClientError(oversizedTarballMessage(tarballUrl, maxSize));
3711
+ }
3712
+ chunks.push(Buffer.from(value));
3713
+ }
3714
+ return Buffer.concat(chunks);
3715
+ }
3716
+ function isSameOrigin(urlA, urlB) {
3717
+ try {
3718
+ return new URL(urlA).origin === new URL(urlB).origin;
3719
+ } catch {
3720
+ return false;
3721
+ }
3722
+ }
3723
+ /**
3724
+ * Convert a hex sha1 shasum to the SRI form used by `verifyTarballIntegrity`.
3725
+ * Rejects malformed shasum values so a broken value is never recorded in the
3726
+ * lockfile as a seemingly valid SRI string.
3727
+ */
3728
+ function shasumToSri(shasum) {
3729
+ if (!/^[0-9a-f]{40}$/i.test(shasum)) throw new NpmClientError(`Malformed sha1 shasum in registry metadata: "${shasum}"`);
3730
+ return `sha1-${Buffer.from(shasum, "hex").toString("base64")}`;
3731
+ }
3732
+ /**
3733
+ * Verify a downloaded tarball against registry integrity metadata.
3734
+ * Prefers the strongest supported algorithm in the SRI `integrity` string and
3735
+ * falls back to the legacy sha1 `shasum`. An `integrity` string that is
3736
+ * present but cannot be parsed fails closed; a warning is only logged when
3737
+ * the registry provides no integrity metadata at all.
3738
+ */
3739
+ function verifyTarballIntegrity(params) {
3740
+ const { tarball, integrity, shasum, context, logger } = params;
3741
+ if (integrity !== void 0) {
3742
+ const sri = pickStrongestSriEntry(integrity);
3743
+ if (!sri) throw new NpmClientError(`Unsupported or malformed integrity metadata for ${context}. Expected an SRI string with sha512/sha384/sha256/sha1.`);
3744
+ const actual = (0, node_crypto.createHash)(sri.algorithm).update(tarball).digest("base64");
3745
+ if (actual !== sri.digest) throw new NpmClientError(`Integrity verification failed for ${context}: expected ${sri.algorithm}-${sri.digest}, got ${sri.algorithm}-${actual}. The tarball may have been tampered with.`);
3746
+ return;
3747
+ }
3748
+ if (shasum) {
3749
+ const actual = (0, node_crypto.createHash)("sha1").update(tarball).digest("hex");
3750
+ if (actual !== shasum.toLowerCase()) throw new NpmClientError(`Integrity verification failed for ${context}: expected sha1 ${shasum}, got ${actual}. The tarball may have been tampered with.`);
3751
+ return;
3752
+ }
3753
+ logger?.warn(`No integrity metadata available for ${context}; skipping tarball verification.`);
3754
+ }
3755
+ function pickStrongestSriEntry(integrity) {
3756
+ if (!integrity) return;
3757
+ const entries = integrity.split(/\s+/).map((entry) => {
3758
+ const separatorIndex = entry.indexOf("-");
3759
+ if (separatorIndex === -1) return void 0;
3760
+ const algorithm = entry.slice(0, separatorIndex);
3761
+ const digest = entry.slice(separatorIndex + 1).split("?")[0] ?? "";
3762
+ const known = INTEGRITY_ALGORITHM_PREFERENCE.find((a) => a === algorithm);
3763
+ if (!known || digest.length === 0) return void 0;
3764
+ return {
3765
+ algorithm: known,
3766
+ digest
3767
+ };
3768
+ }).filter((entry) => Boolean(entry));
3769
+ for (const algorithm of INTEGRITY_ALGORITHM_PREFERENCE) {
3770
+ const match = entries.find((entry) => entry.algorithm === algorithm);
3771
+ if (match) return match;
3772
+ }
3773
+ }
3774
+ /**
3775
+ * Log contextual hints for NpmClientError to help users troubleshoot
3776
+ * authentication problems without ever logging the token itself.
3777
+ */
3778
+ function logNpmAuthHints(params) {
3779
+ const { error, logger } = params;
3780
+ if (error.statusCode === 401 || error.statusCode === 403) logger.info("Hint: The registry rejected the request. Set NPM_TOKEN (or the per-source tokenEnv variable) to a token with read access. Note: .npmrc files are not read by the npm transport.");
3781
+ else if (error.statusCode === 404) logger.info("Hint: Package not found. Check the package name and the registry URL. Some registries also return 404 for unauthorized requests.");
3782
+ }
3783
+ const NpmLockedSkillSchema = zod_mini.z.object({ integrity: zod_mini.z.string() });
3784
+ /**
3785
+ * Schema for a single locked npm source entry.
3786
+ */
3787
+ const NpmLockedSourceSchema = zod_mini.z.object({
3788
+ registry: (0, zod_mini.optional)(zod_mini.z.string()),
3789
+ requestedVersion: (0, zod_mini.optional)(zod_mini.z.string()),
3790
+ resolvedVersion: zod_mini.z.string(),
3791
+ /** SRI integrity of the package tarball as reported by the registry. */
3792
+ integrity: (0, zod_mini.optional)(zod_mini.z.string()),
3793
+ resolvedAt: (0, zod_mini.optional)(zod_mini.z.string()),
3794
+ skills: zod_mini.z.record(zod_mini.z.string(), NpmLockedSkillSchema)
3795
+ });
3796
+ const NpmSourcesLockSchema = zod_mini.z.object({
3797
+ lockfileVersion: zod_mini.z.number(),
3798
+ sources: zod_mini.z.record(zod_mini.z.string(), NpmLockedSourceSchema)
3799
+ });
3800
+ /**
3801
+ * Create an empty npm lockfile structure.
3802
+ */
3803
+ function createEmptyNpmLock() {
3804
+ return {
3805
+ lockfileVersion: 1,
3806
+ sources: {}
3807
+ };
3808
+ }
3809
+ /**
3810
+ * Read the npm lockfile from disk.
3811
+ * @returns The parsed lockfile, or an empty lockfile if it doesn't exist or is invalid.
3812
+ */
3813
+ async function readNpmLockFile(params) {
3814
+ const { logger } = params;
3815
+ const lockPath = (0, node_path.join)(params.projectRoot, require_import.RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH);
3816
+ if (!await require_import.fileExists(lockPath)) {
3817
+ logger.debug("No npm sources lockfile found, starting fresh.");
3818
+ return createEmptyNpmLock();
3819
+ }
3820
+ try {
3821
+ const content = await require_import.readFileContent(lockPath);
3822
+ const result = NpmSourcesLockSchema.safeParse(JSON.parse(content));
3823
+ if (result.success) return result.data;
3824
+ logger.warn(`Invalid npm sources lockfile format (${require_import.RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH}). Starting fresh.`);
3825
+ return createEmptyNpmLock();
3826
+ } catch {
3827
+ logger.warn(`Failed to read npm sources lockfile (${require_import.RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH}). Starting fresh.`);
3828
+ return createEmptyNpmLock();
3829
+ }
3830
+ }
3831
+ /**
3832
+ * Write the npm lockfile to disk.
3833
+ */
3834
+ async function writeNpmLockFile(params) {
3835
+ const { logger } = params;
3836
+ const lockPath = (0, node_path.join)(params.projectRoot, require_import.RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH);
3837
+ await require_import.writeFileContent(lockPath, JSON.stringify(params.lock, null, 2) + "\n");
3838
+ logger.debug(`Wrote npm sources lockfile to ${lockPath}`);
3839
+ }
3840
+ /**
3841
+ * Normalize an npm source key (package name) for lockfile lookups.
3842
+ */
3843
+ function normalizeNpmSourceKey(source) {
3844
+ return source.trim();
3845
+ }
3846
+ /**
3847
+ * Get the locked entry for an npm source key, if it exists.
3848
+ */
3849
+ function getNpmLockedSource(lock, sourceKey) {
3850
+ const normalized = normalizeNpmSourceKey(sourceKey);
3851
+ return Object.prototype.hasOwnProperty.call(lock.sources, normalized) ? lock.sources[normalized] : void 0;
3852
+ }
3853
+ /**
3854
+ * Set (or update) a locked entry for an npm source key (immutable).
3855
+ */
3856
+ function setNpmLockedSource(lock, sourceKey, entry) {
3857
+ return {
3858
+ lockfileVersion: lock.lockfileVersion,
3859
+ sources: {
3860
+ ...lock.sources,
3861
+ [normalizeNpmSourceKey(sourceKey)]: entry
3862
+ }
3863
+ };
3864
+ }
3865
+ /**
3866
+ * Get the skill names from a locked npm source entry.
3867
+ */
3868
+ function getNpmLockedSkillNames(entry) {
3869
+ return Object.keys(entry.skills);
3870
+ }
3871
+ //#endregion
3872
+ //#region src/lib/npm-tar.ts
3873
+ /**
3874
+ * Minimal, hardened tar reader for npm package tarballs (EXPERIMENTAL npm
3875
+ * transport). Intentionally supports only the subset of the (pax-extended)
3876
+ * ustar format that npm-compatible registries produce:
3877
+ *
3878
+ * - regular files (typeflag "0" / "\0")
3879
+ * - directories (typeflag "5") — skipped; directories are created implicitly
3880
+ * - pax extended headers (typeflag "x") — only the `path` override is honored
3881
+ * - GNU long names (typeflag "L")
3882
+ *
3883
+ * Everything else (symlinks, hardlinks, devices, FIFOs, ...) is skipped and
3884
+ * never materialized. Extraction is bounded by file-count and total-byte caps
3885
+ * to prevent decompression bombs, and every entry path is validated against
3886
+ * traversal (absolute paths, `..` segments, backslashes).
3887
+ */
3888
+ const BLOCK_SIZE = 512;
3889
+ var NpmTarError = class extends Error {
3890
+ constructor(message, cause) {
3891
+ super(message, { cause });
3892
+ this.name = "NpmTarError";
3893
+ }
3894
+ };
3895
+ /**
3896
+ * Gunzip and extract a npm package tarball into an in-memory file list.
3897
+ * Throws {@link NpmTarError} on malformed archives, traversal attempts, or
3898
+ * resource-limit violations.
3899
+ */
3900
+ function extractPackageTarball(params) {
3901
+ const { tarball, onSkippedEntry } = params;
3902
+ const maxFiles = params.maxFiles ?? 1e4;
3903
+ const maxTotalBytes = params.maxTotalBytes ?? 104857600;
3904
+ let tar;
3905
+ try {
3906
+ tar = (0, node_zlib.gunzipSync)(tarball, { maxOutputLength: maxTotalBytes + maxFiles * 3 * BLOCK_SIZE + 2 * BLOCK_SIZE });
3907
+ } catch (error) {
3908
+ throw new NpmTarError("Failed to gunzip package tarball", error);
3909
+ }
3910
+ return parseTarBuffer({
3911
+ tar,
3912
+ maxFiles,
3913
+ maxTotalBytes,
3914
+ onSkippedEntry
3915
+ });
3916
+ }
3917
+ function parseTarBuffer(params) {
3918
+ const { tar, maxFiles, maxTotalBytes, onSkippedEntry } = params;
3919
+ const files = [];
3920
+ let totalBytes = 0;
3921
+ let offset = 0;
3922
+ let pendingLongName;
3923
+ let pendingPaxPath;
3924
+ while (offset + BLOCK_SIZE <= tar.length) {
3925
+ const header = tar.subarray(offset, offset + BLOCK_SIZE);
3926
+ if (isZeroBlock(header)) break;
3927
+ verifyHeaderChecksum(header);
3928
+ const size = parseOctalField(header, 124, 12, "size");
3929
+ const typeflag = String.fromCharCode(header[156] ?? 0);
3930
+ const dataStart = offset + BLOCK_SIZE;
3931
+ const dataEnd = dataStart + size;
3932
+ if (dataEnd > tar.length) throw new NpmTarError("Truncated tar archive: entry data extends past end of archive");
3933
+ switch (typeflag) {
3934
+ case "x": {
3935
+ const records = parsePaxRecords(tar.subarray(dataStart, dataEnd));
3936
+ if (records.has("size")) throw new NpmTarError("Unsupported tar archive: pax size override is not supported");
3937
+ pendingPaxPath = records.get("path") ?? pendingPaxPath;
3938
+ break;
3939
+ }
3940
+ case "L":
3941
+ pendingLongName = trimAtFirstNul(tar.toString("utf8", dataStart, dataEnd));
3942
+ break;
3943
+ case "g": {
3944
+ const records = parsePaxRecords(tar.subarray(dataStart, dataEnd));
3945
+ if (records.has("size") || records.has("path")) throw new NpmTarError("Unsupported tar archive: global pax size/path overrides are not supported");
3946
+ break;
3947
+ }
3948
+ case "0":
3949
+ case "\0": {
3950
+ const rawName = resolveEntryName({
3951
+ header,
3952
+ pendingLongName,
3953
+ pendingPaxPath
3954
+ });
3955
+ pendingLongName = void 0;
3956
+ pendingPaxPath = void 0;
3957
+ const relativePath = toSafeRelativePath(rawName);
3958
+ if (relativePath !== null) {
3959
+ if (files.length + 1 > maxFiles) throw new NpmTarError(`Package tarball exceeds max file count of ${maxFiles}. Aborting to prevent resource exhaustion.`);
3960
+ totalBytes += size;
3961
+ if (totalBytes > maxTotalBytes) throw new NpmTarError(`Package tarball exceeds max total size of ${maxTotalBytes / 1024 / 1024}MB. Aborting to prevent resource exhaustion.`);
3962
+ files.push({
3963
+ relativePath,
3964
+ content: Buffer.from(tar.subarray(dataStart, dataEnd))
3965
+ });
3966
+ }
3967
+ break;
3968
+ }
3969
+ case "5":
3970
+ pendingLongName = void 0;
3971
+ pendingPaxPath = void 0;
3972
+ break;
3973
+ default: {
3974
+ const rawName = resolveEntryName({
3975
+ header,
3976
+ pendingLongName,
3977
+ pendingPaxPath
3978
+ });
3979
+ pendingLongName = void 0;
3980
+ pendingPaxPath = void 0;
3981
+ onSkippedEntry?.(`Skipping unsupported tar entry type "${typeflag}" for "${rawName}" (only regular files are extracted).`);
3982
+ break;
3983
+ }
3984
+ }
3985
+ offset = dataStart + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
3986
+ }
3987
+ return files;
3988
+ }
3989
+ function isZeroBlock(block) {
3990
+ return block.every((byte) => byte === 0);
3991
+ }
3992
+ /** Cut a string at its first NUL character (tar fields are NUL-terminated). */
3993
+ function trimAtFirstNul(value) {
3994
+ const nulIndex = value.indexOf("\0");
3995
+ return nulIndex === -1 ? value : value.substring(0, nulIndex);
3996
+ }
3997
+ function parseOctalField(block, offset, length, field) {
3998
+ if (((block[offset] ?? 0) & 128) !== 0) throw new NpmTarError(`Unsupported tar archive: base-256 ${field} field is not supported`);
3999
+ const text = trimAtFirstNul(block.toString("latin1", offset, offset + length)).trim();
4000
+ if (text === "") return 0;
4001
+ if (!/^[0-7]+$/.test(text)) throw new NpmTarError(`Invalid tar header: malformed octal ${field} field`);
4002
+ return Number.parseInt(text, 8);
4003
+ }
4004
+ /**
4005
+ * Verify the ustar header checksum: the unsigned byte sum of the 512-byte
4006
+ * header with the checksum field itself treated as ASCII spaces.
4007
+ */
4008
+ function verifyHeaderChecksum(header) {
4009
+ const stored = parseOctalField(header, 148, 8, "checksum");
4010
+ let sum = 0;
4011
+ for (let i = 0; i < BLOCK_SIZE; i++) sum += i >= 148 && i < 156 ? 32 : header[i] ?? 0;
4012
+ if (sum !== stored) throw new NpmTarError("Invalid tar header: checksum mismatch");
4013
+ }
4014
+ function readCString(block, offset, length) {
4015
+ const end = block.indexOf(0, offset);
4016
+ const stop = end === -1 || end > offset + length ? offset + length : end;
4017
+ return block.toString("utf8", offset, stop);
4018
+ }
4019
+ function resolveEntryName(params) {
4020
+ const { header, pendingLongName, pendingPaxPath } = params;
4021
+ if (pendingPaxPath !== void 0) return pendingPaxPath;
4022
+ if (pendingLongName !== void 0) return pendingLongName;
4023
+ const name = readCString(header, 0, 100);
4024
+ const prefix = header.toString("latin1", 257, 262) === "ustar" ? readCString(header, 345, 155) : "";
4025
+ return prefix.length > 0 ? `${prefix}/${name}` : name;
4026
+ }
4027
+ /**
4028
+ * Parse pax extended header records of the form `<len> <key>=<value>\n`,
4029
+ * where `<len>` is the decimal length of the whole record.
4030
+ */
4031
+ function parsePaxRecords(data) {
4032
+ const records = /* @__PURE__ */ new Map();
4033
+ let offset = 0;
4034
+ while (offset < data.length) {
4035
+ if (data[offset] === 0) break;
4036
+ const spaceIndex = data.indexOf(32, offset);
4037
+ if (spaceIndex === -1) throw new NpmTarError("Invalid pax header: missing length delimiter");
4038
+ const recordLength = Number.parseInt(data.toString("utf8", offset, spaceIndex), 10);
4039
+ if (!Number.isInteger(recordLength) || recordLength <= 0 || offset + recordLength > data.length) throw new NpmTarError("Invalid pax header: malformed record length");
4040
+ const record = data.toString("utf8", spaceIndex + 1, offset + recordLength - 1);
4041
+ const equalsIndex = record.indexOf("=");
4042
+ if (equalsIndex !== -1) records.set(record.slice(0, equalsIndex), record.slice(equalsIndex + 1));
4043
+ offset += recordLength;
4044
+ }
4045
+ return records;
4046
+ }
4047
+ /**
4048
+ * Validate a tar entry path and convert it to a package-root-relative path
4049
+ * with the first component stripped. Returns null for entries that resolve to
4050
+ * the package root itself (e.g. the `package/` folder entry). Throws on
4051
+ * traversal attempts.
4052
+ */
4053
+ function toSafeRelativePath(rawPath) {
4054
+ if (rawPath.includes("\0")) throw new NpmTarError(`Unsafe tar entry path (NUL byte): "${rawPath}"`);
4055
+ if (rawPath.includes("\\")) throw new NpmTarError(`Unsafe tar entry path (backslash): "${rawPath}"`);
4056
+ if (rawPath.startsWith("/")) throw new NpmTarError(`Unsafe tar entry path (absolute): "${rawPath}"`);
4057
+ const segments = rawPath.split("/").filter((segment) => segment !== "" && segment !== ".");
4058
+ if (segments.includes("..")) throw new NpmTarError(`Unsafe tar entry path (".." segment): "${rawPath}"`);
4059
+ segments.shift();
4060
+ if (segments.length === 0) return null;
4061
+ return segments.join("/");
4062
+ }
3503
4063
  /**
3504
4064
  * Schema for a single locked skill entry with content integrity.
3505
4065
  */
@@ -3689,37 +4249,41 @@ async function resolveAndFetchSources(params) {
3689
4249
  projectRoot,
3690
4250
  logger
3691
4251
  });
4252
+ let npmLock = options.updateSources ? createEmptyNpmLock() : await readNpmLockFile({
4253
+ projectRoot,
4254
+ logger
4255
+ });
3692
4256
  if (options.frozen) assertFrozenLockCoversSources({
3693
4257
  lock,
4258
+ npmLock,
3694
4259
  sources
3695
4260
  });
3696
4261
  const originalLockJson = JSON.stringify(lock);
4262
+ const originalNpmLockJson = JSON.stringify(npmLock);
3697
4263
  const client = new GitHubClient({ token: GitHubClient.resolveToken(options.token) });
3698
4264
  const localSkillNames = await require_import.getLocalSkillDirNames(projectRoot);
3699
4265
  let totalSkillCount = 0;
3700
4266
  const allFetchedSkillNames = /* @__PURE__ */ new Set();
3701
4267
  for (const sourceEntry of sources) try {
3702
- const { skillCount, fetchedSkillNames, updatedLock } = await fetchSourceByTransport({
4268
+ const result = await fetchSingleSource({
3703
4269
  sourceEntry,
3704
4270
  client,
3705
4271
  projectRoot,
3706
4272
  lock,
4273
+ npmLock,
3707
4274
  localSkillNames,
3708
4275
  alreadyFetchedSkillNames: allFetchedSkillNames,
3709
4276
  updateSources: options.updateSources ?? false,
3710
4277
  frozen: options.frozen ?? false,
3711
4278
  logger
3712
4279
  });
3713
- lock = updatedLock;
3714
- totalSkillCount += skillCount;
3715
- for (const name of fetchedSkillNames) allFetchedSkillNames.add(name);
4280
+ lock = result.lock;
4281
+ npmLock = result.npmLock;
4282
+ totalSkillCount += result.skillCount;
4283
+ for (const name of result.fetchedSkillNames) allFetchedSkillNames.add(name);
3716
4284
  } catch (error) {
3717
- logger.error(`Failed to fetch source "${sourceEntry.source}": ${require_import.formatError(error)}`);
3718
- if (error instanceof GitHubClientError) logGitHubAuthHints({
3719
- error,
3720
- logger
3721
- });
3722
- else if (error instanceof GitClientError) logGitClientHints({
4285
+ logSourceFetchFailure({
4286
+ sourceEntry,
3723
4287
  error,
3724
4288
  logger
3725
4289
  });
@@ -3729,18 +4293,100 @@ async function resolveAndFetchSources(params) {
3729
4293
  sources,
3730
4294
  logger
3731
4295
  });
3732
- if (!options.frozen && JSON.stringify(lock) !== originalLockJson) await writeLockFile({
4296
+ npmLock = pruneStaleNpmLockEntries({
4297
+ npmLock,
4298
+ sources,
4299
+ logger
4300
+ });
4301
+ await writeLockFilesIfChanged({
3733
4302
  projectRoot,
3734
4303
  lock,
4304
+ npmLock,
4305
+ originalLockJson,
4306
+ originalNpmLockJson,
4307
+ frozen: options.frozen ?? false,
3735
4308
  logger
3736
4309
  });
3737
- else logger.debug("Lockfile unchanged, skipping write.");
3738
4310
  return {
3739
4311
  fetchedSkillCount: totalSkillCount,
3740
4312
  sourcesProcessed: sources.length
3741
4313
  };
3742
4314
  }
3743
4315
  /**
4316
+ * Dispatch a single source to the npm fetcher or the git/github fetcher,
4317
+ * returning the (possibly) updated lock objects for both lockfiles.
4318
+ */
4319
+ async function fetchSingleSource(params) {
4320
+ const { sourceEntry, lock, npmLock } = params;
4321
+ if ((sourceEntry.transport ?? "github") === "npm") {
4322
+ const result = await fetchSourceViaNpm({
4323
+ sourceEntry,
4324
+ projectRoot: params.projectRoot,
4325
+ npmLock,
4326
+ localSkillNames: params.localSkillNames,
4327
+ alreadyFetchedSkillNames: params.alreadyFetchedSkillNames,
4328
+ updateSources: params.updateSources,
4329
+ logger: params.logger
4330
+ });
4331
+ return {
4332
+ skillCount: result.skillCount,
4333
+ fetchedSkillNames: result.fetchedSkillNames,
4334
+ lock,
4335
+ npmLock: result.updatedLock
4336
+ };
4337
+ }
4338
+ const result = await fetchSourceByTransport({
4339
+ sourceEntry,
4340
+ client: params.client,
4341
+ projectRoot: params.projectRoot,
4342
+ lock,
4343
+ localSkillNames: params.localSkillNames,
4344
+ alreadyFetchedSkillNames: params.alreadyFetchedSkillNames,
4345
+ updateSources: params.updateSources,
4346
+ frozen: params.frozen,
4347
+ logger: params.logger
4348
+ });
4349
+ return {
4350
+ skillCount: result.skillCount,
4351
+ fetchedSkillNames: result.fetchedSkillNames,
4352
+ lock: result.updatedLock,
4353
+ npmLock
4354
+ };
4355
+ }
4356
+ /** Log a per-source fetch failure with transport-specific troubleshooting hints. */
4357
+ function logSourceFetchFailure(params) {
4358
+ const { sourceEntry, error, logger } = params;
4359
+ logger.error(`Failed to fetch source "${sourceEntry.source}": ${require_import.formatError(error)}`);
4360
+ if (error instanceof GitHubClientError) logGitHubAuthHints({
4361
+ error,
4362
+ logger
4363
+ });
4364
+ else if (error instanceof GitClientError) logGitClientHints({
4365
+ error,
4366
+ logger
4367
+ });
4368
+ else if (error instanceof NpmClientError) logNpmAuthHints({
4369
+ error,
4370
+ logger
4371
+ });
4372
+ }
4373
+ /** Write each lockfile only when it changed (and never in frozen mode). */
4374
+ async function writeLockFilesIfChanged(params) {
4375
+ const { projectRoot, lock, npmLock, originalLockJson, originalNpmLockJson, frozen, logger } = params;
4376
+ if (!frozen && JSON.stringify(lock) !== originalLockJson) await writeLockFile({
4377
+ projectRoot,
4378
+ lock,
4379
+ logger
4380
+ });
4381
+ else logger.debug("Lockfile unchanged, skipping write.");
4382
+ if (!frozen && JSON.stringify(npmLock) !== originalNpmLockJson) await writeNpmLockFile({
4383
+ projectRoot,
4384
+ lock: npmLock,
4385
+ logger
4386
+ });
4387
+ else logger.debug("npm lockfile unchanged, skipping write.");
4388
+ }
4389
+ /**
3744
4390
  * Log contextual hints for GitClientError to help users troubleshoot.
3745
4391
  */
3746
4392
  function logGitClientHints(params) {
@@ -3749,13 +4395,15 @@ function logGitClientHints(params) {
3749
4395
  else logger.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
3750
4396
  }
3751
4397
  /**
3752
- * Frozen mode: validate the lockfile covers every declared source. Throws with
3753
- * remediation guidance listing any uncovered source keys.
4398
+ * Frozen mode: validate the lockfiles cover every declared source. Throws with
4399
+ * remediation guidance listing any uncovered source keys. npm-transport
4400
+ * sources are checked against the npm lockfile; everything else against the
4401
+ * main sources lockfile.
3754
4402
  */
3755
4403
  function assertFrozenLockCoversSources(params) {
3756
- const { lock, sources } = params;
4404
+ const { lock, npmLock, sources } = params;
3757
4405
  const missingKeys = [];
3758
- for (const source of sources) if (!getLockedSource(lock, source.source)) missingKeys.push(source.source);
4406
+ for (const source of sources) if (!((source.transport ?? "github") === "npm" ? getNpmLockedSource(npmLock, source.source) : getLockedSource(lock, source.source))) missingKeys.push(source.source);
3759
4407
  if (missingKeys.length > 0) throw new Error(`Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`);
3760
4408
  }
3761
4409
  /**
@@ -3791,7 +4439,7 @@ async function fetchSourceByTransport(params) {
3791
4439
  */
3792
4440
  function pruneStaleLockEntries(params) {
3793
4441
  const { lock, sources, logger } = params;
3794
- const sourceKeys = new Set(sources.map((s) => normalizeSourceKey(s.source)));
4442
+ const sourceKeys = new Set(sources.filter((s) => (s.transport ?? "github") !== "npm").map((s) => normalizeSourceKey(s.source)));
3795
4443
  const prunedSources = {};
3796
4444
  for (const [key, value] of Object.entries(lock.sources)) if (sourceKeys.has(normalizeSourceKey(key))) prunedSources[key] = value;
3797
4445
  else logger.debug(`Pruned stale lockfile entry: ${key}`);
@@ -3801,6 +4449,21 @@ function pruneStaleLockEntries(params) {
3801
4449
  };
3802
4450
  }
3803
4451
  /**
4452
+ * Prune stale npm lockfile entries whose keys are not in the current
4453
+ * npm-transport sources (immutable — returns a fresh lock object).
4454
+ */
4455
+ function pruneStaleNpmLockEntries(params) {
4456
+ const { npmLock, sources, logger } = params;
4457
+ const sourceKeys = new Set(sources.filter((s) => (s.transport ?? "github") === "npm").map((s) => normalizeNpmSourceKey(s.source)));
4458
+ const prunedSources = {};
4459
+ for (const [key, value] of Object.entries(npmLock.sources)) if (sourceKeys.has(normalizeNpmSourceKey(key))) prunedSources[key] = value;
4460
+ else logger.debug(`Pruned stale npm lockfile entry: ${key}`);
4461
+ return {
4462
+ lockfileVersion: npmLock.lockfileVersion,
4463
+ sources: prunedSources
4464
+ };
4465
+ }
4466
+ /**
3804
4467
  * Check if all locked skills exist on disk in the curated directory.
3805
4468
  */
3806
4469
  async function checkLockedSkillsExist(curatedDir, skillNames) {
@@ -3868,16 +4531,31 @@ async function writeSkillAndComputeIntegrity(params) {
3868
4531
  return { integrity };
3869
4532
  }
3870
4533
  /**
4534
+ * Merge back locked skills that still exist in the remote but were skipped
4535
+ * during fetching (due to local precedence, already-fetched, etc.). Skills no
4536
+ * longer present in the remote (e.g. renamed or deleted upstream) are
4537
+ * intentionally dropped. Shared by the git/github and npm lock updates.
4538
+ */
4539
+ function mergeFetchedWithLockedSkills(params) {
4540
+ const { fetchedSkills, lockedSkills, remoteSkillNames } = params;
4541
+ const remoteSet = new Set(remoteSkillNames);
4542
+ const mergedSkills = { ...fetchedSkills };
4543
+ if (lockedSkills) {
4544
+ for (const [skillName, skillEntry] of Object.entries(lockedSkills)) if (!(skillName in mergedSkills) && remoteSet.has(skillName)) mergedSkills[skillName] = skillEntry;
4545
+ }
4546
+ return mergedSkills;
4547
+ }
4548
+ /**
3871
4549
  * Merge newly fetched skills with existing locked skills and update the lockfile.
3872
4550
  */
3873
4551
  function buildLockUpdate(params) {
3874
4552
  const { lock, sourceKey, fetchedSkills, locked, requestedRef, resolvedSha, remoteSkillNames, logger } = params;
3875
4553
  const fetchedNames = Object.keys(fetchedSkills);
3876
- const remoteSet = new Set(remoteSkillNames);
3877
- const mergedSkills = { ...fetchedSkills };
3878
- if (locked) {
3879
- for (const [skillName, skillEntry] of Object.entries(locked.skills)) if (!(skillName in mergedSkills) && remoteSet.has(skillName)) mergedSkills[skillName] = skillEntry;
3880
- }
4554
+ const mergedSkills = mergeFetchedWithLockedSkills({
4555
+ fetchedSkills,
4556
+ lockedSkills: locked?.skills,
4557
+ remoteSkillNames
4558
+ });
3881
4559
  const updatedLock = setLockedSource(lock, sourceKey, {
3882
4560
  requestedRef,
3883
4561
  resolvedRef: resolvedSha,
@@ -4318,6 +4996,265 @@ async function fetchSourceViaGit(params) {
4318
4996
  updatedLock: result.updatedLock
4319
4997
  };
4320
4998
  }
4999
+ /**
5000
+ * Select the skill files inside an extracted npm package, mirroring the git
5001
+ * transport's discovery: files under `skills/` (or the configured `path`) are
5002
+ * grouped per subdirectory; a package whose `SKILL.md` sits at the package
5003
+ * root is installed as a single skill (root fallback via
5004
+ * {@link shouldUseRootFallback}), named after the requested skill or, for
5005
+ * wildcard fetches, after the package's base name.
5006
+ */
5007
+ function selectNpmSkillFiles(params) {
5008
+ const { allFiles, skillsPath, skillFilter, isWildcard, packageName } = params;
5009
+ const normalizedBase = node_path.posix.normalize(skillsPath.replace(/\\/g, "/")).replace(/\/+$/, "");
5010
+ if (normalizedBase === "" || normalizedBase === ".") return {
5011
+ remoteFiles: allFiles,
5012
+ skillFilter,
5013
+ isWildcard
5014
+ };
5015
+ const prefix = `${normalizedBase}/`;
5016
+ const filesUnderBase = allFiles.filter((file) => file.relativePath.startsWith(prefix)).map((file) => ({
5017
+ relativePath: file.relativePath.substring(prefix.length),
5018
+ content: file.content
5019
+ }));
5020
+ if (filesUnderBase.length > 0) return {
5021
+ remoteFiles: filesUnderBase,
5022
+ skillFilter,
5023
+ isWildcard
5024
+ };
5025
+ const hasRootSkillFile = allFiles.some((file) => file.relativePath === require_import.SKILL_FILE_NAME);
5026
+ const fallbackFilter = isWildcard ? [npmPackageBaseName(packageName)] : skillFilter;
5027
+ const [singleSkillName] = fallbackFilter;
5028
+ if (fallbackFilter.length === 1 && singleSkillName !== void 0 && shouldUseRootFallback({
5029
+ skillFilter: fallbackFilter,
5030
+ isWildcard: false,
5031
+ hasRootSkillFile,
5032
+ hasRequestedSkillDir: false
5033
+ })) return {
5034
+ remoteFiles: allFiles,
5035
+ skillFilter: fallbackFilter,
5036
+ isWildcard: false
5037
+ };
5038
+ return {
5039
+ remoteFiles: filesUnderBase,
5040
+ skillFilter,
5041
+ isWildcard
5042
+ };
5043
+ }
5044
+ /** Base name of an npm package: `@scope/name` -> `name`. */
5045
+ function npmPackageBaseName(packageName) {
5046
+ const slashIndex = packageName.indexOf("/");
5047
+ return slashIndex === -1 ? packageName : packageName.substring(slashIndex + 1);
5048
+ }
5049
+ /**
5050
+ * Resolve the version to fetch for an npm source: the locked version when
5051
+ * available (deterministic re-fetch), otherwise the declared `ref` (exact
5052
+ * version or dist-tag, defaulting to "latest") resolved via the packument.
5053
+ */
5054
+ function resolveNpmFetchVersion(params) {
5055
+ const { sourceEntry, locked, updateSources } = params;
5056
+ if (locked && !updateSources) return {
5057
+ lockedVersion: locked.resolvedVersion,
5058
+ requestedVersion: locked.requestedVersion
5059
+ };
5060
+ return {
5061
+ lockedVersion: void 0,
5062
+ requestedVersion: sourceEntry.ref ?? "latest"
5063
+ };
5064
+ }
5065
+ /**
5066
+ * Resolve the package version via the registry packument, download the
5067
+ * tarball, and verify it against the registry (and, when re-fetching a locked
5068
+ * version, the locked) integrity metadata.
5069
+ */
5070
+ async function downloadVerifiedNpmTarball(params) {
5071
+ const { packageName, registryUrl, token, lockedVersion, requestedVersion, locked, logger } = params;
5072
+ const packument = await fetchPackument({
5073
+ registryUrl,
5074
+ packageName,
5075
+ token
5076
+ });
5077
+ const resolvedVersion = lockedVersion ?? resolvePackumentVersion({
5078
+ packument,
5079
+ packageName,
5080
+ requested: requestedVersion ?? "latest"
5081
+ });
5082
+ logger.debug(`Resolved ${packageName}@${requestedVersion ?? "latest"} to ${resolvedVersion}`);
5083
+ const dist = getPackumentVersionDist({
5084
+ packument,
5085
+ packageName,
5086
+ version: resolvedVersion
5087
+ });
5088
+ const tarball = await fetchTarball({
5089
+ tarballUrl: dist.tarball,
5090
+ registryUrl,
5091
+ token
5092
+ });
5093
+ const context = `${packageName}@${resolvedVersion}`;
5094
+ verifyTarballIntegrity({
5095
+ tarball,
5096
+ integrity: dist.integrity,
5097
+ shasum: dist.shasum,
5098
+ context,
5099
+ logger
5100
+ });
5101
+ if (locked?.integrity && locked.resolvedVersion === resolvedVersion) verifyTarballIntegrity({
5102
+ tarball,
5103
+ integrity: locked.integrity,
5104
+ context,
5105
+ logger
5106
+ });
5107
+ return {
5108
+ resolvedVersion,
5109
+ dist,
5110
+ tarball
5111
+ };
5112
+ }
5113
+ /**
5114
+ * Extract a verified npm tarball in memory and convert its entries into
5115
+ * remote skill files, skipping any file above MAX_FILE_SIZE.
5116
+ */
5117
+ function extractNpmRemoteFiles(params) {
5118
+ const { tarball, logger } = params;
5119
+ const extracted = extractPackageTarball({
5120
+ tarball,
5121
+ onSkippedEntry: (message) => logger.warn(message)
5122
+ });
5123
+ const allFiles = [];
5124
+ for (const entry of extracted) {
5125
+ if (entry.content.length > 10485760) {
5126
+ logger.warn(`Skipping file "${entry.relativePath}" (${(entry.content.length / 1024 / 1024).toFixed(2)}MB exceeds ${require_import.MAX_FILE_SIZE / 1024 / 1024}MB limit).`);
5127
+ continue;
5128
+ }
5129
+ allFiles.push({
5130
+ relativePath: entry.relativePath,
5131
+ content: entry.content.toString("utf8")
5132
+ });
5133
+ }
5134
+ return allFiles;
5135
+ }
5136
+ /** Build the npm lockfile entry for a fetched source. */
5137
+ function buildNpmLockEntry(params) {
5138
+ const { sourceEntry, requestedVersion, resolvedVersion, dist, mergedSkills } = params;
5139
+ const integrity = dist.integrity ?? (dist.shasum !== void 0 ? shasumToSri(dist.shasum) : void 0);
5140
+ return {
5141
+ ...sourceEntry.registry !== void 0 && { registry: sourceEntry.registry },
5142
+ ...requestedVersion !== void 0 && { requestedVersion },
5143
+ resolvedVersion,
5144
+ ...integrity !== void 0 && { integrity },
5145
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
5146
+ skills: mergedSkills
5147
+ };
5148
+ }
5149
+ /**
5150
+ * Fetch skills from a single npm-transport source (EXPERIMENTAL): resolve the
5151
+ * package version via the registry packument, download and verify the
5152
+ * tarball, extract it in-memory with the hardened tar reader, and install the
5153
+ * discovered skills into the curated directory.
5154
+ */
5155
+ async function fetchSourceViaNpm(params) {
5156
+ const { sourceEntry, projectRoot, npmLock, localSkillNames, alreadyFetchedSkillNames, updateSources, logger } = params;
5157
+ const packageName = sourceEntry.source;
5158
+ validateNpmPackageName(packageName);
5159
+ const registryUrl = sourceEntry.registry ?? "https://registry.npmjs.org";
5160
+ validateNpmRegistryUrl(registryUrl, { logger });
5161
+ const token = resolveNpmToken({ tokenEnv: sourceEntry.tokenEnv });
5162
+ const sourceKey = packageName;
5163
+ const locked = getNpmLockedSource(npmLock, sourceKey);
5164
+ const lockedSkillNames = locked ? getNpmLockedSkillNames(locked) : [];
5165
+ const curatedDir = (0, node_path.join)(projectRoot, require_import.RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
5166
+ const { lockedVersion, requestedVersion } = resolveNpmFetchVersion({
5167
+ sourceEntry,
5168
+ locked,
5169
+ updateSources
5170
+ });
5171
+ if (lockedVersion !== void 0) {
5172
+ if (await checkLockedSkillsExist(curatedDir, lockedSkillNames)) {
5173
+ logger.debug(`Version unchanged for ${sourceKey}, skipping re-fetch.`);
5174
+ return {
5175
+ skillCount: 0,
5176
+ fetchedSkillNames: lockedSkillNames,
5177
+ updatedLock: npmLock
5178
+ };
5179
+ }
5180
+ }
5181
+ const { resolvedVersion, dist, tarball } = await downloadVerifiedNpmTarball({
5182
+ packageName,
5183
+ registryUrl,
5184
+ token,
5185
+ lockedVersion,
5186
+ requestedVersion,
5187
+ locked,
5188
+ logger
5189
+ });
5190
+ const allFiles = extractNpmRemoteFiles({
5191
+ tarball,
5192
+ logger
5193
+ });
5194
+ const declaredFilter = sourceEntry.skills ?? ["*"];
5195
+ const declaredWildcard = declaredFilter.length === 1 && declaredFilter[0] === "*";
5196
+ const { remoteFiles, skillFilter, isWildcard } = selectNpmSkillFiles({
5197
+ allFiles,
5198
+ skillsPath: sourceEntry.path ?? "skills",
5199
+ skillFilter: declaredFilter,
5200
+ isWildcard: declaredWildcard,
5201
+ packageName
5202
+ });
5203
+ const skillFileMap = groupRemoteFilesBySkillRoot({
5204
+ remoteFiles,
5205
+ skillFilter,
5206
+ isWildcard
5207
+ });
5208
+ const allNames = [...skillFileMap.keys()];
5209
+ const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
5210
+ if (locked) await cleanPreviousCuratedSkills({
5211
+ curatedDir,
5212
+ lockedSkillNames,
5213
+ logger
5214
+ });
5215
+ const lockedForIntegrityCheck = locked ? {
5216
+ resolvedRef: locked.resolvedVersion,
5217
+ skills: locked.skills
5218
+ } : void 0;
5219
+ const fetchedSkills = {};
5220
+ for (const skillName of filteredNames) {
5221
+ if (shouldSkipSkill({
5222
+ skillName,
5223
+ sourceKey,
5224
+ localSkillNames,
5225
+ alreadyFetchedSkillNames,
5226
+ logger
5227
+ })) continue;
5228
+ fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
5229
+ skillName,
5230
+ files: skillFileMap.get(skillName) ?? [],
5231
+ curatedDir,
5232
+ locked: lockedForIntegrityCheck,
5233
+ resolvedSha: resolvedVersion,
5234
+ sourceKey,
5235
+ logger
5236
+ });
5237
+ logger.debug(`Fetched skill "${skillName}" from ${sourceKey}`);
5238
+ }
5239
+ const fetchedNames = Object.keys(fetchedSkills);
5240
+ const updatedLock = setNpmLockedSource(npmLock, sourceKey, buildNpmLockEntry({
5241
+ sourceEntry,
5242
+ requestedVersion,
5243
+ resolvedVersion,
5244
+ dist,
5245
+ mergedSkills: mergeFetchedWithLockedSkills({
5246
+ fetchedSkills,
5247
+ lockedSkills: locked?.skills,
5248
+ remoteSkillNames: filteredNames
5249
+ })
5250
+ }));
5251
+ logger.info(`Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`);
5252
+ return {
5253
+ skillCount: fetchedNames.length,
5254
+ fetchedSkillNames: fetchedNames,
5255
+ updatedLock
5256
+ };
5257
+ }
4321
5258
  //#endregion
4322
5259
  //#region src/cli/commands/install.ts
4323
5260
  const INSTALL_MODES = [
@@ -4424,6 +5361,172 @@ async function runGhInstall(logger, options) {
4424
5361
  else logger.success(`All gh sources up to date (${result.sourcesProcessed} checked).`);
4425
5362
  }
4426
5363
  //#endregion
5364
+ //#region src/mcp/checks.ts
5365
+ const logger$4 = new require_import.ConsoleLogger({
5366
+ verbose: false,
5367
+ silent: true
5368
+ });
5369
+ const maxCheckSizeBytes = 1024 * 1024;
5370
+ const maxChecksCount = 1e3;
5371
+ /**
5372
+ * Tool to list all checks from .rulesync/checks/*.md
5373
+ */
5374
+ async function listChecks() {
5375
+ const checksDir = (0, node_path.join)(process.cwd(), require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH);
5376
+ try {
5377
+ const mdFiles = (await require_import.listDirectoryFiles(checksDir)).filter((file) => file.endsWith(".md"));
5378
+ return (await Promise.all(mdFiles.map(async (file) => {
5379
+ try {
5380
+ const check = await require_import.RulesyncCheck.fromFile({
5381
+ relativeFilePath: file,
5382
+ validate: true
5383
+ });
5384
+ return {
5385
+ relativePathFromCwd: (0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, file),
5386
+ frontmatter: check.getFrontmatter()
5387
+ };
5388
+ } catch (error) {
5389
+ logger$4.error(`Failed to read check file ${file}: ${require_import.formatError(error)}`);
5390
+ return null;
5391
+ }
5392
+ }))).filter((check) => check !== null);
5393
+ } catch (error) {
5394
+ logger$4.error(`Failed to read checks directory (${require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH}): ${require_import.formatError(error)}`);
5395
+ return [];
5396
+ }
5397
+ }
5398
+ /**
5399
+ * Tool to get detailed information about a specific check
5400
+ */
5401
+ async function getCheck({ relativePathFromCwd }) {
5402
+ require_import.checkPathTraversal({
5403
+ relativePath: relativePathFromCwd,
5404
+ intendedRootDir: process.cwd()
5405
+ });
5406
+ const filename = (0, node_path.basename)(relativePathFromCwd);
5407
+ try {
5408
+ const check = await require_import.RulesyncCheck.fromFile({
5409
+ relativeFilePath: filename,
5410
+ validate: true
5411
+ });
5412
+ return {
5413
+ relativePathFromCwd: (0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, filename),
5414
+ frontmatter: check.getFrontmatter(),
5415
+ body: check.getBody()
5416
+ };
5417
+ } catch (error) {
5418
+ throw new Error(`Failed to read check file ${relativePathFromCwd}: ${require_import.formatError(error)}`, { cause: error });
5419
+ }
5420
+ }
5421
+ /**
5422
+ * Tool to create or update a check (upsert operation)
5423
+ */
5424
+ async function putCheck({ relativePathFromCwd, frontmatter, body }) {
5425
+ require_import.checkPathTraversal({
5426
+ relativePath: relativePathFromCwd,
5427
+ intendedRootDir: process.cwd()
5428
+ });
5429
+ const filename = (0, node_path.basename)(relativePathFromCwd);
5430
+ const estimatedSize = JSON.stringify(frontmatter).length + body.length;
5431
+ if (estimatedSize > maxCheckSizeBytes) throw new Error(`Check size ${estimatedSize} bytes exceeds maximum ${maxCheckSizeBytes} bytes (1MB) for ${relativePathFromCwd}`);
5432
+ try {
5433
+ const existingChecks = await listChecks();
5434
+ if (!existingChecks.some((check) => check.relativePathFromCwd === (0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, filename)) && existingChecks.length >= maxChecksCount) throw new Error(`Maximum number of checks (${maxChecksCount}) reached in ${require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH}`);
5435
+ const check = new require_import.RulesyncCheck({
5436
+ outputRoot: process.cwd(),
5437
+ relativeDirPath: require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5438
+ relativeFilePath: filename,
5439
+ frontmatter,
5440
+ body,
5441
+ validate: true
5442
+ });
5443
+ await require_import.ensureDir((0, node_path.join)(process.cwd(), require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH));
5444
+ await require_import.writeFileContent(check.getFilePath(), check.getFileContent());
5445
+ return {
5446
+ relativePathFromCwd: (0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, filename),
5447
+ frontmatter: check.getFrontmatter(),
5448
+ body: check.getBody()
5449
+ };
5450
+ } catch (error) {
5451
+ throw new Error(`Failed to write check file ${relativePathFromCwd}: ${require_import.formatError(error)}`, { cause: error });
5452
+ }
5453
+ }
5454
+ /**
5455
+ * Tool to delete a check
5456
+ */
5457
+ async function deleteCheck({ relativePathFromCwd }) {
5458
+ require_import.checkPathTraversal({
5459
+ relativePath: relativePathFromCwd,
5460
+ intendedRootDir: process.cwd()
5461
+ });
5462
+ const filename = (0, node_path.basename)(relativePathFromCwd);
5463
+ const fullPath = (0, node_path.join)(process.cwd(), require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, filename);
5464
+ try {
5465
+ await require_import.removeFile(fullPath);
5466
+ return { relativePathFromCwd: (0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, filename) };
5467
+ } catch (error) {
5468
+ throw new Error(`Failed to delete check file ${relativePathFromCwd}: ${require_import.formatError(error)}`, { cause: error });
5469
+ }
5470
+ }
5471
+ /**
5472
+ * Schema for check-related tool parameters
5473
+ */
5474
+ const checkToolSchemas = {
5475
+ listChecks: zod_mini.z.object({}),
5476
+ getCheck: zod_mini.z.object({ relativePathFromCwd: zod_mini.z.string() }),
5477
+ putCheck: zod_mini.z.object({
5478
+ relativePathFromCwd: zod_mini.z.string(),
5479
+ frontmatter: require_import.RulesyncCheckFrontmatterSchema,
5480
+ body: zod_mini.z.string()
5481
+ }),
5482
+ deleteCheck: zod_mini.z.object({ relativePathFromCwd: zod_mini.z.string() })
5483
+ };
5484
+ /**
5485
+ * Tool definitions for check-related operations
5486
+ */
5487
+ const checkTools = {
5488
+ listChecks: {
5489
+ name: "listChecks",
5490
+ description: `List all checks from ${(0, node_path.join)(require_import.RULESYNC_CHECKS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
5491
+ parameters: checkToolSchemas.listChecks,
5492
+ execute: async () => {
5493
+ const output = { checks: await listChecks() };
5494
+ return JSON.stringify(output, null, 2);
5495
+ }
5496
+ },
5497
+ getCheck: {
5498
+ name: "getCheck",
5499
+ description: "Get detailed information about a specific check. relativePathFromCwd parameter is required.",
5500
+ parameters: checkToolSchemas.getCheck,
5501
+ execute: async (args) => {
5502
+ const result = await getCheck({ relativePathFromCwd: args.relativePathFromCwd });
5503
+ return JSON.stringify(result, null, 2);
5504
+ }
5505
+ },
5506
+ putCheck: {
5507
+ name: "putCheck",
5508
+ description: "Create or update a check (upsert operation). relativePathFromCwd, frontmatter, and body parameters are required.",
5509
+ parameters: checkToolSchemas.putCheck,
5510
+ execute: async (args) => {
5511
+ const result = await putCheck({
5512
+ relativePathFromCwd: args.relativePathFromCwd,
5513
+ frontmatter: args.frontmatter,
5514
+ body: args.body
5515
+ });
5516
+ return JSON.stringify(result, null, 2);
5517
+ }
5518
+ },
5519
+ deleteCheck: {
5520
+ name: "deleteCheck",
5521
+ description: "Delete a check file. relativePathFromCwd parameter is required.",
5522
+ parameters: checkToolSchemas.deleteCheck,
5523
+ execute: async (args) => {
5524
+ const result = await deleteCheck({ relativePathFromCwd: args.relativePathFromCwd });
5525
+ return JSON.stringify(result, null, 2);
5526
+ }
5527
+ }
5528
+ };
5529
+ //#endregion
4427
5530
  //#region src/mcp/commands.ts
4428
5531
  const logger$3 = new require_import.ConsoleLogger({
4429
5532
  verbose: false,
@@ -4675,6 +5778,7 @@ function buildSuccessResponse$2(params) {
4675
5778
  skillsCount: convertResult.skillsCount,
4676
5779
  hooksCount: convertResult.hooksCount,
4677
5780
  permissionsCount: convertResult.permissionsCount,
5781
+ checksCount: convertResult.checksCount,
4678
5782
  totalCount
4679
5783
  },
4680
5784
  config: {
@@ -4785,6 +5889,7 @@ function buildSuccessResponse$1(params) {
4785
5889
  skillsCount: generateResult.skillsCount,
4786
5890
  hooksCount: generateResult.hooksCount,
4787
5891
  permissionsCount: generateResult.permissionsCount,
5892
+ checksCount: generateResult.checksCount,
4788
5893
  totalCount
4789
5894
  },
4790
5895
  config: {
@@ -5070,6 +6175,7 @@ function buildSuccessResponse(params) {
5070
6175
  skillsCount: importResult.skillsCount,
5071
6176
  hooksCount: importResult.hooksCount,
5072
6177
  permissionsCount: importResult.permissionsCount,
6178
+ checksCount: importResult.checksCount,
5073
6179
  totalCount
5074
6180
  },
5075
6181
  config: {
@@ -5855,6 +6961,7 @@ const rulesyncFeatureSchema = zod_mini.z.enum([
5855
6961
  "command",
5856
6962
  "subagent",
5857
6963
  "skill",
6964
+ "check",
5858
6965
  "ignore",
5859
6966
  "mcp",
5860
6967
  "permissions",
@@ -5911,6 +7018,12 @@ const supportedOperationsByFeature = {
5911
7018
  "put",
5912
7019
  "delete"
5913
7020
  ],
7021
+ check: [
7022
+ "list",
7023
+ "get",
7024
+ "put",
7025
+ "delete"
7026
+ ],
5914
7027
  ignore: [
5915
7028
  "get",
5916
7029
  "put",
@@ -5949,6 +7062,7 @@ function parseFrontmatter({ feature, frontmatter }) {
5949
7062
  case "command": return require_import.RulesyncCommandFrontmatterSchema.parse(frontmatter);
5950
7063
  case "subagent": return require_import.RulesyncSubagentFrontmatterSchema.parse(frontmatter);
5951
7064
  case "skill": return require_import.RulesyncSkillFrontmatterSchema.parse(frontmatter);
7065
+ case "check": return require_import.RulesyncCheckFrontmatterSchema.parse(frontmatter);
5952
7066
  }
5953
7067
  }
5954
7068
  function ensureBody({ body, feature, operation }) {
@@ -6012,6 +7126,19 @@ function executeSkill(parsed) {
6012
7126
  });
6013
7127
  return skillTools.deleteSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
6014
7128
  }
7129
+ function executeCheck(parsed) {
7130
+ if (parsed.operation === "list") return checkTools.listChecks.execute();
7131
+ if (parsed.operation === "get") return checkTools.getCheck.execute({ relativePathFromCwd: requireTargetPath(parsed) });
7132
+ if (parsed.operation === "put") return checkTools.putCheck.execute({
7133
+ relativePathFromCwd: requireTargetPath(parsed),
7134
+ frontmatter: parseFrontmatter({
7135
+ feature: "check",
7136
+ frontmatter: parsed.frontmatter ?? {}
7137
+ }),
7138
+ body: ensureBody(parsed)
7139
+ });
7140
+ return checkTools.deleteCheck.execute({ relativePathFromCwd: requireTargetPath(parsed) });
7141
+ }
6015
7142
  function executeIgnore(parsed) {
6016
7143
  if (parsed.operation === "get") return ignoreTools.getIgnoreFile.execute();
6017
7144
  if (parsed.operation === "put") return ignoreTools.putIgnoreFile.execute({ content: requireContent({
@@ -6060,6 +7187,7 @@ const featureExecutors = {
6060
7187
  command: executeCommand,
6061
7188
  subagent: executeSubagent,
6062
7189
  skill: executeSkill,
7190
+ check: executeCheck,
6063
7191
  ignore: executeIgnore,
6064
7192
  mcp: executeMcp,
6065
7193
  permissions: executePermissions,
@@ -6070,7 +7198,7 @@ const featureExecutors = {
6070
7198
  };
6071
7199
  const rulesyncTool = {
6072
7200
  name: "rulesyncTool",
6073
- description: "Manage Rulesync files through a single MCP tool. Features: rule/command/subagent/skill support list/get/put/delete; ignore/mcp/permissions/hooks support get/put/delete only; generate supports run only; import supports run only; convert supports run only. Parameters: list requires no targetPathFromCwd (lists all items); get/delete require targetPathFromCwd; put requires targetPathFromCwd, frontmatter, and body (or content for ignore/mcp/permissions/hooks); generate/run uses generateOptions to configure generation; import/run uses importOptions to configure import; convert/run uses convertOptions to configure conversion.",
7201
+ description: "Manage Rulesync files through a single MCP tool. Features: rule/command/subagent/skill/check support list/get/put/delete; ignore/mcp/permissions/hooks support get/put/delete only; generate supports run only; import supports run only; convert supports run only. Parameters: list requires no targetPathFromCwd (lists all items); get/delete require targetPathFromCwd; put requires targetPathFromCwd, frontmatter, and body (or content for ignore/mcp/permissions/hooks); generate/run uses generateOptions to configure generation; import/run uses importOptions to configure import; convert/run uses convertOptions to configure conversion.",
6074
7202
  parameters: rulesyncToolSchema,
6075
7203
  execute: async (args) => {
6076
7204
  const parsed = rulesyncToolSchema.parse(args);
@@ -6556,7 +7684,7 @@ function wrapCommand$1({ name, errorCode, handler, getVersion, loggerFactory = c
6556
7684
  }
6557
7685
  //#endregion
6558
7686
  //#region src/cli/index.ts
6559
- const getVersion = () => "12.0.0";
7687
+ const getVersion = () => "14.0.0";
6560
7688
  function wrapCommand(name, errorCode, handler) {
6561
7689
  return wrapCommand$1({
6562
7690
  name,