rulesync 8.31.0 → 8.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -101,7 +101,7 @@ import {
101
101
  toolSkillFactories,
102
102
  toolSubagentFactories,
103
103
  writeFileContent
104
- } from "../chunk-NAO27T2K.js";
104
+ } from "../chunk-3SMTK3M3.js";
105
105
 
106
106
  // src/cli/index.ts
107
107
  import { Command } from "commander";
@@ -1154,8 +1154,11 @@ function logFeatureResult(logger5, params) {
1154
1154
  }
1155
1155
  }
1156
1156
  }
1157
- async function generateCommand(logger5, options) {
1158
- const { baseDir, outputRoots, ...rest } = options;
1157
+ function resolveOutputRoots({
1158
+ logger: logger5,
1159
+ baseDir,
1160
+ outputRoots
1161
+ }) {
1159
1162
  if (baseDir !== void 0) {
1160
1163
  logger5.warn(
1161
1164
  "--base-dir is deprecated; use --output-roots instead. It will be removed in a future major release."
@@ -1167,6 +1170,53 @@ async function generateCommand(logger5, options) {
1167
1170
  `Both '--output-roots' and '--base-dir' were provided with differing values; using '--output-roots' (${JSON.stringify(outputRoots)}) and ignoring '--base-dir' (${JSON.stringify(baseDir)}).`
1168
1171
  );
1169
1172
  }
1173
+ return outputRootsResolved;
1174
+ }
1175
+ var FEATURE_DEBUG_MESSAGES = {
1176
+ ignore: "Generating ignore files...",
1177
+ mcp: "Generating MCP files...",
1178
+ commands: "Generating command files...",
1179
+ subagents: "Generating subagent files...",
1180
+ skills: "Generating skill files...",
1181
+ hooks: "Generating hooks...",
1182
+ rules: "Generating rule files..."
1183
+ };
1184
+ var FEATURE_DEBUG_ORDER = [
1185
+ "ignore",
1186
+ "mcp",
1187
+ "commands",
1188
+ "subagents",
1189
+ "skills",
1190
+ "hooks",
1191
+ "rules"
1192
+ ];
1193
+ function logFeatureDebugMessages(logger5, features) {
1194
+ for (const feature of FEATURE_DEBUG_ORDER) {
1195
+ if (features.includes(feature)) {
1196
+ logger5.debug(FEATURE_DEBUG_MESSAGES[feature] ?? "");
1197
+ }
1198
+ }
1199
+ }
1200
+ function buildSummaryParts(result) {
1201
+ const summarySpecs = [
1202
+ { count: result.rulesCount, label: "rules" },
1203
+ { count: result.ignoreCount, label: "ignore files" },
1204
+ { count: result.mcpCount, label: "MCP files" },
1205
+ { count: result.commandsCount, label: "commands" },
1206
+ { count: result.subagentsCount, label: "subagents" },
1207
+ { count: result.skillsCount, label: "skills" },
1208
+ { count: result.hooksCount, label: "hooks" },
1209
+ { count: result.permissionsCount, label: "permissions" }
1210
+ ];
1211
+ const parts = [];
1212
+ for (const { count, label } of summarySpecs) {
1213
+ if (count > 0) parts.push(`${count} ${label}`);
1214
+ }
1215
+ return parts;
1216
+ }
1217
+ async function generateCommand(logger5, options) {
1218
+ const { baseDir, outputRoots, ...rest } = options;
1219
+ const outputRootsResolved = resolveOutputRoots({ logger: logger5, baseDir, outputRoots });
1170
1220
  const config = await ConfigResolver.resolve(
1171
1221
  { ...rest, outputRoots: outputRootsResolved },
1172
1222
  { logger: logger5 }
@@ -1183,27 +1233,7 @@ async function generateCommand(logger5, options) {
1183
1233
  }
1184
1234
  logger5.debug(`Output roots: ${config.getOutputRoots().join(", ")}`);
1185
1235
  const features = config.getFeatures();
1186
- if (features.includes("ignore")) {
1187
- logger5.debug("Generating ignore files...");
1188
- }
1189
- if (features.includes("mcp")) {
1190
- logger5.debug("Generating MCP files...");
1191
- }
1192
- if (features.includes("commands")) {
1193
- logger5.debug("Generating command files...");
1194
- }
1195
- if (features.includes("subagents")) {
1196
- logger5.debug("Generating subagent files...");
1197
- }
1198
- if (features.includes("skills")) {
1199
- logger5.debug("Generating skill files...");
1200
- }
1201
- if (features.includes("hooks")) {
1202
- logger5.debug("Generating hooks...");
1203
- }
1204
- if (features.includes("rules")) {
1205
- logger5.debug("Generating rule files...");
1206
- }
1236
+ logFeatureDebugMessages(logger5, features);
1207
1237
  const result = await generate({ config, logger: logger5 });
1208
1238
  const totalGenerated = calculateTotalCount(result);
1209
1239
  const featureResults = {
@@ -1256,15 +1286,7 @@ async function generateCommand(logger5, options) {
1256
1286
  logger5.info(`\u2713 All files are up to date (${enabledFeatures})`);
1257
1287
  return;
1258
1288
  }
1259
- const parts = [];
1260
- if (result.rulesCount > 0) parts.push(`${result.rulesCount} rules`);
1261
- if (result.ignoreCount > 0) parts.push(`${result.ignoreCount} ignore files`);
1262
- if (result.mcpCount > 0) parts.push(`${result.mcpCount} MCP files`);
1263
- if (result.commandsCount > 0) parts.push(`${result.commandsCount} commands`);
1264
- if (result.subagentsCount > 0) parts.push(`${result.subagentsCount} subagents`);
1265
- if (result.skillsCount > 0) parts.push(`${result.skillsCount} skills`);
1266
- if (result.hooksCount > 0) parts.push(`${result.hooksCount} hooks`);
1267
- if (result.permissionsCount > 0) parts.push(`${result.permissionsCount} permissions`);
1289
+ const parts = buildSummaryParts(result);
1268
1290
  if (isPreview) {
1269
1291
  logger5.info(`${modePrefix} Would write ${totalGenerated} file(s) total (${parts.join(" + ")})`);
1270
1292
  } else {
@@ -2211,7 +2233,7 @@ function findApmLockDependency(lock, repoUrl) {
2211
2233
 
2212
2234
  // src/lib/apm/apm-manifest.ts
2213
2235
  import { join as join5 } from "path";
2214
- import { dump as dump2, load as load2 } from "js-yaml";
2236
+ import { load as load2 } from "js-yaml";
2215
2237
  import { optional as optional2, z as z5 } from "zod/mini";
2216
2238
  var APM_MANIFEST_FILE_NAME = "apm.yml";
2217
2239
  var ApmObjectDependencySchema = z5.looseObject({
@@ -2419,34 +2441,7 @@ async function installApm(params) {
2419
2441
  }
2420
2442
  const existingLock = await readApmLock(projectRoot);
2421
2443
  if (options.frozen) {
2422
- if (!existingLock) {
2423
- throw new Error(
2424
- "Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
2425
- );
2426
- }
2427
- const missing = manifest.dependencies.filter(
2428
- (dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
2429
- );
2430
- if (missing.length > 0) {
2431
- const names = missing.map((d) => d.gitUrl).join(", ");
2432
- throw new Error(
2433
- `Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
2434
- );
2435
- }
2436
- const drifted = manifest.dependencies.filter((dep) => {
2437
- if (dep.ref === void 0) return false;
2438
- const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
2439
- return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
2440
- });
2441
- if (drifted.length > 0) {
2442
- const names = drifted.map((d) => {
2443
- const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
2444
- return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
2445
- }).join(", ");
2446
- throw new Error(
2447
- `Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
2448
- );
2449
- }
2444
+ assertFrozenLockCoversManifest({ existingLock, dependencies: manifest.dependencies });
2450
2445
  }
2451
2446
  const token = GitHubClient.resolveToken(options.token);
2452
2447
  const client = new GitHubClient({ token });
@@ -2501,30 +2496,7 @@ async function installApm(params) {
2501
2496
  }
2502
2497
  }
2503
2498
  if (existingLock) {
2504
- const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
2505
- const toDelete = [];
2506
- for (const prev of existingLock.dependencies) {
2507
- for (const deployed of prev.deployed_files) {
2508
- if (!newDeployedFiles.has(deployed)) {
2509
- toDelete.push(deployed);
2510
- }
2511
- }
2512
- }
2513
- for (const relativePath of toDelete) {
2514
- if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
2515
- logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
2516
- continue;
2517
- }
2518
- try {
2519
- checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
2520
- } catch {
2521
- logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
2522
- continue;
2523
- }
2524
- const absolute = join6(projectRoot, relativePath);
2525
- await removeFile(absolute);
2526
- logger5.debug(`Removed stale apm file: ${relativePath}`);
2527
- }
2499
+ await removeStaleApmFiles({ existingLock, newLock, projectRoot, logger: logger5 });
2528
2500
  }
2529
2501
  if (!frozen) {
2530
2502
  newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -2543,6 +2515,64 @@ async function installApm(params) {
2543
2515
  failedDependencyCount: failedCount
2544
2516
  };
2545
2517
  }
2518
+ function assertFrozenLockCoversManifest(params) {
2519
+ const { existingLock, dependencies } = params;
2520
+ if (!existingLock) {
2521
+ throw new Error(
2522
+ "Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
2523
+ );
2524
+ }
2525
+ const missing = dependencies.filter(
2526
+ (dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
2527
+ );
2528
+ if (missing.length > 0) {
2529
+ const names = missing.map((d) => d.gitUrl).join(", ");
2530
+ throw new Error(
2531
+ `Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
2532
+ );
2533
+ }
2534
+ const drifted = dependencies.filter((dep) => {
2535
+ if (dep.ref === void 0) return false;
2536
+ const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
2537
+ return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
2538
+ });
2539
+ if (drifted.length > 0) {
2540
+ const names = drifted.map((d) => {
2541
+ const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
2542
+ return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
2543
+ }).join(", ");
2544
+ throw new Error(
2545
+ `Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
2546
+ );
2547
+ }
2548
+ }
2549
+ async function removeStaleApmFiles(params) {
2550
+ const { existingLock, newLock, projectRoot, logger: logger5 } = params;
2551
+ const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
2552
+ const toDelete = [];
2553
+ for (const prev of existingLock.dependencies) {
2554
+ for (const deployed of prev.deployed_files) {
2555
+ if (!newDeployedFiles.has(deployed)) {
2556
+ toDelete.push(deployed);
2557
+ }
2558
+ }
2559
+ }
2560
+ for (const relativePath of toDelete) {
2561
+ if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
2562
+ logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
2563
+ continue;
2564
+ }
2565
+ try {
2566
+ checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
2567
+ } catch {
2568
+ logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
2569
+ continue;
2570
+ }
2571
+ const absolute = join6(projectRoot, relativePath);
2572
+ await removeFile(absolute);
2573
+ logger5.debug(`Removed stale apm file: ${relativePath}`);
2574
+ }
2575
+ }
2546
2576
  async function installDependency(params) {
2547
2577
  const { dep, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
2548
2578
  const repoUrl = canonicalRepoUrl(dep);
@@ -2571,58 +2601,25 @@ async function installDependency(params) {
2571
2601
  logger: logger5
2572
2602
  });
2573
2603
  if (files.length === 0) continue;
2574
- for (const file of files) {
2575
- if (file.size > MAX_FILE_SIZE) {
2576
- logger5.warn(
2577
- `Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
2578
- );
2579
- continue;
2580
- }
2581
- const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
2582
- if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
2583
- logger5.warn(
2584
- `Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`
2585
- );
2586
- continue;
2587
- }
2588
- const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
2589
- checkPathTraversal({
2590
- relativePath: deployRelative,
2591
- intendedRootDir: projectRoot
2592
- });
2593
- const content = await withSemaphore(
2594
- semaphore,
2595
- () => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
2596
- );
2597
- const byteLength = Buffer.byteLength(content, "utf8");
2598
- if (byteLength > MAX_FILE_SIZE) {
2599
- logger5.warn(
2600
- `Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
2601
- );
2602
- continue;
2603
- }
2604
- deployed.push({ path: deployRelative, content });
2605
- if (!frozen) {
2606
- await writeFileContent(join6(projectRoot, deployRelative), content);
2607
- }
2608
- }
2604
+ await collectPrimitiveDeployments({
2605
+ dep,
2606
+ client,
2607
+ semaphore,
2608
+ projectRoot,
2609
+ primitive,
2610
+ remoteBase,
2611
+ files,
2612
+ resolvedSha,
2613
+ repoUrl,
2614
+ frozen,
2615
+ deployed,
2616
+ logger: logger5
2617
+ });
2609
2618
  }
2610
2619
  deployed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
2611
2620
  const deployedFiles = deployed.map((d) => d.path);
2612
2621
  const contentHash = computeContentHash(deployed);
2613
- if (frozen && locked?.content_hash) {
2614
- if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
2615
- if (locked.content_hash !== contentHash) {
2616
- throw new Error(
2617
- `content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
2618
- );
2619
- }
2620
- } else {
2621
- logger5.debug(
2622
- `Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
2623
- );
2624
- }
2625
- }
2622
+ assertFrozenContentHashMatches({ frozen, locked, contentHash, repoUrl, logger: logger5 });
2626
2623
  if (frozen) {
2627
2624
  for (const { path: deployRelative, content } of deployed) {
2628
2625
  await writeFileContent(join6(projectRoot, deployRelative), content);
@@ -2643,6 +2640,71 @@ async function installDependency(params) {
2643
2640
  logger5.info(`Installed ${deployedFiles.length} file(s) from ${repoUrl}@${shortSha(resolvedSha)}`);
2644
2641
  return { lockEntry, deployedFiles };
2645
2642
  }
2643
+ async function collectPrimitiveDeployments(params) {
2644
+ const {
2645
+ dep,
2646
+ client,
2647
+ semaphore,
2648
+ projectRoot,
2649
+ primitive,
2650
+ remoteBase,
2651
+ files,
2652
+ resolvedSha,
2653
+ repoUrl,
2654
+ frozen,
2655
+ deployed,
2656
+ logger: logger5
2657
+ } = params;
2658
+ for (const file of files) {
2659
+ if (file.size > MAX_FILE_SIZE) {
2660
+ logger5.warn(
2661
+ `Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
2662
+ );
2663
+ continue;
2664
+ }
2665
+ const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
2666
+ if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
2667
+ logger5.warn(`Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`);
2668
+ continue;
2669
+ }
2670
+ const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
2671
+ checkPathTraversal({
2672
+ relativePath: deployRelative,
2673
+ intendedRootDir: projectRoot
2674
+ });
2675
+ const content = await withSemaphore(
2676
+ semaphore,
2677
+ () => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
2678
+ );
2679
+ const byteLength = Buffer.byteLength(content, "utf8");
2680
+ if (byteLength > MAX_FILE_SIZE) {
2681
+ logger5.warn(
2682
+ `Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
2683
+ );
2684
+ continue;
2685
+ }
2686
+ deployed.push({ path: deployRelative, content });
2687
+ if (!frozen) {
2688
+ await writeFileContent(join6(projectRoot, deployRelative), content);
2689
+ }
2690
+ }
2691
+ }
2692
+ function assertFrozenContentHashMatches(params) {
2693
+ const { frozen, locked, contentHash, repoUrl, logger: logger5 } = params;
2694
+ if (frozen && locked?.content_hash) {
2695
+ if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
2696
+ if (locked.content_hash !== contentHash) {
2697
+ throw new Error(
2698
+ `content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
2699
+ );
2700
+ }
2701
+ } else {
2702
+ logger5.debug(
2703
+ `Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
2704
+ );
2705
+ }
2706
+ }
2707
+ }
2646
2708
  function computeContentHash(files) {
2647
2709
  const hash = createHash("sha256");
2648
2710
  for (const { path: path2, content } of files) {
@@ -2685,7 +2747,7 @@ import { basename, join as join9, posix as posix3 } from "path";
2685
2747
  import { Semaphore as Semaphore3 } from "es-toolkit/promise";
2686
2748
 
2687
2749
  // src/lib/gh/gh-frontmatter.ts
2688
- import { dump as dump3, load as load3 } from "js-yaml";
2750
+ import { dump as dump2, load as load3 } from "js-yaml";
2689
2751
  var FRONTMATTER_FENCE = "---";
2690
2752
  function injectSourceMetadata(params) {
2691
2753
  const { content, source, repository, ref } = params;
@@ -2700,7 +2762,7 @@ function injectSourceMetadata(params) {
2700
2762
  } else if (content === FRONTMATTER_FENCE) {
2701
2763
  openFenceLen = 3;
2702
2764
  } else {
2703
- const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
2765
+ const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
2704
2766
  return `${FRONTMATTER_FENCE}
2705
2767
  ${yaml2}${FRONTMATTER_FENCE}
2706
2768
  ${content}`;
@@ -2727,7 +2789,7 @@ ${content}`;
2727
2789
  throw new Error("invalid frontmatter");
2728
2790
  }
2729
2791
  if (loaded === null || loaded === void 0) {
2730
- const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
2792
+ const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
2731
2793
  return `${FRONTMATTER_FENCE}
2732
2794
  ${yaml2}${FRONTMATTER_FENCE}
2733
2795
  ${rest}`;
@@ -2740,7 +2802,7 @@ ${rest}`;
2740
2802
  ...existing,
2741
2803
  ...provenance
2742
2804
  };
2743
- const yaml = dump3(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
2805
+ const yaml = dump2(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
2744
2806
  return `${FRONTMATTER_FENCE}
2745
2807
  ${yaml}${FRONTMATTER_FENCE}
2746
2808
  ${rest}`;
@@ -2748,7 +2810,7 @@ ${rest}`;
2748
2810
 
2749
2811
  // src/lib/gh/gh-lock.ts
2750
2812
  import { join as join7 } from "path";
2751
- import { dump as dump4, load as load4 } from "js-yaml";
2813
+ import { dump as dump3, load as load4 } from "js-yaml";
2752
2814
  import { optional as optional3, refine as refine2, z as z6 } from "zod/mini";
2753
2815
  var GH_LOCKFILE_FILE_NAME = "rulesync-gh.lock.yaml";
2754
2816
  var GH_LOCKFILE_VERSION = "1";
@@ -2820,7 +2882,7 @@ async function writeGhLock(params) {
2820
2882
  await writeFileContent(path2, content);
2821
2883
  }
2822
2884
  function serializeGhLock(lock) {
2823
- return dump4(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
2885
+ return dump3(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
2824
2886
  }
2825
2887
  function findGhLockInstallation(lock, params) {
2826
2888
  const target = params.source.toLowerCase();
@@ -2871,39 +2933,7 @@ async function installGh(params) {
2871
2933
  if (sources.length === 0) {
2872
2934
  return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
2873
2935
  }
2874
- const resolvedSources = sources.map((entry) => {
2875
- const parsed = parseSource(entry.source);
2876
- if (parsed.provider !== "github") {
2877
- throw new Error(
2878
- `--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
2879
- );
2880
- }
2881
- if (entry.transport !== void 0 && entry.transport !== "github") {
2882
- throw new Error(
2883
- `--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
2884
- );
2885
- }
2886
- if (entry.path !== void 0) {
2887
- throw new Error(
2888
- `--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
2889
- );
2890
- }
2891
- const agent = entry.agent ?? "github-copilot";
2892
- if (!GH_AGENTS.includes(agent)) {
2893
- throw new Error(
2894
- `--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
2895
- );
2896
- }
2897
- const scope = entry.scope ?? "project";
2898
- return {
2899
- entry,
2900
- owner: parsed.owner,
2901
- repo: parsed.repo,
2902
- ref: entry.ref ?? parsed.ref,
2903
- agent,
2904
- scope
2905
- };
2906
- });
2936
+ const resolvedSources = sources.map(resolveGhSource);
2907
2937
  const existingLock = await readGhLock(projectRoot);
2908
2938
  const frozen = options.frozen ?? false;
2909
2939
  const update = options.update ?? false;
@@ -2913,38 +2943,7 @@ async function installGh(params) {
2913
2943
  );
2914
2944
  }
2915
2945
  if (frozen && existingLock) {
2916
- const uncovered = [];
2917
- for (const rs of resolvedSources) {
2918
- const hasAny = existingLock.installations.some(
2919
- (i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
2920
- );
2921
- if (!hasAny) {
2922
- uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
2923
- }
2924
- }
2925
- if (uncovered.length > 0) {
2926
- throw new Error(
2927
- `Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
2928
- );
2929
- }
2930
- const drifted = [];
2931
- for (const rs of resolvedSources) {
2932
- if (!rs.ref) continue;
2933
- const matches = existingLock.installations.filter(
2934
- (i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
2935
- );
2936
- for (const m of matches) {
2937
- if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
2938
- drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
2939
- break;
2940
- }
2941
- }
2942
- }
2943
- if (drifted.length > 0) {
2944
- throw new Error(
2945
- `Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
2946
- );
2947
- }
2946
+ assertFrozenLockCoversSources({ existingLock, resolvedSources });
2948
2947
  }
2949
2948
  const token = GitHubClient.resolveToken(options.token);
2950
2949
  const client = new GitHubClient({ token });
@@ -2980,49 +2979,11 @@ async function installGh(params) {
2980
2979
  })
2981
2980
  );
2982
2981
  if (frozen) {
2983
- for (const result of results) {
2984
- if (result.status !== "ok") continue;
2985
- for (const inst of result.installations) {
2986
- for (const d of inst.deployed) {
2987
- await writeFileContent(d.absolutePath, d.content);
2988
- }
2989
- }
2990
- }
2991
- }
2992
- let totalInstalled = 0;
2993
- let failedCount = 0;
2994
- for (const result of results) {
2995
- if (result.status === "ok") {
2996
- for (const inst of result.installations) {
2997
- newLock.installations.push(inst.installation);
2998
- }
2999
- totalInstalled += result.installations.length;
3000
- } else {
3001
- failedCount += 1;
3002
- for (const preserved of result.preserved) {
3003
- newLock.installations.push(preserved);
3004
- }
3005
- }
2982
+ await writeDeferredFrozenFiles(results);
3006
2983
  }
2984
+ const { totalInstalled, failedCount } = aggregateSourceResults({ results, newLock });
3007
2985
  if (existingLock) {
3008
- const newDeployed = /* @__PURE__ */ new Set();
3009
- for (const inst of newLock.installations) {
3010
- for (const file of inst.deployed_files) {
3011
- newDeployed.add(`${inst.scope}::${file}`);
3012
- }
3013
- }
3014
- for (const prev of existingLock.installations) {
3015
- for (const deployed of prev.deployed_files) {
3016
- const key = `${prev.scope}::${deployed}`;
3017
- if (newDeployed.has(key)) continue;
3018
- await removeStaleFile({
3019
- relativePath: deployed,
3020
- scope: prev.scope === "user" ? "user" : "project",
3021
- projectRoot,
3022
- logger: logger5
3023
- });
3024
- }
3025
- }
2986
+ await removeStaleGhFiles({ existingLock, newLock, projectRoot, logger: logger5 });
3026
2987
  }
3027
2988
  if (!frozen) {
3028
2989
  newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
@@ -3041,80 +3002,152 @@ async function installGh(params) {
3041
3002
  failedSourceCount: failedCount
3042
3003
  };
3043
3004
  }
3044
- async function installSource(params) {
3045
- const { rs, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
3046
- const { entry, owner, repo, agent, scope } = rs;
3047
- const sourceKey = entry.source;
3048
- let resolvedRef;
3049
- let usedTag = false;
3050
- if (rs.ref) {
3051
- resolvedRef = rs.ref;
3052
- } else {
3053
- try {
3054
- const release = await client.getLatestRelease(owner, repo);
3055
- resolvedRef = release.tag_name;
3056
- usedTag = true;
3057
- } catch (error) {
3058
- if (is404(error)) {
3059
- resolvedRef = await client.getDefaultBranch(owner, repo);
3060
- } else {
3061
- throw error;
3062
- }
3063
- }
3064
- }
3065
- const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
3066
- logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
3067
- let topLevel;
3068
- try {
3069
- topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
3070
- } catch (error) {
3071
- if (is404(error)) {
3072
- logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
3073
- return [];
3074
- }
3075
- throw error;
3005
+ function resolveGhSource(entry) {
3006
+ const parsed = parseSource(entry.source);
3007
+ if (parsed.provider !== "github") {
3008
+ throw new Error(
3009
+ `--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
3010
+ );
3076
3011
  }
3077
- const skillDirs = topLevel.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
3078
- const validatedSkills = [];
3079
- for (const sk of skillDirs) {
3080
- const info = await withSemaphore(
3081
- semaphore,
3082
- () => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
3012
+ if (entry.transport !== void 0 && entry.transport !== "github") {
3013
+ throw new Error(
3014
+ `--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
3083
3015
  );
3084
- if (info) {
3085
- validatedSkills.push(sk);
3016
+ }
3017
+ if (entry.path !== void 0) {
3018
+ throw new Error(
3019
+ `--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
3020
+ );
3021
+ }
3022
+ const agent = entry.agent ?? "github-copilot";
3023
+ if (!GH_AGENTS.includes(agent)) {
3024
+ throw new Error(
3025
+ `--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
3026
+ );
3027
+ }
3028
+ const scope = entry.scope ?? "project";
3029
+ return {
3030
+ entry,
3031
+ owner: parsed.owner,
3032
+ repo: parsed.repo,
3033
+ ref: entry.ref ?? parsed.ref,
3034
+ agent,
3035
+ scope
3036
+ };
3037
+ }
3038
+ function assertFrozenLockCoversSources(params) {
3039
+ const { existingLock, resolvedSources } = params;
3040
+ const uncovered = [];
3041
+ for (const rs of resolvedSources) {
3042
+ const hasAny = existingLock.installations.some(
3043
+ (i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
3044
+ );
3045
+ if (!hasAny) {
3046
+ uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
3086
3047
  }
3087
3048
  }
3088
- let selected = validatedSkills;
3089
- if (entry.skills && entry.skills.length > 0) {
3090
- const requested = new Set(entry.skills);
3091
- selected = validatedSkills.filter((s) => requested.has(s.name));
3092
- const presentNames = new Set(validatedSkills.map((s) => s.name));
3093
- for (const want of entry.skills) {
3094
- if (!presentNames.has(want)) {
3095
- logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
3049
+ if (uncovered.length > 0) {
3050
+ throw new Error(
3051
+ `Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
3052
+ );
3053
+ }
3054
+ const drifted = [];
3055
+ for (const rs of resolvedSources) {
3056
+ if (!rs.ref) continue;
3057
+ const matches = existingLock.installations.filter(
3058
+ (i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
3059
+ );
3060
+ for (const m of matches) {
3061
+ if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
3062
+ drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
3063
+ break;
3096
3064
  }
3097
3065
  }
3098
3066
  }
3099
- if (frozen && existingLock) {
3100
- const missing = [];
3101
- for (const sk of selected) {
3102
- const locked = findGhLockInstallation(existingLock, {
3103
- source: sourceKey,
3104
- agent,
3105
- scope,
3106
- skill: sk.name
3107
- });
3108
- if (!locked) {
3109
- missing.push(sk.name);
3067
+ if (drifted.length > 0) {
3068
+ throw new Error(
3069
+ `Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
3070
+ );
3071
+ }
3072
+ }
3073
+ async function writeDeferredFrozenFiles(results) {
3074
+ for (const result of results) {
3075
+ if (result.status !== "ok") continue;
3076
+ for (const inst of result.installations) {
3077
+ for (const d of inst.deployed) {
3078
+ await writeFileContent(d.absolutePath, d.content);
3110
3079
  }
3111
3080
  }
3112
- if (missing.length > 0) {
3113
- throw new Error(
3114
- `Frozen install failed: rulesync-gh.lock.yaml is missing entries for ${sourceKey} (agent=${agent}, scope=${scope}) skills: ${missing.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
3115
- );
3081
+ }
3082
+ }
3083
+ function aggregateSourceResults(params) {
3084
+ const { results, newLock } = params;
3085
+ let totalInstalled = 0;
3086
+ let failedCount = 0;
3087
+ for (const result of results) {
3088
+ if (result.status === "ok") {
3089
+ for (const inst of result.installations) {
3090
+ newLock.installations.push(inst.installation);
3091
+ }
3092
+ totalInstalled += result.installations.length;
3093
+ } else {
3094
+ failedCount += 1;
3095
+ for (const preserved of result.preserved) {
3096
+ newLock.installations.push(preserved);
3097
+ }
3098
+ }
3099
+ }
3100
+ return { totalInstalled, failedCount };
3101
+ }
3102
+ async function removeStaleGhFiles(params) {
3103
+ const { existingLock, newLock, projectRoot, logger: logger5 } = params;
3104
+ const newDeployed = /* @__PURE__ */ new Set();
3105
+ for (const inst of newLock.installations) {
3106
+ for (const file of inst.deployed_files) {
3107
+ newDeployed.add(`${inst.scope}::${file}`);
3116
3108
  }
3117
3109
  }
3110
+ for (const prev of existingLock.installations) {
3111
+ for (const deployed of prev.deployed_files) {
3112
+ const key = `${prev.scope}::${deployed}`;
3113
+ if (newDeployed.has(key)) continue;
3114
+ await removeStaleFile({
3115
+ relativePath: deployed,
3116
+ scope: prev.scope === "user" ? "user" : "project",
3117
+ projectRoot,
3118
+ logger: logger5
3119
+ });
3120
+ }
3121
+ }
3122
+ }
3123
+ async function installSource(params) {
3124
+ const { rs, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
3125
+ const { entry, owner, repo, agent, scope } = rs;
3126
+ const sourceKey = entry.source;
3127
+ const { resolvedRef, resolvedSha, usedTag } = await resolveGhRef({
3128
+ rs,
3129
+ client,
3130
+ owner,
3131
+ repo,
3132
+ sourceKey,
3133
+ logger: logger5
3134
+ });
3135
+ const validatedSkills = await discoverValidatedSkills({
3136
+ client,
3137
+ semaphore,
3138
+ owner,
3139
+ repo,
3140
+ resolvedSha,
3141
+ sourceKey,
3142
+ logger: logger5
3143
+ });
3144
+ if (validatedSkills === null) {
3145
+ return [];
3146
+ }
3147
+ const selected = selectSkills({ validatedSkills, entry, sourceKey, logger: logger5 });
3148
+ if (frozen && existingLock) {
3149
+ assertFrozenSkillCoverage({ selected, existingLock, sourceKey, agent, scope });
3150
+ }
3118
3151
  const results = [];
3119
3152
  const installRelDir = relativeInstallDirFor({ agent, scope });
3120
3153
  const scopeRoot = scope === "user" ? getHomeDirectory() : projectRoot;
@@ -3136,79 +3169,38 @@ async function installSource(params) {
3136
3169
  ref: resolvedSha,
3137
3170
  semaphore
3138
3171
  });
3139
- const deployed = [];
3140
- for (const file of allFiles) {
3141
- if (file.size > MAX_FILE_SIZE) {
3142
- logger5.warn(
3143
- `Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
3144
- );
3145
- continue;
3146
- }
3147
- const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
3148
- if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
3149
- logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
3150
- continue;
3151
- }
3152
- const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
3153
- checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
3154
- const installAbs = join9(scopeRoot, installRelDir);
3155
- const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
3156
- checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
3157
- let content = await withSemaphore(
3158
- semaphore,
3159
- () => client.getFileContent(owner, repo, file.path, resolvedSha)
3160
- );
3161
- const byteLength = Buffer.byteLength(content, "utf8");
3162
- if (byteLength > MAX_FILE_SIZE) {
3163
- logger5.warn(
3164
- `Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
3165
- );
3166
- continue;
3167
- }
3168
- if (basename(file.path) === SKILL_FILE_NAME2) {
3169
- try {
3170
- content = injectSourceMetadata({
3171
- content,
3172
- source: sourceUrl,
3173
- repository,
3174
- ref: provenanceRef
3175
- });
3176
- } catch {
3177
- logger5.warn(
3178
- `Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
3179
- );
3180
- content = `---
3181
- source: ${sourceUrl}
3182
- repository: ${repository}
3183
- ref: ${provenanceRef}
3184
- ---
3185
- ${content}`;
3186
- }
3187
- }
3188
- const absolutePath = join9(scopeRoot, deployRelative);
3189
- deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
3190
- if (!frozen) {
3191
- await writeFileContent(absolutePath, content);
3192
- }
3193
- }
3172
+ const deployed = await buildSkillDeployment({
3173
+ sk,
3174
+ allFiles,
3175
+ client,
3176
+ semaphore,
3177
+ owner,
3178
+ repo,
3179
+ resolvedSha,
3180
+ installRelDir,
3181
+ scopeRoot,
3182
+ sourceUrl,
3183
+ repository,
3184
+ provenanceRef,
3185
+ sourceKey,
3186
+ frozen,
3187
+ logger: logger5
3188
+ });
3194
3189
  deployed.sort(
3195
3190
  (a, b) => a.relativeToScopeRoot < b.relativeToScopeRoot ? -1 : a.relativeToScopeRoot > b.relativeToScopeRoot ? 1 : 0
3196
3191
  );
3197
3192
  const deployedFiles = deployed.map((d) => d.relativeToScopeRoot);
3198
3193
  const contentHash = computeContentHash2(deployed);
3199
- if (frozen && locked?.content_hash) {
3200
- if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
3201
- if (locked.content_hash !== contentHash) {
3202
- throw new Error(
3203
- `content_hash mismatch for ${sourceKey} skill "${sk.name}" (agent=${agent}, scope=${scope}): lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
3204
- );
3205
- }
3206
- } else {
3207
- logger5.debug(
3208
- `Skipping content_hash integrity check for ${sourceKey} skill "${sk.name}": recorded hash "${locked.content_hash}" was not written by rulesync.`
3209
- );
3210
- }
3211
- }
3194
+ assertFrozenSkillIntegrity({
3195
+ frozen,
3196
+ locked,
3197
+ contentHash,
3198
+ sourceKey,
3199
+ skillName: sk.name,
3200
+ agent,
3201
+ scope,
3202
+ logger: logger5
3203
+ });
3212
3204
  const installation = {
3213
3205
  source: sourceKey,
3214
3206
  owner,
@@ -3232,6 +3224,180 @@ ${content}`;
3232
3224
  }
3233
3225
  return results;
3234
3226
  }
3227
+ async function resolveGhRef(params) {
3228
+ const { rs, client, owner, repo, sourceKey, logger: logger5 } = params;
3229
+ let resolvedRef;
3230
+ let usedTag = false;
3231
+ if (rs.ref) {
3232
+ resolvedRef = rs.ref;
3233
+ } else {
3234
+ try {
3235
+ const release = await client.getLatestRelease(owner, repo);
3236
+ resolvedRef = release.tag_name;
3237
+ usedTag = true;
3238
+ } catch (error) {
3239
+ if (is404(error)) {
3240
+ resolvedRef = await client.getDefaultBranch(owner, repo);
3241
+ } else {
3242
+ throw error;
3243
+ }
3244
+ }
3245
+ }
3246
+ const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
3247
+ logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
3248
+ return { resolvedRef, resolvedSha, usedTag };
3249
+ }
3250
+ async function discoverValidatedSkills(params) {
3251
+ const { client, semaphore, owner, repo, resolvedSha, sourceKey, logger: logger5 } = params;
3252
+ let topLevel;
3253
+ try {
3254
+ topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
3255
+ } catch (error) {
3256
+ if (is404(error)) {
3257
+ logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
3258
+ return null;
3259
+ }
3260
+ throw error;
3261
+ }
3262
+ const skillDirs = topLevel.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
3263
+ const validatedSkills = [];
3264
+ for (const sk of skillDirs) {
3265
+ const info = await withSemaphore(
3266
+ semaphore,
3267
+ () => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
3268
+ );
3269
+ if (info) {
3270
+ validatedSkills.push(sk);
3271
+ }
3272
+ }
3273
+ return validatedSkills;
3274
+ }
3275
+ function selectSkills(params) {
3276
+ const { validatedSkills, entry, sourceKey, logger: logger5 } = params;
3277
+ if (!entry.skills || entry.skills.length === 0) {
3278
+ return validatedSkills;
3279
+ }
3280
+ const requested = new Set(entry.skills);
3281
+ const selected = validatedSkills.filter((s) => requested.has(s.name));
3282
+ const presentNames = new Set(validatedSkills.map((s) => s.name));
3283
+ for (const want of entry.skills) {
3284
+ if (!presentNames.has(want)) {
3285
+ logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
3286
+ }
3287
+ }
3288
+ return selected;
3289
+ }
3290
+ function assertFrozenSkillCoverage(params) {
3291
+ const { selected, existingLock, sourceKey, agent, scope } = params;
3292
+ const missing = [];
3293
+ for (const sk of selected) {
3294
+ const locked = findGhLockInstallation(existingLock, {
3295
+ source: sourceKey,
3296
+ agent,
3297
+ scope,
3298
+ skill: sk.name
3299
+ });
3300
+ if (!locked) {
3301
+ missing.push(sk.name);
3302
+ }
3303
+ }
3304
+ if (missing.length > 0) {
3305
+ throw new Error(
3306
+ `Frozen install failed: rulesync-gh.lock.yaml is missing entries for ${sourceKey} (agent=${agent}, scope=${scope}) skills: ${missing.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
3307
+ );
3308
+ }
3309
+ }
3310
+ async function buildSkillDeployment(params) {
3311
+ const {
3312
+ sk,
3313
+ allFiles,
3314
+ client,
3315
+ semaphore,
3316
+ owner,
3317
+ repo,
3318
+ resolvedSha,
3319
+ installRelDir,
3320
+ scopeRoot,
3321
+ sourceUrl,
3322
+ repository,
3323
+ provenanceRef,
3324
+ sourceKey,
3325
+ frozen,
3326
+ logger: logger5
3327
+ } = params;
3328
+ const deployed = [];
3329
+ for (const file of allFiles) {
3330
+ if (file.size > MAX_FILE_SIZE) {
3331
+ logger5.warn(
3332
+ `Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
3333
+ );
3334
+ continue;
3335
+ }
3336
+ const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
3337
+ if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
3338
+ logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
3339
+ continue;
3340
+ }
3341
+ const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
3342
+ checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
3343
+ const installAbs = join9(scopeRoot, installRelDir);
3344
+ const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
3345
+ checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
3346
+ let content = await withSemaphore(
3347
+ semaphore,
3348
+ () => client.getFileContent(owner, repo, file.path, resolvedSha)
3349
+ );
3350
+ const byteLength = Buffer.byteLength(content, "utf8");
3351
+ if (byteLength > MAX_FILE_SIZE) {
3352
+ logger5.warn(
3353
+ `Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
3354
+ );
3355
+ continue;
3356
+ }
3357
+ if (basename(file.path) === SKILL_FILE_NAME2) {
3358
+ try {
3359
+ content = injectSourceMetadata({
3360
+ content,
3361
+ source: sourceUrl,
3362
+ repository,
3363
+ ref: provenanceRef
3364
+ });
3365
+ } catch {
3366
+ logger5.warn(
3367
+ `Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
3368
+ );
3369
+ content = `---
3370
+ source: ${sourceUrl}
3371
+ repository: ${repository}
3372
+ ref: ${provenanceRef}
3373
+ ---
3374
+ ${content}`;
3375
+ }
3376
+ }
3377
+ const absolutePath = join9(scopeRoot, deployRelative);
3378
+ deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
3379
+ if (!frozen) {
3380
+ await writeFileContent(absolutePath, content);
3381
+ }
3382
+ }
3383
+ return deployed;
3384
+ }
3385
+ function assertFrozenSkillIntegrity(params) {
3386
+ const { frozen, locked, contentHash, sourceKey, skillName, agent, scope, logger: logger5 } = params;
3387
+ if (frozen && locked?.content_hash) {
3388
+ if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
3389
+ if (locked.content_hash !== contentHash) {
3390
+ throw new Error(
3391
+ `content_hash mismatch for ${sourceKey} skill "${skillName}" (agent=${agent}, scope=${scope}): lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
3392
+ );
3393
+ }
3394
+ } else {
3395
+ logger5.debug(
3396
+ `Skipping content_hash integrity check for ${sourceKey} skill "${skillName}": recorded hash "${locked.content_hash}" was not written by rulesync.`
3397
+ );
3398
+ }
3399
+ }
3400
+ }
3235
3401
  async function removeStaleFile(params) {
3236
3402
  const { relativePath, scope, projectRoot, logger: logger5 } = params;
3237
3403
  if (posix3.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
@@ -3623,18 +3789,7 @@ async function resolveAndFetchSources(params) {
3623
3789
  }
3624
3790
  let lock = options.updateSources ? createEmptyLock() : await readLockFile({ projectRoot, logger: logger5 });
3625
3791
  if (options.frozen) {
3626
- const missingKeys = [];
3627
- for (const source of sources) {
3628
- const locked = getLockedSource(lock, source.source);
3629
- if (!locked) {
3630
- missingKeys.push(source.source);
3631
- }
3632
- }
3633
- if (missingKeys.length > 0) {
3634
- throw new Error(
3635
- `Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
3636
- );
3637
- }
3792
+ assertFrozenLockCoversSources2({ lock, sources });
3638
3793
  }
3639
3794
  const originalLockJson = JSON.stringify(lock);
3640
3795
  const token = GitHubClient.resolveToken(options.token);
@@ -3644,31 +3799,17 @@ async function resolveAndFetchSources(params) {
3644
3799
  const allFetchedSkillNames = /* @__PURE__ */ new Set();
3645
3800
  for (const sourceEntry of sources) {
3646
3801
  try {
3647
- const transport = sourceEntry.transport ?? "github";
3648
- let result;
3649
- if (transport === "git") {
3650
- result = await fetchSourceViaGit({
3651
- sourceEntry,
3652
- projectRoot,
3653
- lock,
3654
- localSkillNames,
3655
- alreadyFetchedSkillNames: allFetchedSkillNames,
3656
- updateSources: options.updateSources ?? false,
3657
- frozen: options.frozen ?? false,
3658
- logger: logger5
3659
- });
3660
- } else {
3661
- result = await fetchSource({
3662
- sourceEntry,
3663
- client,
3664
- projectRoot,
3665
- lock,
3666
- localSkillNames,
3667
- alreadyFetchedSkillNames: allFetchedSkillNames,
3668
- updateSources: options.updateSources ?? false,
3669
- logger: logger5
3670
- });
3671
- }
3802
+ const result = await fetchSourceByTransport({
3803
+ sourceEntry,
3804
+ client,
3805
+ projectRoot,
3806
+ lock,
3807
+ localSkillNames,
3808
+ alreadyFetchedSkillNames: allFetchedSkillNames,
3809
+ updateSources: options.updateSources ?? false,
3810
+ frozen: options.frozen ?? false,
3811
+ logger: logger5
3812
+ });
3672
3813
  const { skillCount, fetchedSkillNames, updatedLock } = result;
3673
3814
  lock = updatedLock;
3674
3815
  totalSkillCount += skillCount;
@@ -3684,16 +3825,7 @@ async function resolveAndFetchSources(params) {
3684
3825
  }
3685
3826
  }
3686
3827
  }
3687
- const sourceKeys = new Set(sources.map((s) => normalizeSourceKey(s.source)));
3688
- const prunedSources = {};
3689
- for (const [key, value] of Object.entries(lock.sources)) {
3690
- if (sourceKeys.has(normalizeSourceKey(key))) {
3691
- prunedSources[key] = value;
3692
- } else {
3693
- logger5.debug(`Pruned stale lockfile entry: ${key}`);
3694
- }
3695
- }
3696
- lock = { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
3828
+ lock = pruneStaleLockEntries({ lock, sources, logger: logger5 });
3697
3829
  if (!options.frozen && JSON.stringify(lock) !== originalLockJson) {
3698
3830
  await writeLockFile({ projectRoot, lock, logger: logger5 });
3699
3831
  } else {
@@ -3709,6 +3841,70 @@ function logGitClientHints(params) {
3709
3841
  logger5.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
3710
3842
  }
3711
3843
  }
3844
+ function assertFrozenLockCoversSources2(params) {
3845
+ const { lock, sources } = params;
3846
+ const missingKeys = [];
3847
+ for (const source of sources) {
3848
+ const locked = getLockedSource(lock, source.source);
3849
+ if (!locked) {
3850
+ missingKeys.push(source.source);
3851
+ }
3852
+ }
3853
+ if (missingKeys.length > 0) {
3854
+ throw new Error(
3855
+ `Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
3856
+ );
3857
+ }
3858
+ }
3859
+ async function fetchSourceByTransport(params) {
3860
+ const {
3861
+ sourceEntry,
3862
+ client,
3863
+ projectRoot,
3864
+ lock,
3865
+ localSkillNames,
3866
+ alreadyFetchedSkillNames,
3867
+ updateSources,
3868
+ frozen,
3869
+ logger: logger5
3870
+ } = params;
3871
+ const transport = sourceEntry.transport ?? "github";
3872
+ if (transport === "git") {
3873
+ return fetchSourceViaGit({
3874
+ sourceEntry,
3875
+ projectRoot,
3876
+ lock,
3877
+ localSkillNames,
3878
+ alreadyFetchedSkillNames,
3879
+ updateSources,
3880
+ frozen,
3881
+ logger: logger5
3882
+ });
3883
+ }
3884
+ return fetchSource({
3885
+ sourceEntry,
3886
+ client,
3887
+ projectRoot,
3888
+ lock,
3889
+ localSkillNames,
3890
+ alreadyFetchedSkillNames,
3891
+ updateSources,
3892
+ logger: logger5
3893
+ });
3894
+ }
3895
+ function pruneStaleLockEntries(params) {
3896
+ const { lock, sources, logger: logger5 } = params;
3897
+ const sourceKeys = new Set(sources.map((s) => normalizeSourceKey(s.source)));
3898
+ const prunedSources = {};
3899
+ for (const [key, value] of Object.entries(lock.sources)) {
3900
+ if (sourceKeys.has(normalizeSourceKey(key))) {
3901
+ prunedSources[key] = value;
3902
+ } else {
3903
+ logger5.debug(`Pruned stale lockfile entry: ${key}`);
3904
+ }
3905
+ }
3906
+ return { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
3907
+ }
3712
3908
  async function checkLockedSkillsExist(curatedDir, skillNames) {
3713
3909
  if (skillNames.length === 0) return true;
3714
3910
  for (const name of skillNames) {
@@ -3840,7 +4036,189 @@ function groupRemoteFilesBySkillRoot(params) {
3840
4036
  grouped.set(singleSkillName, rootLevelFiles);
3841
4037
  }
3842
4038
  }
3843
- return grouped;
4039
+ return grouped;
4040
+ }
4041
+ async function resolveGithubFetchRef(params) {
4042
+ const { parsed, locked, updateSources, sourceKey, client, logger: logger5 } = params;
4043
+ if (locked && !updateSources) {
4044
+ logger5.debug(`Using locked ref for ${sourceKey}: ${locked.resolvedRef}`);
4045
+ return {
4046
+ ref: locked.resolvedRef,
4047
+ resolvedSha: locked.resolvedRef,
4048
+ requestedRef: locked.requestedRef
4049
+ };
4050
+ }
4051
+ const requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
4052
+ const resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
4053
+ logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
4054
+ return { ref: resolvedSha, resolvedSha, requestedRef };
4055
+ }
4056
+ async function fetchRootLevelFallbackSkill(params) {
4057
+ const {
4058
+ entries,
4059
+ parsed,
4060
+ ref,
4061
+ resolvedSha,
4062
+ skillFilter,
4063
+ isWildcard,
4064
+ curatedDir,
4065
+ locked,
4066
+ sourceKey,
4067
+ localSkillNames,
4068
+ alreadyFetchedSkillNames,
4069
+ client,
4070
+ semaphore,
4071
+ fetchedSkills,
4072
+ logger: logger5
4073
+ } = params;
4074
+ const rootFiles = entries.filter((entry) => entry.type === "file");
4075
+ const rootSkillFiles = [];
4076
+ for (const file of rootFiles) {
4077
+ if (file.size > MAX_FILE_SIZE) {
4078
+ logger5.warn(
4079
+ `Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
4080
+ );
4081
+ continue;
4082
+ }
4083
+ const content = await withSemaphore(
4084
+ semaphore,
4085
+ () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
4086
+ );
4087
+ rootSkillFiles.push({ relativePath: file.name, content });
4088
+ }
4089
+ const groupedRootFiles = groupRemoteFilesBySkillRoot({
4090
+ remoteFiles: rootSkillFiles,
4091
+ skillFilter,
4092
+ isWildcard
4093
+ });
4094
+ const [fallbackSkillName] = groupedRootFiles.keys();
4095
+ if (fallbackSkillName === void 0) {
4096
+ return { handled: false, remoteSkillNames: [] };
4097
+ }
4098
+ if (!shouldSkipSkill({
4099
+ skillName: fallbackSkillName,
4100
+ sourceKey,
4101
+ localSkillNames,
4102
+ alreadyFetchedSkillNames,
4103
+ logger: logger5
4104
+ })) {
4105
+ fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
4106
+ skillName: fallbackSkillName,
4107
+ files: groupedRootFiles.get(fallbackSkillName) ?? [],
4108
+ curatedDir,
4109
+ locked,
4110
+ resolvedSha,
4111
+ sourceKey,
4112
+ logger: logger5
4113
+ });
4114
+ logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
4115
+ }
4116
+ return { handled: true, remoteSkillNames: [fallbackSkillName] };
4117
+ }
4118
+ async function fetchGithubSkillDir(params) {
4119
+ const {
4120
+ skillDir,
4121
+ parsed,
4122
+ ref,
4123
+ resolvedSha,
4124
+ curatedDir,
4125
+ locked,
4126
+ sourceKey,
4127
+ client,
4128
+ semaphore,
4129
+ logger: logger5
4130
+ } = params;
4131
+ const allFiles = await listDirectoryRecursive({
4132
+ client,
4133
+ owner: parsed.owner,
4134
+ repo: parsed.repo,
4135
+ path: skillDir.path,
4136
+ ref,
4137
+ semaphore
4138
+ });
4139
+ const files = allFiles.filter((file) => {
4140
+ if (file.size > MAX_FILE_SIZE) {
4141
+ logger5.warn(
4142
+ `Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
4143
+ );
4144
+ return false;
4145
+ }
4146
+ return true;
4147
+ });
4148
+ const skillFiles = [];
4149
+ for (const file of files) {
4150
+ const relativeToSkill = file.path.substring(skillDir.path.length + 1);
4151
+ const content = await withSemaphore(
4152
+ semaphore,
4153
+ () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
4154
+ );
4155
+ skillFiles.push({ relativePath: relativeToSkill, content });
4156
+ }
4157
+ return writeSkillAndComputeIntegrity({
4158
+ skillName: skillDir.name,
4159
+ files: skillFiles,
4160
+ curatedDir,
4161
+ locked,
4162
+ resolvedSha,
4163
+ sourceKey,
4164
+ logger: logger5
4165
+ });
4166
+ }
4167
+ async function discoverGithubSkillDirs(params) {
4168
+ const {
4169
+ parsed,
4170
+ ref,
4171
+ resolvedSha,
4172
+ skillFilter,
4173
+ isWildcard,
4174
+ curatedDir,
4175
+ locked,
4176
+ sourceKey,
4177
+ localSkillNames,
4178
+ alreadyFetchedSkillNames,
4179
+ client,
4180
+ semaphore,
4181
+ fetchedSkills,
4182
+ logger: logger5
4183
+ } = params;
4184
+ const skillsBasePath = parsed.path ?? "skills";
4185
+ try {
4186
+ const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
4187
+ const remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
4188
+ if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
4189
+ const fallback = await fetchRootLevelFallbackSkill({
4190
+ entries,
4191
+ parsed,
4192
+ ref,
4193
+ resolvedSha,
4194
+ skillFilter,
4195
+ isWildcard,
4196
+ curatedDir,
4197
+ locked,
4198
+ sourceKey,
4199
+ localSkillNames,
4200
+ alreadyFetchedSkillNames,
4201
+ client,
4202
+ semaphore,
4203
+ fetchedSkills,
4204
+ logger: logger5
4205
+ });
4206
+ if (fallback.handled) {
4207
+ return {
4208
+ status: "ok",
4209
+ remoteSkillDirs,
4210
+ fallbackHandled: true,
4211
+ remoteSkillNames: fallback.remoteSkillNames
4212
+ };
4213
+ }
4214
+ }
4215
+ return { status: "ok", remoteSkillDirs, fallbackHandled: false, remoteSkillNames: [] };
4216
+ } catch (error) {
4217
+ if (error instanceof GitHubClientError && error.statusCode === 404) {
4218
+ return { status: "notFound" };
4219
+ }
4220
+ throw error;
4221
+ }
3844
4222
  }
3845
4223
  async function fetchSource(params) {
3846
4224
  const {
@@ -3861,20 +4239,14 @@ async function fetchSource(params) {
3861
4239
  const sourceKey = sourceEntry.source;
3862
4240
  const locked = getLockedSource(lock, sourceKey);
3863
4241
  const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
3864
- let ref;
3865
- let resolvedSha;
3866
- let requestedRef;
3867
- if (locked && !updateSources) {
3868
- ref = locked.resolvedRef;
3869
- resolvedSha = locked.resolvedRef;
3870
- requestedRef = locked.requestedRef;
3871
- logger5.debug(`Using locked ref for ${sourceKey}: ${resolvedSha}`);
3872
- } else {
3873
- requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
3874
- resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
3875
- ref = resolvedSha;
3876
- logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
3877
- }
4242
+ const { ref, resolvedSha, requestedRef } = await resolveGithubFetchRef({
4243
+ parsed,
4244
+ locked,
4245
+ updateSources,
4246
+ sourceKey,
4247
+ client,
4248
+ logger: logger5
4249
+ });
3878
4250
  const curatedDir = join12(projectRoot, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
3879
4251
  if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
3880
4252
  const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
@@ -3891,69 +4263,29 @@ async function fetchSource(params) {
3891
4263
  const isWildcard = skillFilter.length === 1 && skillFilter[0] === "*";
3892
4264
  const semaphore = new Semaphore4(FETCH_CONCURRENCY_LIMIT);
3893
4265
  const fetchedSkills = {};
3894
- const skillsBasePath = parsed.path ?? "skills";
3895
- let remoteSkillDirs;
3896
- let remoteSkillNames = [];
3897
- let fallbackHandled = false;
3898
- try {
3899
- const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
3900
- remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
3901
- if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
3902
- const rootFiles = entries.filter((entry) => entry.type === "file");
3903
- const rootSkillFiles = [];
3904
- for (const file of rootFiles) {
3905
- if (file.size > MAX_FILE_SIZE) {
3906
- logger5.warn(
3907
- `Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
3908
- );
3909
- continue;
3910
- }
3911
- const content = await withSemaphore(
3912
- semaphore,
3913
- () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
3914
- );
3915
- rootSkillFiles.push({ relativePath: file.name, content });
3916
- }
3917
- const groupedRootFiles = groupRemoteFilesBySkillRoot({
3918
- remoteFiles: rootSkillFiles,
3919
- skillFilter,
3920
- isWildcard
3921
- });
3922
- const [fallbackSkillName] = groupedRootFiles.keys();
3923
- if (fallbackSkillName !== void 0) {
3924
- fallbackHandled = true;
3925
- remoteSkillNames = [fallbackSkillName];
3926
- if (!shouldSkipSkill({
3927
- skillName: fallbackSkillName,
3928
- sourceKey,
3929
- localSkillNames,
3930
- alreadyFetchedSkillNames,
3931
- logger: logger5
3932
- })) {
3933
- fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
3934
- skillName: fallbackSkillName,
3935
- files: groupedRootFiles.get(fallbackSkillName) ?? [],
3936
- curatedDir,
3937
- locked,
3938
- resolvedSha,
3939
- sourceKey,
3940
- logger: logger5
3941
- });
3942
- logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
3943
- }
3944
- }
3945
- }
3946
- } catch (error) {
3947
- if (error instanceof GitHubClientError && error.statusCode === 404) {
3948
- logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
3949
- return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
3950
- }
3951
- throw error;
4266
+ const discovery = await discoverGithubSkillDirs({
4267
+ parsed,
4268
+ ref,
4269
+ resolvedSha,
4270
+ skillFilter,
4271
+ isWildcard,
4272
+ curatedDir,
4273
+ locked,
4274
+ sourceKey,
4275
+ localSkillNames,
4276
+ alreadyFetchedSkillNames,
4277
+ client,
4278
+ semaphore,
4279
+ fetchedSkills,
4280
+ logger: logger5
4281
+ });
4282
+ if (discovery.status === "notFound") {
4283
+ logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
4284
+ return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
3952
4285
  }
4286
+ const { remoteSkillDirs, fallbackHandled, remoteSkillNames: fallbackSkillNames } = discovery;
3953
4287
  const filteredDirs = isWildcard ? remoteSkillDirs : remoteSkillDirs.filter((d) => skillFilter.includes(d.name));
3954
- if (!fallbackHandled) {
3955
- remoteSkillNames = filteredDirs.map((d) => d.name);
3956
- }
4288
+ const remoteSkillNames = fallbackHandled ? fallbackSkillNames : filteredDirs.map((d) => d.name);
3957
4289
  if (locked) {
3958
4290
  await cleanPreviousCuratedSkills({ curatedDir, lockedSkillNames, logger: logger5 });
3959
4291
  }
@@ -3967,39 +4299,16 @@ async function fetchSource(params) {
3967
4299
  })) {
3968
4300
  continue;
3969
4301
  }
3970
- const allFiles = await listDirectoryRecursive({
3971
- client,
3972
- owner: parsed.owner,
3973
- repo: parsed.repo,
3974
- path: skillDir.path,
4302
+ fetchedSkills[skillDir.name] = await fetchGithubSkillDir({
4303
+ skillDir,
4304
+ parsed,
3975
4305
  ref,
3976
- semaphore
3977
- });
3978
- const files = allFiles.filter((file) => {
3979
- if (file.size > MAX_FILE_SIZE) {
3980
- logger5.warn(
3981
- `Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
3982
- );
3983
- return false;
3984
- }
3985
- return true;
3986
- });
3987
- const skillFiles = [];
3988
- for (const file of files) {
3989
- const relativeToSkill = file.path.substring(skillDir.path.length + 1);
3990
- const content = await withSemaphore(
3991
- semaphore,
3992
- () => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
3993
- );
3994
- skillFiles.push({ relativePath: relativeToSkill, content });
3995
- }
3996
- fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
3997
- skillName: skillDir.name,
3998
- files: skillFiles,
4306
+ resolvedSha,
3999
4307
  curatedDir,
4000
4308
  locked,
4001
- resolvedSha,
4002
4309
  sourceKey,
4310
+ client,
4311
+ semaphore,
4003
4312
  logger: logger5
4004
4313
  });
4005
4314
  logger5.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
@@ -5908,6 +6217,174 @@ function ensureBody({ body, feature, operation }) {
5908
6217
  }
5909
6218
  return body;
5910
6219
  }
6220
+ function requireContent({
6221
+ content,
6222
+ feature
6223
+ }) {
6224
+ if (!content) {
6225
+ throw new Error(`content is required for ${feature} put operation`);
6226
+ }
6227
+ return content;
6228
+ }
6229
+ function executeRule(parsed) {
6230
+ if (parsed.operation === "list") {
6231
+ return ruleTools.listRules.execute();
6232
+ }
6233
+ if (parsed.operation === "get") {
6234
+ return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
6235
+ }
6236
+ if (parsed.operation === "put") {
6237
+ return ruleTools.putRule.execute({
6238
+ relativePathFromCwd: requireTargetPath(parsed),
6239
+ frontmatter: parseFrontmatter({
6240
+ feature: "rule",
6241
+ frontmatter: parsed.frontmatter ?? {}
6242
+ }),
6243
+ body: ensureBody(parsed)
6244
+ });
6245
+ }
6246
+ return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
6247
+ }
6248
+ function executeCommand(parsed) {
6249
+ if (parsed.operation === "list") {
6250
+ return commandTools.listCommands.execute();
6251
+ }
6252
+ if (parsed.operation === "get") {
6253
+ return commandTools.getCommand.execute({
6254
+ relativePathFromCwd: requireTargetPath(parsed)
6255
+ });
6256
+ }
6257
+ if (parsed.operation === "put") {
6258
+ return commandTools.putCommand.execute({
6259
+ relativePathFromCwd: requireTargetPath(parsed),
6260
+ frontmatter: parseFrontmatter({
6261
+ feature: "command",
6262
+ frontmatter: parsed.frontmatter ?? {}
6263
+ }),
6264
+ body: ensureBody(parsed)
6265
+ });
6266
+ }
6267
+ return commandTools.deleteCommand.execute({
6268
+ relativePathFromCwd: requireTargetPath(parsed)
6269
+ });
6270
+ }
6271
+ function executeSubagent(parsed) {
6272
+ if (parsed.operation === "list") {
6273
+ return subagentTools.listSubagents.execute();
6274
+ }
6275
+ if (parsed.operation === "get") {
6276
+ return subagentTools.getSubagent.execute({
6277
+ relativePathFromCwd: requireTargetPath(parsed)
6278
+ });
6279
+ }
6280
+ if (parsed.operation === "put") {
6281
+ return subagentTools.putSubagent.execute({
6282
+ relativePathFromCwd: requireTargetPath(parsed),
6283
+ frontmatter: parseFrontmatter({
6284
+ feature: "subagent",
6285
+ frontmatter: parsed.frontmatter ?? {}
6286
+ }),
6287
+ body: ensureBody(parsed)
6288
+ });
6289
+ }
6290
+ return subagentTools.deleteSubagent.execute({
6291
+ relativePathFromCwd: requireTargetPath(parsed)
6292
+ });
6293
+ }
6294
+ function executeSkill(parsed) {
6295
+ if (parsed.operation === "list") {
6296
+ return skillTools.listSkills.execute();
6297
+ }
6298
+ if (parsed.operation === "get") {
6299
+ return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
6300
+ }
6301
+ if (parsed.operation === "put") {
6302
+ return skillTools.putSkill.execute({
6303
+ relativeDirPathFromCwd: requireTargetPath(parsed),
6304
+ frontmatter: parseFrontmatter({
6305
+ feature: "skill",
6306
+ frontmatter: parsed.frontmatter ?? {}
6307
+ }),
6308
+ body: ensureBody(parsed),
6309
+ otherFiles: parsed.otherFiles ?? []
6310
+ });
6311
+ }
6312
+ return skillTools.deleteSkill.execute({
6313
+ relativeDirPathFromCwd: requireTargetPath(parsed)
6314
+ });
6315
+ }
6316
+ function executeIgnore(parsed) {
6317
+ if (parsed.operation === "get") {
6318
+ return ignoreTools.getIgnoreFile.execute();
6319
+ }
6320
+ if (parsed.operation === "put") {
6321
+ return ignoreTools.putIgnoreFile.execute({
6322
+ content: requireContent({ content: parsed.content, feature: "ignore" })
6323
+ });
6324
+ }
6325
+ return ignoreTools.deleteIgnoreFile.execute();
6326
+ }
6327
+ function executeMcp(parsed) {
6328
+ if (parsed.operation === "get") {
6329
+ return mcpTools.getMcpFile.execute();
6330
+ }
6331
+ if (parsed.operation === "put") {
6332
+ return mcpTools.putMcpFile.execute({
6333
+ content: requireContent({ content: parsed.content, feature: "mcp" })
6334
+ });
6335
+ }
6336
+ return mcpTools.deleteMcpFile.execute();
6337
+ }
6338
+ function executePermissions(parsed) {
6339
+ if (parsed.operation === "get") {
6340
+ return permissionsTools.getPermissionsFile.execute();
6341
+ }
6342
+ if (parsed.operation === "put") {
6343
+ return permissionsTools.putPermissionsFile.execute({
6344
+ content: requireContent({ content: parsed.content, feature: "permissions" })
6345
+ });
6346
+ }
6347
+ return permissionsTools.deletePermissionsFile.execute();
6348
+ }
6349
+ function executeHooks(parsed) {
6350
+ if (parsed.operation === "get") {
6351
+ return hooksTools.getHooksFile.execute();
6352
+ }
6353
+ if (parsed.operation === "put") {
6354
+ return hooksTools.putHooksFile.execute({
6355
+ content: requireContent({ content: parsed.content, feature: "hooks" })
6356
+ });
6357
+ }
6358
+ return hooksTools.deleteHooksFile.execute();
6359
+ }
6360
+ function executeGenerate2(parsed) {
6361
+ return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
6362
+ }
6363
+ function executeImport2(parsed) {
6364
+ if (!parsed.importOptions) {
6365
+ throw new Error("importOptions is required for import feature");
6366
+ }
6367
+ return importTools.executeImport.execute(parsed.importOptions);
6368
+ }
6369
+ function executeConvert2(parsed) {
6370
+ if (!parsed.convertOptions) {
6371
+ throw new Error("convertOptions is required for convert feature");
6372
+ }
6373
+ return convertTools.executeConvert.execute(parsed.convertOptions);
6374
+ }
6375
+ var featureExecutors = {
6376
+ rule: executeRule,
6377
+ command: executeCommand,
6378
+ subagent: executeSubagent,
6379
+ skill: executeSkill,
6380
+ ignore: executeIgnore,
6381
+ mcp: executeMcp,
6382
+ permissions: executePermissions,
6383
+ hooks: executeHooks,
6384
+ generate: executeGenerate2,
6385
+ import: executeImport2,
6386
+ convert: executeConvert2
6387
+ };
5911
6388
  var rulesyncTool = {
5912
6389
  name: "rulesyncTool",
5913
6390
  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.",
@@ -5915,161 +6392,11 @@ var rulesyncTool = {
5915
6392
  execute: async (args) => {
5916
6393
  const parsed = rulesyncToolSchema.parse(args);
5917
6394
  assertSupported({ feature: parsed.feature, operation: parsed.operation });
5918
- switch (parsed.feature) {
5919
- case "rule": {
5920
- if (parsed.operation === "list") {
5921
- return ruleTools.listRules.execute();
5922
- }
5923
- if (parsed.operation === "get") {
5924
- return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
5925
- }
5926
- if (parsed.operation === "put") {
5927
- return ruleTools.putRule.execute({
5928
- relativePathFromCwd: requireTargetPath(parsed),
5929
- frontmatter: parseFrontmatter({
5930
- feature: "rule",
5931
- frontmatter: parsed.frontmatter ?? {}
5932
- }),
5933
- body: ensureBody(parsed)
5934
- });
5935
- }
5936
- return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
5937
- }
5938
- case "command": {
5939
- if (parsed.operation === "list") {
5940
- return commandTools.listCommands.execute();
5941
- }
5942
- if (parsed.operation === "get") {
5943
- return commandTools.getCommand.execute({
5944
- relativePathFromCwd: requireTargetPath(parsed)
5945
- });
5946
- }
5947
- if (parsed.operation === "put") {
5948
- return commandTools.putCommand.execute({
5949
- relativePathFromCwd: requireTargetPath(parsed),
5950
- frontmatter: parseFrontmatter({
5951
- feature: "command",
5952
- frontmatter: parsed.frontmatter ?? {}
5953
- }),
5954
- body: ensureBody(parsed)
5955
- });
5956
- }
5957
- return commandTools.deleteCommand.execute({
5958
- relativePathFromCwd: requireTargetPath(parsed)
5959
- });
5960
- }
5961
- case "subagent": {
5962
- if (parsed.operation === "list") {
5963
- return subagentTools.listSubagents.execute();
5964
- }
5965
- if (parsed.operation === "get") {
5966
- return subagentTools.getSubagent.execute({
5967
- relativePathFromCwd: requireTargetPath(parsed)
5968
- });
5969
- }
5970
- if (parsed.operation === "put") {
5971
- return subagentTools.putSubagent.execute({
5972
- relativePathFromCwd: requireTargetPath(parsed),
5973
- frontmatter: parseFrontmatter({
5974
- feature: "subagent",
5975
- frontmatter: parsed.frontmatter ?? {}
5976
- }),
5977
- body: ensureBody(parsed)
5978
- });
5979
- }
5980
- return subagentTools.deleteSubagent.execute({
5981
- relativePathFromCwd: requireTargetPath(parsed)
5982
- });
5983
- }
5984
- case "skill": {
5985
- if (parsed.operation === "list") {
5986
- return skillTools.listSkills.execute();
5987
- }
5988
- if (parsed.operation === "get") {
5989
- return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
5990
- }
5991
- if (parsed.operation === "put") {
5992
- return skillTools.putSkill.execute({
5993
- relativeDirPathFromCwd: requireTargetPath(parsed),
5994
- frontmatter: parseFrontmatter({
5995
- feature: "skill",
5996
- frontmatter: parsed.frontmatter ?? {}
5997
- }),
5998
- body: ensureBody(parsed),
5999
- otherFiles: parsed.otherFiles ?? []
6000
- });
6001
- }
6002
- return skillTools.deleteSkill.execute({
6003
- relativeDirPathFromCwd: requireTargetPath(parsed)
6004
- });
6005
- }
6006
- case "ignore": {
6007
- if (parsed.operation === "get") {
6008
- return ignoreTools.getIgnoreFile.execute();
6009
- }
6010
- if (parsed.operation === "put") {
6011
- if (!parsed.content) {
6012
- throw new Error("content is required for ignore put operation");
6013
- }
6014
- return ignoreTools.putIgnoreFile.execute({ content: parsed.content });
6015
- }
6016
- return ignoreTools.deleteIgnoreFile.execute();
6017
- }
6018
- case "mcp": {
6019
- if (parsed.operation === "get") {
6020
- return mcpTools.getMcpFile.execute();
6021
- }
6022
- if (parsed.operation === "put") {
6023
- if (!parsed.content) {
6024
- throw new Error("content is required for mcp put operation");
6025
- }
6026
- return mcpTools.putMcpFile.execute({ content: parsed.content });
6027
- }
6028
- return mcpTools.deleteMcpFile.execute();
6029
- }
6030
- case "permissions": {
6031
- if (parsed.operation === "get") {
6032
- return permissionsTools.getPermissionsFile.execute();
6033
- }
6034
- if (parsed.operation === "put") {
6035
- if (!parsed.content) {
6036
- throw new Error("content is required for permissions put operation");
6037
- }
6038
- return permissionsTools.putPermissionsFile.execute({ content: parsed.content });
6039
- }
6040
- return permissionsTools.deletePermissionsFile.execute();
6041
- }
6042
- case "hooks": {
6043
- if (parsed.operation === "get") {
6044
- return hooksTools.getHooksFile.execute();
6045
- }
6046
- if (parsed.operation === "put") {
6047
- if (!parsed.content) {
6048
- throw new Error("content is required for hooks put operation");
6049
- }
6050
- return hooksTools.putHooksFile.execute({ content: parsed.content });
6051
- }
6052
- return hooksTools.deleteHooksFile.execute();
6053
- }
6054
- case "generate": {
6055
- return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
6056
- }
6057
- case "import": {
6058
- if (!parsed.importOptions) {
6059
- throw new Error("importOptions is required for import feature");
6060
- }
6061
- return importTools.executeImport.execute(parsed.importOptions);
6062
- }
6063
- case "convert": {
6064
- if (!parsed.convertOptions) {
6065
- throw new Error("convertOptions is required for convert feature");
6066
- }
6067
- return convertTools.executeConvert.execute(parsed.convertOptions);
6068
- }
6069
- default: {
6070
- throw new Error(`Unknown feature: ${parsed.feature}`);
6071
- }
6395
+ const executor = featureExecutors[parsed.feature];
6396
+ if (!executor) {
6397
+ throw new Error(`Unknown feature: ${parsed.feature}`);
6072
6398
  }
6399
+ return executor(parsed);
6073
6400
  }
6074
6401
  };
6075
6402
 
@@ -6288,103 +6615,150 @@ function parseSha256Sums(content) {
6288
6615
  }
6289
6616
  return result;
6290
6617
  }
6291
- async function performBinaryUpdate(currentVersion, options = {}) {
6292
- const { force = false, token } = options;
6293
- const updateCheck = await checkForUpdate(currentVersion, token);
6294
- if (!updateCheck.hasUpdate && !force) {
6295
- return `Already at the latest version (${currentVersion})`;
6296
- }
6618
+ function resolveUpdateAssets(release) {
6297
6619
  const assetName = getPlatformAssetName();
6298
6620
  if (!assetName) {
6299
6621
  throw new Error(
6300
6622
  `Unsupported platform: ${os.platform()} ${os.arch()}. Please download manually from ${RELEASES_URL}`
6301
6623
  );
6302
6624
  }
6303
- const binaryAsset = findAsset(updateCheck.release, assetName);
6625
+ const binaryAsset = findAsset(release, assetName);
6304
6626
  if (!binaryAsset) {
6305
6627
  throw new Error(
6306
6628
  `Binary for ${assetName} not found in release. Please download manually from ${RELEASES_URL}`
6307
6629
  );
6308
6630
  }
6309
- const checksumAsset = findAsset(updateCheck.release, "SHA256SUMS");
6631
+ const checksumAsset = findAsset(release, "SHA256SUMS");
6310
6632
  if (!checksumAsset) {
6311
6633
  throw new Error(
6312
6634
  `SHA256SUMS not found in release. Cannot verify download integrity. Please download manually from ${RELEASES_URL}`
6313
6635
  );
6314
6636
  }
6315
- const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rulesync-update-"));
6316
- let restoreFailed = false;
6637
+ return { assetName, binaryAsset, checksumAsset };
6638
+ }
6639
+ async function downloadAndVerifyBinary(params) {
6640
+ const { tempDir, assetName, binaryAsset, checksumAsset } = params;
6641
+ const tempBinaryPath = path.join(tempDir, assetName);
6642
+ await downloadFile(binaryAsset.browser_download_url, tempBinaryPath);
6643
+ const checksumsPath = path.join(tempDir, "SHA256SUMS");
6644
+ await downloadFile(checksumAsset.browser_download_url, checksumsPath);
6645
+ const checksumsContent = await fs.promises.readFile(checksumsPath, "utf-8");
6646
+ const checksums = parseSha256Sums(checksumsContent);
6647
+ const expectedChecksum = checksums.get(assetName);
6648
+ if (!expectedChecksum) {
6649
+ throw new Error(
6650
+ `Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
6651
+ );
6652
+ }
6653
+ const actualChecksum = await calculateSha256(tempBinaryPath);
6654
+ if (actualChecksum !== expectedChecksum) {
6655
+ throw new Error(
6656
+ `Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
6657
+ );
6658
+ }
6659
+ return tempBinaryPath;
6660
+ }
6661
+ async function replaceCurrentBinary(params) {
6662
+ const { tempBinaryPath, currentExePath, currentDir } = params;
6663
+ const tempInPlace = path.join(currentDir, `.rulesync-update-${crypto.randomUUID()}`);
6317
6664
  try {
6665
+ await fs.promises.copyFile(tempBinaryPath, tempInPlace);
6318
6666
  if (os.platform() !== "win32") {
6319
- await fs.promises.chmod(tempDir, 448);
6667
+ await fs.promises.chmod(tempInPlace, 493);
6320
6668
  }
6321
- const tempBinaryPath = path.join(tempDir, assetName);
6322
- await downloadFile(binaryAsset.browser_download_url, tempBinaryPath);
6323
- const checksumsPath = path.join(tempDir, "SHA256SUMS");
6324
- await downloadFile(checksumAsset.browser_download_url, checksumsPath);
6325
- const checksumsContent = await fs.promises.readFile(checksumsPath, "utf-8");
6326
- const checksums = parseSha256Sums(checksumsContent);
6327
- const expectedChecksum = checksums.get(assetName);
6328
- if (!expectedChecksum) {
6329
- throw new Error(
6330
- `Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
6331
- );
6669
+ await fs.promises.rename(tempInPlace, currentExePath);
6670
+ } catch {
6671
+ try {
6672
+ await fs.promises.unlink(tempInPlace);
6673
+ } catch {
6332
6674
  }
6333
- const actualChecksum = await calculateSha256(tempBinaryPath);
6334
- if (actualChecksum !== expectedChecksum) {
6335
- throw new Error(
6336
- `Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
6337
- );
6675
+ await fs.promises.copyFile(tempBinaryPath, currentExePath);
6676
+ if (os.platform() !== "win32") {
6677
+ await fs.promises.chmod(currentExePath, 493);
6338
6678
  }
6339
- const currentExePath = await fs.promises.realpath(process.execPath);
6340
- const currentDir = path.dirname(currentExePath);
6341
- const backupPath = path.join(tempDir, "rulesync.backup");
6342
- try {
6343
- await fs.promises.copyFile(currentExePath, backupPath);
6344
- } catch (error) {
6345
- if (isPermissionError(error)) {
6346
- throw new UpdatePermissionError(
6347
- `Permission denied: Cannot read ${currentExePath}. Try running with sudo.`
6348
- );
6349
- }
6350
- throw error;
6679
+ }
6680
+ }
6681
+ async function installVerifiedBinary(params) {
6682
+ const { tempDir, tempBinaryPath, currentVersion, latestVersion } = params;
6683
+ const currentExePath = await fs.promises.realpath(process.execPath);
6684
+ const currentDir = path.dirname(currentExePath);
6685
+ const backupPath = path.join(tempDir, "rulesync.backup");
6686
+ try {
6687
+ await fs.promises.copyFile(currentExePath, backupPath);
6688
+ } catch (error) {
6689
+ if (isPermissionError(error)) {
6690
+ throw new UpdatePermissionError(
6691
+ `Permission denied: Cannot read ${currentExePath}. Try running with sudo.`
6692
+ );
6351
6693
  }
6694
+ throw error;
6695
+ }
6696
+ try {
6697
+ await replaceCurrentBinary({ tempBinaryPath, currentExePath, currentDir });
6698
+ return {
6699
+ message: `Successfully updated from ${currentVersion} to ${latestVersion}`,
6700
+ restoreFailed: false
6701
+ };
6702
+ } catch (error) {
6352
6703
  try {
6353
- const tempInPlace = path.join(currentDir, `.rulesync-update-${crypto.randomUUID()}`);
6354
- try {
6355
- await fs.promises.copyFile(tempBinaryPath, tempInPlace);
6356
- if (os.platform() !== "win32") {
6357
- await fs.promises.chmod(tempInPlace, 493);
6358
- }
6359
- await fs.promises.rename(tempInPlace, currentExePath);
6360
- } catch {
6361
- try {
6362
- await fs.promises.unlink(tempInPlace);
6363
- } catch {
6364
- }
6365
- await fs.promises.copyFile(tempBinaryPath, currentExePath);
6366
- if (os.platform() !== "win32") {
6367
- await fs.promises.chmod(currentExePath, 493);
6368
- }
6369
- }
6370
- return `Successfully updated from ${currentVersion} to ${updateCheck.latestVersion}`;
6371
- } catch (error) {
6372
- try {
6373
- await fs.promises.copyFile(backupPath, currentExePath);
6374
- } catch {
6375
- restoreFailed = true;
6376
- throw new Error(
6704
+ await fs.promises.copyFile(backupPath, currentExePath);
6705
+ } catch {
6706
+ throw new RestoreFailedError(
6707
+ new Error(
6377
6708
  `Failed to replace binary and restore failed. Backup is preserved at: ${backupPath} (in ${tempDir}). Please manually copy it to ${currentExePath}. Original error: ${error instanceof Error ? error.message : String(error)}`,
6378
6709
  { cause: error }
6379
- );
6380
- }
6381
- if (isPermissionError(error)) {
6382
- throw new UpdatePermissionError(
6383
- `Permission denied: Cannot write to ${path.dirname(currentExePath)}. Try running with sudo.`
6384
- );
6385
- }
6386
- throw error;
6710
+ )
6711
+ );
6712
+ }
6713
+ if (isPermissionError(error)) {
6714
+ throw new UpdatePermissionError(
6715
+ `Permission denied: Cannot write to ${path.dirname(currentExePath)}. Try running with sudo.`
6716
+ );
6717
+ }
6718
+ throw error;
6719
+ }
6720
+ }
6721
+ var RestoreFailedError = class extends Error {
6722
+ cause;
6723
+ constructor(cause) {
6724
+ super(cause.message);
6725
+ this.name = "RestoreFailedError";
6726
+ this.cause = cause;
6727
+ }
6728
+ };
6729
+ async function performBinaryUpdate(currentVersion, options = {}) {
6730
+ const { force = false, token } = options;
6731
+ const updateCheck = await checkForUpdate(currentVersion, token);
6732
+ if (!updateCheck.hasUpdate && !force) {
6733
+ return `Already at the latest version (${currentVersion})`;
6734
+ }
6735
+ const { assetName, binaryAsset, checksumAsset } = resolveUpdateAssets(updateCheck.release);
6736
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rulesync-update-"));
6737
+ let restoreFailed = false;
6738
+ try {
6739
+ if (os.platform() !== "win32") {
6740
+ await fs.promises.chmod(tempDir, 448);
6741
+ }
6742
+ const tempBinaryPath = await downloadAndVerifyBinary({
6743
+ tempDir,
6744
+ assetName,
6745
+ binaryAsset,
6746
+ checksumAsset
6747
+ });
6748
+ const installed = await installVerifiedBinary({
6749
+ tempDir,
6750
+ tempBinaryPath,
6751
+ currentVersion,
6752
+ latestVersion: updateCheck.latestVersion
6753
+ });
6754
+ restoreFailed = installed.restoreFailed;
6755
+ return installed.message;
6756
+ } catch (error) {
6757
+ if (error instanceof RestoreFailedError) {
6758
+ restoreFailed = true;
6759
+ throw error.cause;
6387
6760
  }
6761
+ throw error;
6388
6762
  } finally {
6389
6763
  if (!restoreFailed) {
6390
6764
  try {
@@ -6515,7 +6889,7 @@ function wrapCommand({
6515
6889
  }
6516
6890
 
6517
6891
  // src/cli/index.ts
6518
- var getVersion = () => "8.31.0";
6892
+ var getVersion = () => "8.32.0";
6519
6893
  function wrapCommand2(name, errorCode, handler) {
6520
6894
  return wrapCommand({ name, errorCode, handler, getVersion });
6521
6895
  }