rulesync 8.12.0 → 8.14.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
@@ -79,7 +79,7 @@ import {
79
79
  stringifyFrontmatter,
80
80
  toPosixPath,
81
81
  writeFileContent
82
- } from "../chunk-643VJ2QM.js";
82
+ } from "../chunk-C7PHKGD2.js";
83
83
 
84
84
  // src/cli/index.ts
85
85
  import { Command } from "commander";
@@ -658,32 +658,32 @@ async function convertFetchedFilesToRulesync(params) {
658
658
  {
659
659
  feature: "rules",
660
660
  getTargets: () => RulesProcessor.getToolTargets({ global: false }),
661
- createProcessor: () => new RulesProcessor({ baseDir: tempDir, toolTarget: target, global: false, logger: logger5 })
661
+ createProcessor: () => new RulesProcessor({ outputRoot: tempDir, toolTarget: target, global: false, logger: logger5 })
662
662
  },
663
663
  {
664
664
  feature: "commands",
665
665
  getTargets: () => CommandsProcessor.getToolTargets({ global: false, includeSimulated: false }),
666
- createProcessor: () => new CommandsProcessor({ baseDir: tempDir, toolTarget: target, global: false, logger: logger5 })
666
+ createProcessor: () => new CommandsProcessor({ outputRoot: tempDir, toolTarget: target, global: false, logger: logger5 })
667
667
  },
668
668
  {
669
669
  feature: "subagents",
670
670
  getTargets: () => SubagentsProcessor.getToolTargets({ global: false, includeSimulated: false }),
671
- createProcessor: () => new SubagentsProcessor({ baseDir: tempDir, toolTarget: target, global: false, logger: logger5 })
671
+ createProcessor: () => new SubagentsProcessor({ outputRoot: tempDir, toolTarget: target, global: false, logger: logger5 })
672
672
  },
673
673
  {
674
674
  feature: "ignore",
675
675
  getTargets: () => IgnoreProcessor.getToolTargets(),
676
- createProcessor: () => new IgnoreProcessor({ baseDir: tempDir, toolTarget: target, logger: logger5 })
676
+ createProcessor: () => new IgnoreProcessor({ outputRoot: tempDir, toolTarget: target, logger: logger5 })
677
677
  },
678
678
  {
679
679
  feature: "mcp",
680
680
  getTargets: () => McpProcessor.getToolTargets({ global: false }),
681
- createProcessor: () => new McpProcessor({ baseDir: tempDir, toolTarget: target, global: false, logger: logger5 })
681
+ createProcessor: () => new McpProcessor({ outputRoot: tempDir, toolTarget: target, global: false, logger: logger5 })
682
682
  },
683
683
  {
684
684
  feature: "hooks",
685
685
  getTargets: () => HooksProcessor.getToolTargets({ global: false }),
686
- createProcessor: () => new HooksProcessor({ baseDir: tempDir, toolTarget: target, global: false, logger: logger5 })
686
+ createProcessor: () => new HooksProcessor({ outputRoot: tempDir, toolTarget: target, global: false, logger: logger5 })
687
687
  }
688
688
  ];
689
689
  for (const config of featureConfigs) {
@@ -728,7 +728,7 @@ function isNotFoundError(error) {
728
728
  return false;
729
729
  }
730
730
  async function fetchFiles(params) {
731
- const { source, options = {}, baseDir = process.cwd(), logger: logger5 } = params;
731
+ const { source, options = {}, outputRoot = process.cwd(), logger: logger5 } = params;
732
732
  const parsed = parseSource(source);
733
733
  if (parsed.provider === "gitlab") {
734
734
  throw new Error(
@@ -743,7 +743,7 @@ async function fetchFiles(params) {
743
743
  const target = options.target ?? "rulesync";
744
744
  checkPathTraversal({
745
745
  relativePath: outputDir,
746
- intendedRootDir: baseDir
746
+ intendedRootDir: outputRoot
747
747
  });
748
748
  const token = GitHubClient.resolveToken(options.token);
749
749
  const client = new GitHubClient({ token });
@@ -766,7 +766,7 @@ async function fetchFiles(params) {
766
766
  enabledFeatures,
767
767
  target,
768
768
  outputDir,
769
- baseDir,
769
+ outputRoot,
770
770
  conflictStrategy,
771
771
  logger: logger5
772
772
  });
@@ -793,7 +793,7 @@ async function fetchFiles(params) {
793
793
  skipped: 0
794
794
  };
795
795
  }
796
- const outputBasePath = join(baseDir, outputDir);
796
+ const outputBasePath = join(outputRoot, outputDir);
797
797
  for (const { relativePath, size } of filesToFetch) {
798
798
  checkPathTraversal({
799
799
  relativePath,
@@ -907,7 +907,7 @@ async function fetchAndConvertToolFiles(params) {
907
907
  enabledFeatures,
908
908
  target,
909
909
  outputDir,
910
- baseDir,
910
+ outputRoot,
911
911
  conflictStrategy: _conflictStrategy,
912
912
  logger: logger5
913
913
  } = params;
@@ -956,7 +956,7 @@ async function fetchAndConvertToolFiles(params) {
956
956
  logger5.debug(`Fetched to temp: ${toolRelativePath}`);
957
957
  })
958
958
  );
959
- const outputBasePath = join(baseDir, outputDir);
959
+ const outputBasePath = join(outputRoot, outputDir);
960
960
  const { converted, convertedPaths } = await convertFetchedFilesToRulesync({
961
961
  tempDir,
962
962
  outputDir: outputBasePath,
@@ -1110,6 +1110,15 @@ async function fetchCommand(logger5, options) {
1110
1110
  }
1111
1111
 
1112
1112
  // src/cli/commands/generate.ts
1113
+ function sameDirSets(a, b) {
1114
+ const aSet = new Set(a);
1115
+ const bSet = new Set(b);
1116
+ if (aSet.size !== bSet.size) return false;
1117
+ for (const v of aSet) {
1118
+ if (!bSet.has(v)) return false;
1119
+ }
1120
+ return true;
1121
+ }
1113
1122
  function logFeatureResult(logger5, params) {
1114
1123
  const { count, paths, featureName, isPreview, modePrefix } = params;
1115
1124
  if (count > 0) {
@@ -1124,18 +1133,33 @@ function logFeatureResult(logger5, params) {
1124
1133
  }
1125
1134
  }
1126
1135
  async function generateCommand(logger5, options) {
1127
- const config = await ConfigResolver.resolve(options);
1136
+ const { baseDir, outputRoots, ...rest } = options;
1137
+ if (baseDir !== void 0) {
1138
+ logger5.warn(
1139
+ "--base-dir is deprecated; use --output-roots instead. It will be removed in a future major release."
1140
+ );
1141
+ }
1142
+ const outputRootsResolved = outputRoots !== void 0 && outputRoots.length > 0 ? outputRoots : baseDir;
1143
+ if (baseDir !== void 0 && outputRoots !== void 0 && baseDir.length > 0 && outputRoots.length > 0 && !sameDirSets(outputRoots, baseDir)) {
1144
+ logger5.warn(
1145
+ `Both '--output-roots' and '--base-dir' were provided with differing values; using '--output-roots' (${JSON.stringify(outputRoots)}) and ignoring '--base-dir' (${JSON.stringify(baseDir)}).`
1146
+ );
1147
+ }
1148
+ const config = await ConfigResolver.resolve(
1149
+ { ...rest, outputRoots: outputRootsResolved },
1150
+ { logger: logger5 }
1151
+ );
1128
1152
  const check = config.getCheck();
1129
1153
  const isPreview = config.isPreviewMode();
1130
1154
  const modePrefix = isPreview ? "[DRY RUN]" : "";
1131
1155
  logger5.debug("Generating files...");
1132
- if (!await checkRulesyncDirExists({ baseDir: process.cwd() })) {
1156
+ if (!await checkRulesyncDirExists({ inputRoot: config.getInputRoot() })) {
1133
1157
  throw new CLIError(
1134
1158
  ".rulesync directory not found. Run 'rulesync init' first.",
1135
1159
  ErrorCodes.RULESYNC_DIR_NOT_FOUND
1136
1160
  );
1137
1161
  }
1138
- logger5.debug(`Base directories: ${config.getBaseDirs().join(", ")}`);
1162
+ logger5.debug(`Output roots: ${config.getOutputRoots().join(", ")}`);
1139
1163
  const features = config.getFeatures();
1140
1164
  if (features.includes("ignore")) {
1141
1165
  logger5.debug("Generating ignore files...");
@@ -1289,6 +1313,12 @@ var GITIGNORE_ENTRY_REGISTRY = [
1289
1313
  // Cursor
1290
1314
  { target: "cursor", feature: "rules", entry: "**/.cursor/" },
1291
1315
  { target: "cursor", feature: "ignore", entry: "**/.cursorignore" },
1316
+ // .cursor/cli.json (project) and .cursor/cli-config.json (global) are
1317
+ // already covered by the broader **/.cursor/ entry above; the additional
1318
+ // `permissions` and `hooks` tags below register the same prefix under the
1319
+ // matching feature so target+feature filtering still resolves correctly.
1320
+ { target: "cursor", feature: "permissions", entry: "**/.cursor/" },
1321
+ { target: "cursor", feature: "hooks", entry: "**/.cursor/" },
1292
1322
  // deepagents-cli
1293
1323
  { target: "deepagents", feature: "rules", entry: "**/.deepagents/AGENTS.md" },
1294
1324
  { target: "deepagents", feature: "rules", entry: "**/.deepagents/memories/" },
@@ -1356,6 +1386,15 @@ var GITIGNORE_ENTRY_REGISTRY = [
1356
1386
  feature: "mcp",
1357
1387
  entry: "**/.copilot/mcp-config.json"
1358
1388
  },
1389
+ // Project: <project>/.github/agents/*.agent.md
1390
+ // Global: ~/.copilot/agents/
1391
+ { target: "copilotcli", feature: "subagents", entry: "**/.github/agents/" },
1392
+ { target: "copilotcli", feature: "subagents", entry: "**/.copilot/agents/" },
1393
+ // Project: <project>/.github/hooks/copilotcli-hooks.json
1394
+ // Global: ~/.copilot/hooks/copilot-hooks.json (rulesync convention pending
1395
+ // official documentation of a global hooks location)
1396
+ { target: "copilotcli", feature: "hooks", entry: "**/.github/hooks/" },
1397
+ { target: "copilotcli", feature: "hooks", entry: "**/.copilot/hooks/" },
1359
1398
  // Junie
1360
1399
  { target: "junie", feature: "rules", entry: "**/.junie/guidelines.md" },
1361
1400
  { target: "junie", feature: "mcp", entry: "**/.junie/mcp.json" },
@@ -1747,7 +1786,7 @@ async function importCommand(logger5, options) {
1747
1786
  if (options.targets.length > 1) {
1748
1787
  throw new CLIError("Only one tool can be imported at a time", ErrorCodes.IMPORT_FAILED);
1749
1788
  }
1750
- const config = await ConfigResolver.resolve(options);
1789
+ const config = await ConfigResolver.resolve(options, { logger: logger5 });
1751
1790
  const tool = config.getTargets()[0];
1752
1791
  logger5.debug(`Importing files from ${tool}...`);
1753
1792
  const result = await importFromTool({ config, tool, logger: logger5 });
@@ -1805,7 +1844,7 @@ async function createConfigFile() {
1805
1844
  $schema: RULESYNC_CONFIG_SCHEMA_URL,
1806
1845
  targets: ["copilot", "cursor", "claudecode", "codexcli"],
1807
1846
  features: ["rules", "ignore", "mcp", "commands", "subagents", "skills", "hooks"],
1808
- baseDirs: ["."],
1847
+ outputRoots: ["."],
1809
1848
  delete: true,
1810
1849
  verbose: false,
1811
1850
  silent: false,
@@ -2087,8 +2126,8 @@ var ApmLockSchema = z4.looseObject({
2087
2126
  dependencies: z4.array(ApmLockDependencySchema),
2088
2127
  mcp_servers: optional(z4.array(z4.string()))
2089
2128
  });
2090
- function getApmLockPath(baseDir) {
2091
- return join4(baseDir, APM_LOCKFILE_FILE_NAME);
2129
+ function getApmLockPath(projectRoot) {
2130
+ return join4(projectRoot, APM_LOCKFILE_FILE_NAME);
2092
2131
  }
2093
2132
  function createEmptyApmLock(params) {
2094
2133
  const base = params.existingLock ? { ...params.existingLock } : {};
@@ -2121,8 +2160,8 @@ ${issues}`);
2121
2160
  }
2122
2161
  return parsed.data;
2123
2162
  }
2124
- async function readApmLock(baseDir) {
2125
- const path2 = getApmLockPath(baseDir);
2163
+ async function readApmLock(projectRoot) {
2164
+ const path2 = getApmLockPath(projectRoot);
2126
2165
  if (!await fileExists(path2)) {
2127
2166
  return null;
2128
2167
  }
@@ -2130,7 +2169,7 @@ async function readApmLock(baseDir) {
2130
2169
  return parseApmLock(content);
2131
2170
  }
2132
2171
  async function writeApmLock(params) {
2133
- const path2 = getApmLockPath(params.baseDir);
2172
+ const path2 = getApmLockPath(params.projectRoot);
2134
2173
  const content = serializeApmLock(params.lock);
2135
2174
  await writeFileContent(path2, content);
2136
2175
  }
@@ -2164,11 +2203,11 @@ var ApmManifestSchema = z5.looseObject({
2164
2203
  })
2165
2204
  )
2166
2205
  });
2167
- function getApmManifestPath(baseDir) {
2168
- return join5(baseDir, APM_MANIFEST_FILE_NAME);
2206
+ function getApmManifestPath(projectRoot) {
2207
+ return join5(projectRoot, APM_MANIFEST_FILE_NAME);
2169
2208
  }
2170
- async function apmManifestExists(baseDir) {
2171
- return fileExists(getApmManifestPath(baseDir));
2209
+ async function apmManifestExists(projectRoot) {
2210
+ return fileExists(getApmManifestPath(projectRoot));
2172
2211
  }
2173
2212
  function parseApmManifest(content) {
2174
2213
  const loaded = load2(content);
@@ -2190,8 +2229,8 @@ function parseApmManifest(content) {
2190
2229
  dependencies
2191
2230
  };
2192
2231
  }
2193
- async function readApmManifest(baseDir) {
2194
- const path2 = getApmManifestPath(baseDir);
2232
+ async function readApmManifest(projectRoot) {
2233
+ const path2 = getApmManifestPath(projectRoot);
2195
2234
  const content = await readFileContent(path2);
2196
2235
  return parseApmManifest(content);
2197
2236
  }
@@ -2344,13 +2383,13 @@ var APM_PRIMITIVES = [
2344
2383
  }
2345
2384
  ];
2346
2385
  async function installApm(params) {
2347
- const { baseDir, options = {}, logger: logger5 } = params;
2348
- const manifest = await readApmManifest(baseDir);
2386
+ const { projectRoot, options = {}, logger: logger5 } = params;
2387
+ const manifest = await readApmManifest(projectRoot);
2349
2388
  if (manifest.dependencies.length === 0) {
2350
2389
  logger5.warn("apm.yml has no dependencies.apm entries. Nothing to install.");
2351
2390
  return { dependenciesProcessed: 0, deployedFileCount: 0, failedDependencyCount: 0 };
2352
2391
  }
2353
- const existingLock = await readApmLock(baseDir);
2392
+ const existingLock = await readApmLock(projectRoot);
2354
2393
  if (options.frozen) {
2355
2394
  if (!existingLock) {
2356
2395
  throw new Error(
@@ -2394,7 +2433,7 @@ async function installApm(params) {
2394
2433
  dep,
2395
2434
  client,
2396
2435
  semaphore,
2397
- baseDir,
2436
+ projectRoot,
2398
2437
  existingLock,
2399
2438
  frozen,
2400
2439
  update: options.update ?? false,
@@ -2449,19 +2488,19 @@ async function installApm(params) {
2449
2488
  continue;
2450
2489
  }
2451
2490
  try {
2452
- checkPathTraversal({ relativePath, intendedRootDir: baseDir });
2491
+ checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
2453
2492
  } catch {
2454
- logger5.warn(`Refusing to remove stale apm file outside baseDir: "${relativePath}".`);
2493
+ logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
2455
2494
  continue;
2456
2495
  }
2457
- const absolute = join6(baseDir, relativePath);
2496
+ const absolute = join6(projectRoot, relativePath);
2458
2497
  await removeFile(absolute);
2459
2498
  logger5.debug(`Removed stale apm file: ${relativePath}`);
2460
2499
  }
2461
2500
  }
2462
2501
  if (!frozen) {
2463
2502
  newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
2464
- await writeApmLock({ baseDir, lock: newLock });
2503
+ await writeApmLock({ projectRoot, lock: newLock });
2465
2504
  if (failedCount === 0) {
2466
2505
  logger5.debug("rulesync-apm.lock.yaml updated.");
2467
2506
  } else {
@@ -2477,7 +2516,7 @@ async function installApm(params) {
2477
2516
  };
2478
2517
  }
2479
2518
  async function installDependency(params) {
2480
- const { dep, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
2519
+ const { dep, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
2481
2520
  const repoUrl = canonicalRepoUrl(dep);
2482
2521
  const locked = existingLock ? findApmLockDependency(existingLock, repoUrl) : void 0;
2483
2522
  let resolvedRef;
@@ -2521,7 +2560,7 @@ async function installDependency(params) {
2521
2560
  const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
2522
2561
  checkPathTraversal({
2523
2562
  relativePath: deployRelative,
2524
- intendedRootDir: baseDir
2563
+ intendedRootDir: projectRoot
2525
2564
  });
2526
2565
  const content = await withSemaphore(
2527
2566
  semaphore,
@@ -2536,7 +2575,7 @@ async function installDependency(params) {
2536
2575
  }
2537
2576
  deployed.push({ path: deployRelative, content });
2538
2577
  if (!frozen) {
2539
- await writeFileContent(join6(baseDir, deployRelative), content);
2578
+ await writeFileContent(join6(projectRoot, deployRelative), content);
2540
2579
  }
2541
2580
  }
2542
2581
  }
@@ -2558,7 +2597,7 @@ async function installDependency(params) {
2558
2597
  }
2559
2598
  if (frozen) {
2560
2599
  for (const { path: deployRelative, content } of deployed) {
2561
- await writeFileContent(join6(baseDir, deployRelative), content);
2600
+ await writeFileContent(join6(projectRoot, deployRelative), content);
2562
2601
  }
2563
2602
  }
2564
2603
  const lockEntry = {
@@ -2706,8 +2745,8 @@ var GhLockSchema = z6.looseObject({
2706
2745
  generated_at: z6.string(),
2707
2746
  installations: z6.array(GhLockInstallationSchema)
2708
2747
  });
2709
- function getGhLockPath(baseDir) {
2710
- return join7(baseDir, GH_LOCKFILE_FILE_NAME);
2748
+ function getGhLockPath(projectRoot) {
2749
+ return join7(projectRoot, GH_LOCKFILE_FILE_NAME);
2711
2750
  }
2712
2751
  function createEmptyGhLock(params) {
2713
2752
  const base = params?.existingLock ? { ...params.existingLock } : {};
@@ -2739,8 +2778,8 @@ ${issues}`);
2739
2778
  }
2740
2779
  return parsed.data;
2741
2780
  }
2742
- async function readGhLock(baseDir) {
2743
- const path2 = getGhLockPath(baseDir);
2781
+ async function readGhLock(projectRoot) {
2782
+ const path2 = getGhLockPath(projectRoot);
2744
2783
  if (!await fileExists(path2)) {
2745
2784
  return null;
2746
2785
  }
@@ -2748,7 +2787,7 @@ async function readGhLock(baseDir) {
2748
2787
  return parseGhLock(content);
2749
2788
  }
2750
2789
  async function writeGhLock(params) {
2751
- const path2 = getGhLockPath(params.baseDir);
2790
+ const path2 = getGhLockPath(params.projectRoot);
2752
2791
  const content = serializeGhLock(params.lock);
2753
2792
  await writeFileContent(path2, content);
2754
2793
  }
@@ -2800,7 +2839,7 @@ function relativeInstallDirFor(params) {
2800
2839
  var SKILLS_REMOTE_DIR = "skills";
2801
2840
  var SKILL_FILE_NAME2 = "SKILL.md";
2802
2841
  async function installGh(params) {
2803
- const { baseDir, sources, options = {}, logger: logger5 } = params;
2842
+ const { projectRoot, sources, options = {}, logger: logger5 } = params;
2804
2843
  if (sources.length === 0) {
2805
2844
  return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
2806
2845
  }
@@ -2837,7 +2876,7 @@ async function installGh(params) {
2837
2876
  scope
2838
2877
  };
2839
2878
  });
2840
- const existingLock = await readGhLock(baseDir);
2879
+ const existingLock = await readGhLock(projectRoot);
2841
2880
  const frozen = options.frozen ?? false;
2842
2881
  const update = options.update ?? false;
2843
2882
  if (frozen && !existingLock) {
@@ -2888,7 +2927,7 @@ async function installGh(params) {
2888
2927
  rs,
2889
2928
  client,
2890
2929
  semaphore,
2891
- baseDir,
2930
+ projectRoot,
2892
2931
  existingLock,
2893
2932
  frozen,
2894
2933
  update,
@@ -2951,7 +2990,7 @@ async function installGh(params) {
2951
2990
  await removeStaleFile({
2952
2991
  relativePath: deployed,
2953
2992
  scope: prev.scope === "user" ? "user" : "project",
2954
- baseDir,
2993
+ projectRoot,
2955
2994
  logger: logger5
2956
2995
  });
2957
2996
  }
@@ -2959,7 +2998,7 @@ async function installGh(params) {
2959
2998
  }
2960
2999
  if (!frozen) {
2961
3000
  newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
2962
- await writeGhLock({ baseDir, lock: newLock });
3001
+ await writeGhLock({ projectRoot, lock: newLock });
2963
3002
  if (failedCount === 0) {
2964
3003
  logger5.debug("rulesync-gh.lock.yaml updated.");
2965
3004
  } else {
@@ -2975,7 +3014,7 @@ async function installGh(params) {
2975
3014
  };
2976
3015
  }
2977
3016
  async function installSource(params) {
2978
- const { rs, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
3017
+ const { rs, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
2979
3018
  const { entry, owner, repo, agent, scope } = rs;
2980
3019
  const sourceKey = entry.source;
2981
3020
  let resolvedRef;
@@ -3050,7 +3089,7 @@ async function installSource(params) {
3050
3089
  }
3051
3090
  const results = [];
3052
3091
  const installRelDir = relativeInstallDirFor({ agent, scope });
3053
- const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
3092
+ const scopeRoot = scope === "user" ? getHomeDirectory() : projectRoot;
3054
3093
  const sourceUrl = `https://github.com/${owner}/${repo}`;
3055
3094
  const repository = `${owner}/${repo}`;
3056
3095
  const provenanceRef = usedTag ? resolvedRef : resolvedSha;
@@ -3166,12 +3205,12 @@ ${content}`;
3166
3205
  return results;
3167
3206
  }
3168
3207
  async function removeStaleFile(params) {
3169
- const { relativePath, scope, baseDir, logger: logger5 } = params;
3208
+ const { relativePath, scope, projectRoot, logger: logger5 } = params;
3170
3209
  if (posix3.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
3171
3210
  logger5.warn(`Refusing to remove stale gh file with suspicious path: "${relativePath}".`);
3172
3211
  return;
3173
3212
  }
3174
- const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
3213
+ const scopeRoot = scope === "user" ? getHomeDirectory() : projectRoot;
3175
3214
  try {
3176
3215
  checkPathTraversal({ relativePath, intendedRootDir: scopeRoot });
3177
3216
  } catch {
@@ -3343,7 +3382,7 @@ async function fetchSkillFiles(params) {
3343
3382
  var MAX_WALK_DEPTH = 20;
3344
3383
  var MAX_TOTAL_FILES = 1e4;
3345
3384
  var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
3346
- async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }, logger5) {
3385
+ async function walkDirectory(dir, outputRoot, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }, logger5) {
3347
3386
  if (depth > MAX_WALK_DEPTH) {
3348
3387
  throw new GitClientError(
3349
3388
  `Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
@@ -3358,7 +3397,7 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
3358
3397
  continue;
3359
3398
  }
3360
3399
  if (await directoryExists(fullPath)) {
3361
- results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx, logger5));
3400
+ results.push(...await walkDirectory(fullPath, outputRoot, depth + 1, ctx, logger5));
3362
3401
  } else {
3363
3402
  const size = await getFileSize(fullPath);
3364
3403
  if (size > MAX_FILE_SIZE) {
@@ -3380,7 +3419,7 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
3380
3419
  );
3381
3420
  }
3382
3421
  const content = await readFileContent(fullPath);
3383
- results.push({ relativePath: relative(baseDir, fullPath), content, size });
3422
+ results.push({ relativePath: relative(outputRoot, fullPath), content, size });
3384
3423
  }
3385
3424
  }
3386
3425
  return results;
@@ -3434,7 +3473,7 @@ function createEmptyLock() {
3434
3473
  }
3435
3474
  async function readLockFile(params) {
3436
3475
  const { logger: logger5 } = params;
3437
- const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
3476
+ const lockPath = join11(params.projectRoot, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
3438
3477
  if (!await fileExists(lockPath)) {
3439
3478
  logger5.debug("No sources lockfile found, starting fresh.");
3440
3479
  return createEmptyLock();
@@ -3463,7 +3502,7 @@ async function readLockFile(params) {
3463
3502
  }
3464
3503
  async function writeLockFile(params) {
3465
3504
  const { logger: logger5 } = params;
3466
- const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
3505
+ const lockPath = join11(params.projectRoot, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
3467
3506
  const content = JSON.stringify(params.lock, null, 2) + "\n";
3468
3507
  await writeFileContent(lockPath, content);
3469
3508
  logger5.debug(`Wrote sources lockfile to ${lockPath}`);
@@ -3538,7 +3577,7 @@ function getLockedSkillNames(entry) {
3538
3577
 
3539
3578
  // src/lib/sources.ts
3540
3579
  async function resolveAndFetchSources(params) {
3541
- const { sources, baseDir, options = {}, logger: logger5 } = params;
3580
+ const { sources, projectRoot, options = {}, logger: logger5 } = params;
3542
3581
  if (sources.length === 0) {
3543
3582
  return { fetchedSkillCount: 0, sourcesProcessed: 0 };
3544
3583
  }
@@ -3546,7 +3585,7 @@ async function resolveAndFetchSources(params) {
3546
3585
  logger5.info("Skipping source fetching.");
3547
3586
  return { fetchedSkillCount: 0, sourcesProcessed: 0 };
3548
3587
  }
3549
- let lock = options.updateSources ? createEmptyLock() : await readLockFile({ baseDir, logger: logger5 });
3588
+ let lock = options.updateSources ? createEmptyLock() : await readLockFile({ projectRoot, logger: logger5 });
3550
3589
  if (options.frozen) {
3551
3590
  const missingKeys = [];
3552
3591
  for (const source of sources) {
@@ -3564,7 +3603,7 @@ async function resolveAndFetchSources(params) {
3564
3603
  const originalLockJson = JSON.stringify(lock);
3565
3604
  const token = GitHubClient.resolveToken(options.token);
3566
3605
  const client = new GitHubClient({ token });
3567
- const localSkillNames = await getLocalSkillDirNames(baseDir);
3606
+ const localSkillNames = await getLocalSkillDirNames(projectRoot);
3568
3607
  let totalSkillCount = 0;
3569
3608
  const allFetchedSkillNames = /* @__PURE__ */ new Set();
3570
3609
  for (const sourceEntry of sources) {
@@ -3574,7 +3613,7 @@ async function resolveAndFetchSources(params) {
3574
3613
  if (transport === "git") {
3575
3614
  result = await fetchSourceViaGit({
3576
3615
  sourceEntry,
3577
- baseDir,
3616
+ projectRoot,
3578
3617
  lock,
3579
3618
  localSkillNames,
3580
3619
  alreadyFetchedSkillNames: allFetchedSkillNames,
@@ -3586,7 +3625,7 @@ async function resolveAndFetchSources(params) {
3586
3625
  result = await fetchSource({
3587
3626
  sourceEntry,
3588
3627
  client,
3589
- baseDir,
3628
+ projectRoot,
3590
3629
  lock,
3591
3630
  localSkillNames,
3592
3631
  alreadyFetchedSkillNames: allFetchedSkillNames,
@@ -3620,7 +3659,7 @@ async function resolveAndFetchSources(params) {
3620
3659
  }
3621
3660
  lock = { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
3622
3661
  if (!options.frozen && JSON.stringify(lock) !== originalLockJson) {
3623
- await writeLockFile({ baseDir, lock, logger: logger5 });
3662
+ await writeLockFile({ projectRoot, lock, logger: logger5 });
3624
3663
  } else {
3625
3664
  logger5.debug("Lockfile unchanged, skipping write.");
3626
3665
  }
@@ -3771,7 +3810,7 @@ async function fetchSource(params) {
3771
3810
  const {
3772
3811
  sourceEntry,
3773
3812
  client,
3774
- baseDir,
3813
+ projectRoot,
3775
3814
  localSkillNames,
3776
3815
  alreadyFetchedSkillNames,
3777
3816
  updateSources,
@@ -3800,7 +3839,7 @@ async function fetchSource(params) {
3800
3839
  ref = resolvedSha;
3801
3840
  logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
3802
3841
  }
3803
- const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
3842
+ const curatedDir = join12(projectRoot, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
3804
3843
  if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
3805
3844
  const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
3806
3845
  if (allExist) {
@@ -3948,7 +3987,7 @@ async function fetchSource(params) {
3948
3987
  async function fetchSourceViaGit(params) {
3949
3988
  const {
3950
3989
  sourceEntry,
3951
- baseDir,
3990
+ projectRoot,
3952
3991
  localSkillNames,
3953
3992
  alreadyFetchedSkillNames,
3954
3993
  updateSources,
@@ -3975,7 +4014,7 @@ async function fetchSourceViaGit(params) {
3975
4014
  requestedRef = def.ref;
3976
4015
  resolvedSha = def.sha;
3977
4016
  }
3978
- const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
4017
+ const curatedDir = join12(projectRoot, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
3979
4018
  if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
3980
4019
  if (await checkLockedSkillsExist(curatedDir, lockedSkillNames)) {
3981
4020
  return { skillCount: 0, fetchedSkillNames: lockedSkillNames, updatedLock: lock };
@@ -4057,8 +4096,8 @@ async function installCommand(logger5, options) {
4057
4096
  await runRulesyncInstall(logger5, options);
4058
4097
  }
4059
4098
  async function runRulesyncInstall(logger5, options) {
4060
- const baseDir = process.cwd();
4061
- const apmExists = await apmManifestExists(baseDir);
4099
+ const projectRoot = process.cwd();
4100
+ const apmExists = await apmManifestExists(projectRoot);
4062
4101
  const config = await ConfigResolver.resolve({
4063
4102
  configPath: options.configPath,
4064
4103
  verbose: options.verbose,
@@ -4083,7 +4122,7 @@ async function runRulesyncInstall(logger5, options) {
4083
4122
  logger5.debug(`Installing skills from ${sources.length} source(s)...`);
4084
4123
  const result = await resolveAndFetchSources({
4085
4124
  sources,
4086
- baseDir,
4125
+ projectRoot,
4087
4126
  options: {
4088
4127
  updateSources: options.update,
4089
4128
  frozen: options.frozen,
@@ -4104,14 +4143,14 @@ async function runRulesyncInstall(logger5, options) {
4104
4143
  }
4105
4144
  }
4106
4145
  async function runApmInstall(logger5, options) {
4107
- const baseDir = process.cwd();
4108
- if (!await apmManifestExists(baseDir)) {
4146
+ const projectRoot = process.cwd();
4147
+ if (!await apmManifestExists(projectRoot)) {
4109
4148
  throw new Error(
4110
4149
  "--mode apm requires an apm.yml at the project root. Create one or drop --mode apm to fall back to rulesync mode."
4111
4150
  );
4112
4151
  }
4113
4152
  const result = await installApm({
4114
- baseDir,
4153
+ projectRoot,
4115
4154
  options: {
4116
4155
  update: options.update,
4117
4156
  frozen: options.frozen,
@@ -4138,7 +4177,7 @@ async function runApmInstall(logger5, options) {
4138
4177
  }
4139
4178
  }
4140
4179
  async function runGhInstall(logger5, options) {
4141
- const baseDir = process.cwd();
4180
+ const projectRoot = process.cwd();
4142
4181
  const config = await ConfigResolver.resolve({
4143
4182
  configPath: options.configPath,
4144
4183
  verbose: options.verbose,
@@ -4150,7 +4189,7 @@ async function runGhInstall(logger5, options) {
4150
4189
  return;
4151
4190
  }
4152
4191
  const result = await installGh({
4153
- baseDir,
4192
+ projectRoot,
4154
4193
  sources,
4155
4194
  options: {
4156
4195
  update: options.update,
@@ -4273,7 +4312,7 @@ async function putCommand({
4273
4312
  }
4274
4313
  const fileContent = stringifyFrontmatter(body, frontmatter);
4275
4314
  const command = new RulesyncCommand({
4276
- baseDir: process.cwd(),
4315
+ outputRoot: process.cwd(),
4277
4316
  relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
4278
4317
  relativeFilePath: filename,
4279
4318
  frontmatter,
@@ -4418,7 +4457,7 @@ async function executeConvert(options) {
4418
4457
  features: options.features ?? ["*"],
4419
4458
  global: options.global,
4420
4459
  dryRun: options.dryRun,
4421
- // Always use default baseDirs (process.cwd()) and configPath
4460
+ // Always use default outputRoots (process.cwd()) and configPath
4422
4461
  // verbose and silent are meaningless in MCP context
4423
4462
  verbose: false,
4424
4463
  silent: true
@@ -4486,7 +4525,7 @@ var generateOptionsSchema = z10.object({
4486
4525
  });
4487
4526
  async function executeGenerate(options = {}) {
4488
4527
  try {
4489
- const exists = await checkRulesyncDirExists({ baseDir: process.cwd() });
4528
+ const exists = await checkRulesyncDirExists({ inputRoot: process.cwd() });
4490
4529
  if (!exists) {
4491
4530
  return {
4492
4531
  success: false,
@@ -4503,7 +4542,7 @@ async function executeGenerate(options = {}) {
4503
4542
  simulateCommands: options.simulateCommands,
4504
4543
  simulateSubagents: options.simulateSubagents,
4505
4544
  simulateSkills: options.simulateSkills,
4506
- // Always use default baseDirs (process.cwd()) and configPath
4545
+ // Always use default outputRoots (process.cwd()) and configPath
4507
4546
  // verbose and silent are meaningless in MCP context
4508
4547
  verbose: false,
4509
4548
  silent: true
@@ -4603,19 +4642,19 @@ async function putHooksFile({ content }) {
4603
4642
  );
4604
4643
  }
4605
4644
  try {
4606
- const baseDir = process.cwd();
4645
+ const outputRoot = process.cwd();
4607
4646
  const paths = RulesyncHooks.getSettablePaths();
4608
4647
  const relativeDirPath = paths.relativeDirPath;
4609
4648
  const relativeFilePath = paths.relativeFilePath;
4610
- const fullPath = join14(baseDir, relativeDirPath, relativeFilePath);
4649
+ const fullPath = join14(outputRoot, relativeDirPath, relativeFilePath);
4611
4650
  const rulesyncHooks = new RulesyncHooks({
4612
- baseDir,
4651
+ outputRoot,
4613
4652
  relativeDirPath,
4614
4653
  relativeFilePath,
4615
4654
  fileContent: content,
4616
4655
  validate: true
4617
4656
  });
4618
- await ensureDir(join14(baseDir, relativeDirPath));
4657
+ await ensureDir(join14(outputRoot, relativeDirPath));
4619
4658
  await writeFileContent(fullPath, content);
4620
4659
  const relativePathFromCwd = join14(relativeDirPath, relativeFilePath);
4621
4660
  return {
@@ -4633,9 +4672,9 @@ async function putHooksFile({ content }) {
4633
4672
  }
4634
4673
  async function deleteHooksFile() {
4635
4674
  try {
4636
- const baseDir = process.cwd();
4675
+ const outputRoot = process.cwd();
4637
4676
  const paths = RulesyncHooks.getSettablePaths();
4638
- const filePath = join14(baseDir, paths.relativeDirPath, paths.relativeFilePath);
4677
+ const filePath = join14(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
4639
4678
  await removeFile(filePath);
4640
4679
  const relativePathFromCwd = join14(paths.relativeDirPath, paths.relativeFilePath);
4641
4680
  return {
@@ -4809,7 +4848,7 @@ async function executeImport(options) {
4809
4848
  // eslint-disable-next-line no-type-assertion/no-type-assertion
4810
4849
  features: options.features,
4811
4850
  global: options.global,
4812
- // Always use default baseDirs (process.cwd()) and configPath
4851
+ // Always use default outputRoots (process.cwd()) and configPath
4813
4852
  // verbose and silent are meaningless in MCP context
4814
4853
  verbose: false,
4815
4854
  silent: true
@@ -4906,19 +4945,19 @@ async function putMcpFile({ content }) {
4906
4945
  );
4907
4946
  }
4908
4947
  try {
4909
- const baseDir = process.cwd();
4948
+ const outputRoot = process.cwd();
4910
4949
  const paths = RulesyncMcp.getSettablePaths();
4911
4950
  const relativeDirPath = paths.recommended.relativeDirPath;
4912
4951
  const relativeFilePath = paths.recommended.relativeFilePath;
4913
- const fullPath = join16(baseDir, relativeDirPath, relativeFilePath);
4952
+ const fullPath = join16(outputRoot, relativeDirPath, relativeFilePath);
4914
4953
  const rulesyncMcp = new RulesyncMcp({
4915
- baseDir,
4954
+ outputRoot,
4916
4955
  relativeDirPath,
4917
4956
  relativeFilePath,
4918
4957
  fileContent: content,
4919
4958
  validate: true
4920
4959
  });
4921
- await ensureDir(join16(baseDir, relativeDirPath));
4960
+ await ensureDir(join16(outputRoot, relativeDirPath));
4922
4961
  await writeFileContent(fullPath, content);
4923
4962
  const relativePathFromCwd = join16(relativeDirPath, relativeFilePath);
4924
4963
  return {
@@ -4936,14 +4975,18 @@ async function putMcpFile({ content }) {
4936
4975
  }
4937
4976
  async function deleteMcpFile() {
4938
4977
  try {
4939
- const baseDir = process.cwd();
4978
+ const outputRoot = process.cwd();
4940
4979
  const paths = RulesyncMcp.getSettablePaths();
4941
4980
  const recommendedPath = join16(
4942
- baseDir,
4981
+ outputRoot,
4943
4982
  paths.recommended.relativeDirPath,
4944
4983
  paths.recommended.relativeFilePath
4945
4984
  );
4946
- const legacyPath = join16(baseDir, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
4985
+ const legacyPath = join16(
4986
+ outputRoot,
4987
+ paths.legacy.relativeDirPath,
4988
+ paths.legacy.relativeFilePath
4989
+ );
4947
4990
  await removeFile(recommendedPath);
4948
4991
  await removeFile(legacyPath);
4949
4992
  const relativePathFromCwd = join16(
@@ -5042,19 +5085,19 @@ async function putPermissionsFile({ content }) {
5042
5085
  );
5043
5086
  }
5044
5087
  try {
5045
- const baseDir = process.cwd();
5088
+ const outputRoot = process.cwd();
5046
5089
  const paths = RulesyncPermissions.getSettablePaths();
5047
5090
  const relativeDirPath = paths.relativeDirPath;
5048
5091
  const relativeFilePath = paths.relativeFilePath;
5049
- const fullPath = join17(baseDir, relativeDirPath, relativeFilePath);
5092
+ const fullPath = join17(outputRoot, relativeDirPath, relativeFilePath);
5050
5093
  const rulesyncPermissions = new RulesyncPermissions({
5051
- baseDir,
5094
+ outputRoot,
5052
5095
  relativeDirPath,
5053
5096
  relativeFilePath,
5054
5097
  fileContent: content,
5055
5098
  validate: true
5056
5099
  });
5057
- await ensureDir(join17(baseDir, relativeDirPath));
5100
+ await ensureDir(join17(outputRoot, relativeDirPath));
5058
5101
  await writeFileContent(fullPath, content);
5059
5102
  const relativePathFromCwd = join17(relativeDirPath, relativeFilePath);
5060
5103
  return {
@@ -5072,9 +5115,9 @@ async function putPermissionsFile({ content }) {
5072
5115
  }
5073
5116
  async function deletePermissionsFile() {
5074
5117
  try {
5075
- const baseDir = process.cwd();
5118
+ const outputRoot = process.cwd();
5076
5119
  const paths = RulesyncPermissions.getSettablePaths();
5077
- const filePath = join17(baseDir, paths.relativeDirPath, paths.relativeFilePath);
5120
+ const filePath = join17(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
5078
5121
  await removeFile(filePath);
5079
5122
  const relativePathFromCwd = join17(paths.relativeDirPath, paths.relativeFilePath);
5080
5123
  return {
@@ -5212,7 +5255,7 @@ async function putRule({
5212
5255
  );
5213
5256
  }
5214
5257
  const rule = new RulesyncRule({
5215
- baseDir: process.cwd(),
5258
+ outputRoot: process.cwd(),
5216
5259
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
5217
5260
  relativeFilePath: filename,
5218
5261
  frontmatter,
@@ -5419,7 +5462,7 @@ async function putSkill({
5419
5462
  }
5420
5463
  const aiDirFiles = otherFiles.map(mcpSkillFileToAiDirFile);
5421
5464
  const skill = new RulesyncSkill({
5422
- baseDir: process.cwd(),
5465
+ outputRoot: process.cwd(),
5423
5466
  relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
5424
5467
  dirName,
5425
5468
  frontmatter,
@@ -5636,7 +5679,7 @@ async function putSubagent({
5636
5679
  );
5637
5680
  }
5638
5681
  const subagent = new RulesyncSubagent({
5639
- baseDir: process.cwd(),
5682
+ outputRoot: process.cwd(),
5640
5683
  relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
5641
5684
  relativeFilePath: filename,
5642
5685
  frontmatter,
@@ -6435,7 +6478,7 @@ function wrapCommand({
6435
6478
  }
6436
6479
 
6437
6480
  // src/cli/index.ts
6438
- var getVersion = () => "8.12.0";
6481
+ var getVersion = () => "8.14.0";
6439
6482
  function wrapCommand2(name, errorCode, handler) {
6440
6483
  return wrapCommand({ name, errorCode, handler, getVersion });
6441
6484
  }
@@ -6552,8 +6595,12 @@ var main = async () => {
6552
6595
  `Comma-separated list of features to generate (${ALL_FEATURES.join(",")}) or '*' for all`,
6553
6596
  parseCommaSeparatedList
6554
6597
  ).option("--delete", "Delete all existing files in output directories before generating").option(
6598
+ "-o, --output-roots <paths>",
6599
+ "Output root directories to generate files into (comma-separated for multiple paths)",
6600
+ parseCommaSeparatedList
6601
+ ).option(
6555
6602
  "-b, --base-dir <paths>",
6556
- "Base directories to generate files (comma-separated for multiple paths)",
6603
+ "[Deprecated] Use --output-roots instead. Output root directories (comma-separated for multiple paths)",
6557
6604
  parseCommaSeparatedList
6558
6605
  ).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-c, --config <path>", "Path to configuration file").option("-g, --global", "Generate for global(user scope) configuration files").option(
6559
6606
  "--simulate-commands",
@@ -6564,6 +6611,9 @@ var main = async () => {
6564
6611
  ).option(
6565
6612
  "--simulate-skills",
6566
6613
  "Generate simulated skills. This feature is only available for copilot, cursor and codexcli."
6614
+ ).option(
6615
+ "--input-root <path>",
6616
+ "Path to the directory containing .rulesync/ (parent of .rulesync/)"
6567
6617
  ).option("--dry-run", "Dry run: show changes without writing files").option("--check", "Check if files are up to date (exits with code 1 if changes needed)").action(
6568
6618
  wrapCommand2("generate", "GENERATION_FAILED", async (logger5, options) => {
6569
6619
  await generateCommand(logger5, options);