rulesync 6.6.0 → 6.6.2

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/README.md CHANGED
@@ -120,6 +120,9 @@ Get-FileHash rulesync.exe -Algorithm SHA256 | ForEach-Object {
120
120
  ```bash
121
121
  # Create necessary directories, sample rule files, and configuration file
122
122
  npx rulesync init
123
+
124
+ # Install official skills (recommended)
125
+ npx rulesync fetch dyoshikawa/rulesync --features skills
123
126
  ```
124
127
 
125
128
  On the other hand, if you already have AI tool configurations:
@@ -231,7 +234,7 @@ npx rulesync generate --targets "*" --features rules
231
234
  # Generate simulated commands and subagents
232
235
  npx rulesync generate --targets copilot,cursor,codexcli --features commands,subagents --simulate-commands --simulate-subagents
233
236
 
234
- # Preview changes without writing files (dry-run mode)
237
+ # Dry run: show changes without writing files
235
238
  npx rulesync generate --dry-run --targets claudecode --features rules
236
239
 
237
240
  # Check if files are up to date (for CI/CD pipelines)
@@ -250,13 +253,13 @@ npx rulesync update --check
250
253
  npx rulesync update --force
251
254
  ```
252
255
 
253
- ## Preview Modes
256
+ ## Dry Run
254
257
 
255
- Rulesync provides two preview modes for the `generate` command that allow you to see what changes would be made without actually writing files:
258
+ Rulesync provides two dry run options for the `generate` command that allow you to see what changes would be made without actually writing files:
256
259
 
257
260
  ### `--dry-run`
258
261
 
259
- Preview changes without writing any files. Shows what would be written or deleted with a `[PREVIEW]` prefix.
262
+ Show what would be written or deleted without actually writing any files. Changes are displayed with a `[DRY RUN]` prefix.
260
263
 
261
264
  ```bash
262
265
  npx rulesync generate --dry-run --targets claudecode --features rules
@@ -333,6 +336,9 @@ npx rulesync fetch owner/repo@v1.0.0 --features rules,commands
333
336
  export GITHUB_TOKEN=ghp_xxxx
334
337
  npx rulesync fetch owner/private-repo
335
338
 
339
+ # Or use GitHub CLI to get the token
340
+ GITHUB_TOKEN=$(gh auth token) npx rulesync fetch owner/private-repo
341
+
336
342
  # Preserve existing files (skip conflicts)
337
343
  npx rulesync fetch owner/repo --conflict skip
338
344
 
package/dist/index.cjs CHANGED
@@ -125,6 +125,7 @@ var Logger = class {
125
125
  var logger = new Logger();
126
126
 
127
127
  // src/lib/fetch.ts
128
+ var import_promise = require("es-toolkit/promise");
128
129
  var import_node_path107 = require("path");
129
130
 
130
131
  // src/constants/rulesync-paths.ts
@@ -10409,6 +10410,9 @@ var ToolRule = class extends ToolFile {
10409
10410
  return false;
10410
10411
  }
10411
10412
  };
10413
+ function buildToolPath(toolDir, subDir, excludeToolDir) {
10414
+ return excludeToolDir ? subDir : (0, import_node_path84.join)(toolDir, subDir);
10415
+ }
10412
10416
 
10413
10417
  // src/features/rules/agentsmd-rule.ts
10414
10418
  var AgentsMdRule = class _AgentsMdRule extends ToolRule {
@@ -10426,7 +10430,7 @@ var AgentsMdRule = class _AgentsMdRule extends ToolRule {
10426
10430
  relativeFilePath: "AGENTS.md"
10427
10431
  },
10428
10432
  nonRoot: {
10429
- relativeDirPath: _options.excludeToolDir ? "memories" : (0, import_node_path85.join)(".agents", "memories")
10433
+ relativeDirPath: buildToolPath(".agents", "memories", _options.excludeToolDir)
10430
10434
  }
10431
10435
  };
10432
10436
  }
@@ -10666,7 +10670,7 @@ var AntigravityRule = class _AntigravityRule extends ToolRule {
10666
10670
  static getSettablePaths(_options = {}) {
10667
10671
  return {
10668
10672
  nonRoot: {
10669
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path86.join)(".agent", "rules")
10673
+ relativeDirPath: buildToolPath(".agent", "rules", _options.excludeToolDir)
10670
10674
  }
10671
10675
  };
10672
10676
  }
@@ -10842,7 +10846,7 @@ var AugmentcodeLegacyRule = class _AugmentcodeLegacyRule extends ToolRule {
10842
10846
  relativeFilePath: ".augment-guidelines"
10843
10847
  },
10844
10848
  nonRoot: {
10845
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path87.join)(".augment", "rules")
10849
+ relativeDirPath: buildToolPath(".augment", "rules", _options.excludeToolDir)
10846
10850
  }
10847
10851
  };
10848
10852
  }
@@ -10915,7 +10919,7 @@ var AugmentcodeRule = class _AugmentcodeRule extends ToolRule {
10915
10919
  static getSettablePaths(_options = {}) {
10916
10920
  return {
10917
10921
  nonRoot: {
10918
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path88.join)(".augment", "rules")
10922
+ relativeDirPath: buildToolPath(".augment", "rules", _options.excludeToolDir)
10919
10923
  }
10920
10924
  };
10921
10925
  }
@@ -10984,7 +10988,7 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
10984
10988
  if (global) {
10985
10989
  return {
10986
10990
  root: {
10987
- relativeDirPath: excludeToolDir ? "." : ".claude",
10991
+ relativeDirPath: buildToolPath(".claude", ".", excludeToolDir),
10988
10992
  relativeFilePath: "CLAUDE.md"
10989
10993
  }
10990
10994
  };
@@ -10995,7 +10999,7 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
10995
10999
  relativeFilePath: "CLAUDE.md"
10996
11000
  },
10997
11001
  nonRoot: {
10998
- relativeDirPath: excludeToolDir ? "memories" : (0, import_node_path89.join)(".claude", "memories")
11002
+ relativeDirPath: buildToolPath(".claude", "memories", excludeToolDir)
10999
11003
  }
11000
11004
  };
11001
11005
  }
@@ -11099,7 +11103,7 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
11099
11103
  if (global) {
11100
11104
  return {
11101
11105
  root: {
11102
- relativeDirPath: excludeToolDir ? "." : ".claude",
11106
+ relativeDirPath: buildToolPath(".claude", ".", excludeToolDir),
11103
11107
  relativeFilePath: "CLAUDE.md"
11104
11108
  }
11105
11109
  };
@@ -11110,7 +11114,7 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
11110
11114
  relativeFilePath: "CLAUDE.md"
11111
11115
  },
11112
11116
  nonRoot: {
11113
- relativeDirPath: excludeToolDir ? "rules" : (0, import_node_path90.join)(".claude", "rules")
11117
+ relativeDirPath: buildToolPath(".claude", "rules", excludeToolDir)
11114
11118
  }
11115
11119
  };
11116
11120
  }
@@ -11376,7 +11380,7 @@ var CodexcliRule = class _CodexcliRule extends ToolRule {
11376
11380
  if (global) {
11377
11381
  return {
11378
11382
  root: {
11379
- relativeDirPath: excludeToolDir ? "." : ".codex",
11383
+ relativeDirPath: buildToolPath(".codex", ".", excludeToolDir),
11380
11384
  relativeFilePath: "AGENTS.md"
11381
11385
  }
11382
11386
  };
@@ -11387,7 +11391,7 @@ var CodexcliRule = class _CodexcliRule extends ToolRule {
11387
11391
  relativeFilePath: "AGENTS.md"
11388
11392
  },
11389
11393
  nonRoot: {
11390
- relativeDirPath: excludeToolDir ? "memories" : (0, import_node_path92.join)(".codex", "memories")
11394
+ relativeDirPath: buildToolPath(".codex", "memories", excludeToolDir)
11391
11395
  }
11392
11396
  };
11393
11397
  }
@@ -11489,11 +11493,11 @@ var CopilotRule = class _CopilotRule extends ToolRule {
11489
11493
  static getSettablePaths(_options = {}) {
11490
11494
  return {
11491
11495
  root: {
11492
- relativeDirPath: _options.excludeToolDir ? "." : ".github",
11496
+ relativeDirPath: buildToolPath(".github", ".", _options.excludeToolDir),
11493
11497
  relativeFilePath: "copilot-instructions.md"
11494
11498
  },
11495
11499
  nonRoot: {
11496
- relativeDirPath: _options.excludeToolDir ? "instructions" : (0, import_node_path93.join)(".github", "instructions")
11500
+ relativeDirPath: buildToolPath(".github", "instructions", _options.excludeToolDir)
11497
11501
  }
11498
11502
  };
11499
11503
  }
@@ -11677,7 +11681,7 @@ var CursorRule = class _CursorRule extends ToolRule {
11677
11681
  static getSettablePaths(_options = {}) {
11678
11682
  return {
11679
11683
  nonRoot: {
11680
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path94.join)(".cursor", "rules")
11684
+ relativeDirPath: buildToolPath(".cursor", "rules", _options.excludeToolDir)
11681
11685
  }
11682
11686
  };
11683
11687
  }
@@ -11879,21 +11883,21 @@ var FactorydroidRule = class _FactorydroidRule extends ToolRule {
11879
11883
  if (options?.global) {
11880
11884
  return {
11881
11885
  root: {
11882
- relativeDirPath: options.excludeToolDir ? "." : ".factorydroid",
11886
+ relativeDirPath: buildToolPath(".factorydroid", ".", options.excludeToolDir),
11883
11887
  relativeFilePath: "AGENTS.md"
11884
11888
  },
11885
11889
  nonRoot: {
11886
- relativeDirPath: options.excludeToolDir ? "memories" : (0, import_node_path95.join)(".factorydroid", "memories")
11890
+ relativeDirPath: buildToolPath(".factorydroid", "memories", options.excludeToolDir)
11887
11891
  }
11888
11892
  };
11889
11893
  }
11890
11894
  return {
11891
11895
  root: {
11892
- relativeDirPath: options?.excludeToolDir ? "." : ".factorydroid",
11896
+ relativeDirPath: buildToolPath(".factorydroid", ".", options?.excludeToolDir),
11893
11897
  relativeFilePath: "AGENTS.md"
11894
11898
  },
11895
11899
  nonRoot: {
11896
- relativeDirPath: options?.excludeToolDir ? "memories" : (0, import_node_path95.join)(".factorydroid", "memories")
11900
+ relativeDirPath: buildToolPath(".factorydroid", "memories", options?.excludeToolDir)
11897
11901
  }
11898
11902
  };
11899
11903
  }
@@ -11974,7 +11978,7 @@ var GeminiCliRule = class _GeminiCliRule extends ToolRule {
11974
11978
  if (global) {
11975
11979
  return {
11976
11980
  root: {
11977
- relativeDirPath: excludeToolDir ? "." : ".gemini",
11981
+ relativeDirPath: buildToolPath(".gemini", ".", excludeToolDir),
11978
11982
  relativeFilePath: "GEMINI.md"
11979
11983
  }
11980
11984
  };
@@ -11985,7 +11989,7 @@ var GeminiCliRule = class _GeminiCliRule extends ToolRule {
11985
11989
  relativeFilePath: "GEMINI.md"
11986
11990
  },
11987
11991
  nonRoot: {
11988
- relativeDirPath: excludeToolDir ? "memories" : (0, import_node_path96.join)(".gemini", "memories")
11992
+ relativeDirPath: buildToolPath(".gemini", "memories", excludeToolDir)
11989
11993
  }
11990
11994
  };
11991
11995
  }
@@ -12079,11 +12083,11 @@ var JunieRule = class _JunieRule extends ToolRule {
12079
12083
  static getSettablePaths(_options = {}) {
12080
12084
  return {
12081
12085
  root: {
12082
- relativeDirPath: _options.excludeToolDir ? "." : ".junie",
12086
+ relativeDirPath: buildToolPath(".junie", ".", _options.excludeToolDir),
12083
12087
  relativeFilePath: "guidelines.md"
12084
12088
  },
12085
12089
  nonRoot: {
12086
- relativeDirPath: _options.excludeToolDir ? "memories" : (0, import_node_path97.join)(".junie", "memories")
12090
+ relativeDirPath: buildToolPath(".junie", "memories", _options.excludeToolDir)
12087
12091
  }
12088
12092
  };
12089
12093
  }
@@ -12154,7 +12158,7 @@ var KiloRule = class _KiloRule extends ToolRule {
12154
12158
  static getSettablePaths(_options = {}) {
12155
12159
  return {
12156
12160
  nonRoot: {
12157
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path98.join)(".kilocode", "rules")
12161
+ relativeDirPath: buildToolPath(".kilocode", "rules", _options.excludeToolDir)
12158
12162
  }
12159
12163
  };
12160
12164
  }
@@ -12221,7 +12225,7 @@ var KiroRule = class _KiroRule extends ToolRule {
12221
12225
  static getSettablePaths(_options = {}) {
12222
12226
  return {
12223
12227
  nonRoot: {
12224
- relativeDirPath: _options.excludeToolDir ? "steering" : (0, import_node_path99.join)(".kiro", "steering")
12228
+ relativeDirPath: buildToolPath(".kiro", "steering", _options.excludeToolDir)
12225
12229
  }
12226
12230
  };
12227
12231
  }
@@ -12294,7 +12298,7 @@ var OpenCodeRule = class _OpenCodeRule extends ToolRule {
12294
12298
  relativeFilePath: "AGENTS.md"
12295
12299
  },
12296
12300
  nonRoot: {
12297
- relativeDirPath: _options.excludeToolDir ? "memories" : (0, import_node_path100.join)(".opencode", "memories")
12301
+ relativeDirPath: buildToolPath(".opencode", "memories", _options.excludeToolDir)
12298
12302
  }
12299
12303
  };
12300
12304
  }
@@ -12369,7 +12373,7 @@ var QwencodeRule = class _QwencodeRule extends ToolRule {
12369
12373
  relativeFilePath: "QWEN.md"
12370
12374
  },
12371
12375
  nonRoot: {
12372
- relativeDirPath: _options.excludeToolDir ? "memories" : (0, import_node_path101.join)(".qwen", "memories")
12376
+ relativeDirPath: buildToolPath(".qwen", "memories", _options.excludeToolDir)
12373
12377
  }
12374
12378
  };
12375
12379
  }
@@ -12525,7 +12529,7 @@ var RooRule = class _RooRule extends ToolRule {
12525
12529
  static getSettablePaths(_options = {}) {
12526
12530
  return {
12527
12531
  nonRoot: {
12528
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path103.join)(".roo", "rules")
12532
+ relativeDirPath: buildToolPath(".roo", "rules", _options.excludeToolDir)
12529
12533
  }
12530
12534
  };
12531
12535
  }
@@ -12620,7 +12624,7 @@ var WarpRule = class _WarpRule extends ToolRule {
12620
12624
  relativeFilePath: "WARP.md"
12621
12625
  },
12622
12626
  nonRoot: {
12623
- relativeDirPath: _options.excludeToolDir ? "memories" : (0, import_node_path104.join)(".warp", "memories")
12627
+ relativeDirPath: buildToolPath(".warp", "memories", _options.excludeToolDir)
12624
12628
  }
12625
12629
  };
12626
12630
  }
@@ -12691,7 +12695,7 @@ var WindsurfRule = class _WindsurfRule extends ToolRule {
12691
12695
  static getSettablePaths(_options = {}) {
12692
12696
  return {
12693
12697
  nonRoot: {
12694
- relativeDirPath: _options.excludeToolDir ? "rules" : (0, import_node_path105.join)(".windsurf", "rules")
12698
+ relativeDirPath: buildToolPath(".windsurf", "rules", _options.excludeToolDir)
12695
12699
  }
12696
12700
  };
12697
12701
  }
@@ -13543,6 +13547,17 @@ var GitHubClientError = class extends Error {
13543
13547
  this.name = "GitHubClientError";
13544
13548
  }
13545
13549
  };
13550
+ function logGitHubAuthHints(error) {
13551
+ logger.error(`GitHub API Error: ${error.message}`);
13552
+ if (error.statusCode === 401 || error.statusCode === 403) {
13553
+ logger.info(
13554
+ "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories or better rate limits."
13555
+ );
13556
+ logger.info(
13557
+ "Tip: If you use GitHub CLI, you can use `GITHUB_TOKEN=$(gh auth token) rulesync fetch ...`"
13558
+ );
13559
+ }
13560
+ }
13546
13561
  var GitHubClient = class {
13547
13562
  octokit;
13548
13563
  hasToken;
@@ -13857,13 +13872,25 @@ function parseSource(source) {
13857
13872
  }
13858
13873
  return { provider: "github", ...parseShorthand(source) };
13859
13874
  }
13875
+ var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
13876
+ var GITLAB_HOSTS = /* @__PURE__ */ new Set(["gitlab.com", "www.gitlab.com"]);
13877
+ var MAX_RECURSION_DEPTH = 15;
13878
+ var FETCH_CONCURRENCY_LIMIT = 10;
13879
+ async function withSemaphore(semaphore, fn) {
13880
+ await semaphore.acquire();
13881
+ try {
13882
+ return await fn();
13883
+ } finally {
13884
+ semaphore.release();
13885
+ }
13886
+ }
13860
13887
  function parseUrl(url) {
13861
13888
  const urlObj = new URL(url);
13862
13889
  const host = urlObj.hostname.toLowerCase();
13863
13890
  let provider;
13864
- if (host === "github.com" || host.endsWith(".github.com")) {
13891
+ if (GITHUB_HOSTS.has(host)) {
13865
13892
  provider = "github";
13866
- } else if (host === "gitlab.com" || host.endsWith(".gitlab.com")) {
13893
+ } else if (GITLAB_HOSTS.has(host)) {
13867
13894
  provider = "gitlab";
13868
13895
  } else {
13869
13896
  throw new Error(
@@ -13996,13 +14023,15 @@ async function fetchFiles(params) {
13996
14023
  conflictStrategy
13997
14024
  });
13998
14025
  }
14026
+ const semaphore = new import_promise.Semaphore(FETCH_CONCURRENCY_LIMIT);
13999
14027
  const filesToFetch = await collectFeatureFiles({
14000
14028
  client,
14001
14029
  owner: parsed.owner,
14002
14030
  repo: parsed.repo,
14003
14031
  basePath: resolvedPath,
14004
14032
  ref,
14005
- enabledFeatures
14033
+ enabledFeatures,
14034
+ semaphore
14006
14035
  });
14007
14036
  if (filesToFetch.length === 0) {
14008
14037
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14015,9 +14044,8 @@ async function fetchFiles(params) {
14015
14044
  skipped: 0
14016
14045
  };
14017
14046
  }
14018
- const results = [];
14019
14047
  const outputBasePath = (0, import_node_path107.join)(baseDir, outputDir);
14020
- for (const { remotePath, relativePath, size } of filesToFetch) {
14048
+ for (const { relativePath, size } of filesToFetch) {
14021
14049
  checkPathTraversal({
14022
14050
  relativePath,
14023
14051
  intendedRootDir: outputBasePath
@@ -14027,20 +14055,25 @@ async function fetchFiles(params) {
14027
14055
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14028
14056
  );
14029
14057
  }
14030
- const localPath = (0, import_node_path107.join)(outputBasePath, relativePath);
14031
- const exists = await fileExists(localPath);
14032
- let status;
14033
- if (exists && conflictStrategy === "skip") {
14034
- status = "skipped";
14035
- logger.debug(`Skipping existing file: ${relativePath}`);
14036
- } else {
14037
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14058
+ }
14059
+ const results = await Promise.all(
14060
+ filesToFetch.map(async ({ remotePath, relativePath }) => {
14061
+ const localPath = (0, import_node_path107.join)(outputBasePath, relativePath);
14062
+ const exists = await fileExists(localPath);
14063
+ if (exists && conflictStrategy === "skip") {
14064
+ logger.debug(`Skipping existing file: ${relativePath}`);
14065
+ return { relativePath, status: "skipped" };
14066
+ }
14067
+ const content = await withSemaphore(
14068
+ semaphore,
14069
+ () => client.getFileContent(parsed.owner, parsed.repo, remotePath, ref)
14070
+ );
14038
14071
  await writeFileContent(localPath, content);
14039
- status = exists ? "overwritten" : "created";
14072
+ const status = exists ? "overwritten" : "created";
14040
14073
  logger.debug(`Wrote: ${relativePath} (${status})`);
14041
- }
14042
- results.push({ relativePath, status });
14043
- }
14074
+ return { relativePath, status };
14075
+ })
14076
+ );
14044
14077
  const summary = {
14045
14078
  source: `${parsed.owner}/${parsed.repo}`,
14046
14079
  ref,
@@ -14052,24 +14085,29 @@ async function fetchFiles(params) {
14052
14085
  return summary;
14053
14086
  }
14054
14087
  async function collectFeatureFiles(params) {
14055
- const { client, owner, repo, basePath, ref, enabledFeatures } = params;
14056
- const filesToFetch = [];
14057
- for (const feature of enabledFeatures) {
14058
- const featurePaths = FEATURE_PATHS[feature];
14059
- for (const featurePath of featurePaths) {
14088
+ const { client, owner, repo, basePath, ref, enabledFeatures, semaphore } = params;
14089
+ const tasks = enabledFeatures.flatMap(
14090
+ (feature) => FEATURE_PATHS[feature].map((featurePath) => ({ feature, featurePath }))
14091
+ );
14092
+ const results = await Promise.all(
14093
+ tasks.map(async ({ featurePath }) => {
14060
14094
  const fullPath = basePath === "." || basePath === "" ? featurePath : (0, import_node_path107.join)(basePath, featurePath);
14095
+ const collected = [];
14061
14096
  try {
14062
14097
  if (featurePath.includes(".")) {
14063
14098
  try {
14064
- const entries = await client.listDirectory(
14065
- owner,
14066
- repo,
14067
- basePath === "." || basePath === "" ? "." : basePath,
14068
- ref
14099
+ const entries = await withSemaphore(
14100
+ semaphore,
14101
+ () => client.listDirectory(
14102
+ owner,
14103
+ repo,
14104
+ basePath === "." || basePath === "" ? "." : basePath,
14105
+ ref
14106
+ )
14069
14107
  );
14070
14108
  const fileEntry = entries.find((e) => e.name === featurePath && e.type === "file");
14071
14109
  if (fileEntry) {
14072
- filesToFetch.push({
14110
+ collected.push({
14073
14111
  remotePath: fileEntry.path,
14074
14112
  relativePath: featurePath,
14075
14113
  size: fileEntry.size
@@ -14083,10 +14121,17 @@ async function collectFeatureFiles(params) {
14083
14121
  }
14084
14122
  }
14085
14123
  } else {
14086
- const dirFiles = await listDirectoryRecursive(client, owner, repo, fullPath, ref);
14124
+ const dirFiles = await listDirectoryRecursive({
14125
+ client,
14126
+ owner,
14127
+ repo,
14128
+ path: fullPath,
14129
+ ref,
14130
+ semaphore
14131
+ });
14087
14132
  for (const file of dirFiles) {
14088
14133
  const relativePath = basePath === "." || basePath === "" ? file.path : file.path.substring(basePath.length + 1);
14089
- filesToFetch.push({
14134
+ collected.push({
14090
14135
  remotePath: file.path,
14091
14136
  relativePath,
14092
14137
  size: file.size
@@ -14096,26 +14141,49 @@ async function collectFeatureFiles(params) {
14096
14141
  } catch (error) {
14097
14142
  if (isNotFoundError(error)) {
14098
14143
  logger.debug(`Feature not found: ${fullPath}`);
14099
- continue;
14144
+ return collected;
14100
14145
  }
14101
14146
  throw error;
14102
14147
  }
14103
- }
14104
- }
14105
- return filesToFetch;
14148
+ return collected;
14149
+ })
14150
+ );
14151
+ return results.flat();
14106
14152
  }
14107
- async function listDirectoryRecursive(client, owner, repo, path4, ref) {
14108
- const entries = await client.listDirectory(owner, repo, path4, ref);
14153
+ async function listDirectoryRecursive(params) {
14154
+ const { client, owner, repo, path: path4, ref, depth = 0, semaphore } = params;
14155
+ if (depth > MAX_RECURSION_DEPTH) {
14156
+ throw new Error(
14157
+ `Maximum recursion depth (${MAX_RECURSION_DEPTH}) exceeded while listing directory: ${path4}`
14158
+ );
14159
+ }
14160
+ const entries = await withSemaphore(
14161
+ semaphore,
14162
+ () => client.listDirectory(owner, repo, path4, ref)
14163
+ );
14109
14164
  const files = [];
14165
+ const directories = [];
14110
14166
  for (const entry of entries) {
14111
14167
  if (entry.type === "file") {
14112
14168
  files.push(entry);
14113
14169
  } else if (entry.type === "dir") {
14114
- const subFiles = await listDirectoryRecursive(client, owner, repo, entry.path, ref);
14115
- files.push(...subFiles);
14170
+ directories.push(entry);
14116
14171
  }
14117
14172
  }
14118
- return files;
14173
+ const subResults = await Promise.all(
14174
+ directories.map(
14175
+ (dir) => listDirectoryRecursive({
14176
+ client,
14177
+ owner,
14178
+ repo,
14179
+ path: dir.path,
14180
+ ref,
14181
+ depth: depth + 1,
14182
+ semaphore
14183
+ })
14184
+ )
14185
+ );
14186
+ return [...files, ...subResults.flat()];
14119
14187
  }
14120
14188
  async function fetchAndConvertToolFiles(params) {
14121
14189
  const {
@@ -14131,6 +14199,7 @@ async function fetchAndConvertToolFiles(params) {
14131
14199
  } = params;
14132
14200
  const tempDir = await createTempDirectory();
14133
14201
  logger.debug(`Created temp directory: ${tempDir}`);
14202
+ const semaphore = new import_promise.Semaphore(FETCH_CONCURRENCY_LIMIT);
14134
14203
  try {
14135
14204
  const filesToFetch = await collectFeatureFiles({
14136
14205
  client,
@@ -14138,7 +14207,8 @@ async function fetchAndConvertToolFiles(params) {
14138
14207
  repo: parsed.repo,
14139
14208
  basePath: resolvedPath,
14140
14209
  ref,
14141
- enabledFeatures
14210
+ enabledFeatures,
14211
+ semaphore
14142
14212
  });
14143
14213
  if (filesToFetch.length === 0) {
14144
14214
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14151,25 +14221,31 @@ async function fetchAndConvertToolFiles(params) {
14151
14221
  skipped: 0
14152
14222
  };
14153
14223
  }
14154
- const toolPaths = getToolPathMapping(target);
14155
- const fetchedFiles = [];
14156
- for (const { remotePath, relativePath, size } of filesToFetch) {
14224
+ for (const { relativePath, size } of filesToFetch) {
14157
14225
  if (size > MAX_FILE_SIZE) {
14158
14226
  throw new GitHubClientError(
14159
14227
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14160
14228
  );
14161
14229
  }
14162
- const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14163
- checkPathTraversal({
14164
- relativePath: toolRelativePath,
14165
- intendedRootDir: tempDir
14166
- });
14167
- const localPath = (0, import_node_path107.join)(tempDir, toolRelativePath);
14168
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14169
- await writeFileContent(localPath, content);
14170
- fetchedFiles.push(toolRelativePath);
14171
- logger.debug(`Fetched to temp: ${toolRelativePath}`);
14172
14230
  }
14231
+ const toolPaths = getToolPathMapping(target);
14232
+ await Promise.all(
14233
+ filesToFetch.map(async ({ remotePath, relativePath }) => {
14234
+ const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14235
+ checkPathTraversal({
14236
+ relativePath: toolRelativePath,
14237
+ intendedRootDir: tempDir
14238
+ });
14239
+ const localPath = (0, import_node_path107.join)(tempDir, toolRelativePath);
14240
+ const content = await withSemaphore(
14241
+ semaphore,
14242
+ () => client.getFileContent(parsed.owner, parsed.repo, remotePath, ref)
14243
+ );
14244
+ await writeFileContent(localPath, content);
14245
+ logger.debug(`Fetched to temp: ${toolRelativePath}`);
14246
+ return toolRelativePath;
14247
+ })
14248
+ );
14173
14249
  const outputBasePath = (0, import_node_path107.join)(baseDir, outputDir);
14174
14250
  const { converted, convertedPaths } = await convertFetchedFilesToRulesync({
14175
14251
  tempDir,
@@ -14307,12 +14383,7 @@ async function fetchCommand(options) {
14307
14383
  }
14308
14384
  } catch (error) {
14309
14385
  if (error instanceof GitHubClientError) {
14310
- logger.error(`GitHub API Error: ${error.message}`);
14311
- if (error.statusCode === 401 || error.statusCode === 403) {
14312
- logger.info(
14313
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories."
14314
- );
14315
- }
14386
+ logGitHubAuthHints(error);
14316
14387
  } else {
14317
14388
  logger.error(formatError(error));
14318
14389
  }
@@ -17183,7 +17254,8 @@ var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17183
17254
  var ALLOWED_DOWNLOAD_DOMAINS = [
17184
17255
  "github.com",
17185
17256
  "objects.githubusercontent.com",
17186
- "github-releases.githubusercontent.com"
17257
+ "github-releases.githubusercontent.com",
17258
+ "release-assets.githubusercontent.com"
17187
17259
  ];
17188
17260
  var UpdatePermissionError = class extends Error {
17189
17261
  constructor(message) {
@@ -17509,12 +17581,7 @@ async function updateCommand(currentVersion, options) {
17509
17581
  logger.success(message);
17510
17582
  } catch (error) {
17511
17583
  if (error instanceof GitHubClientError) {
17512
- logger.error(`GitHub API Error: ${error.message}`);
17513
- if (error.statusCode === 401 || error.statusCode === 403) {
17514
- logger.info(
17515
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for better rate limits."
17516
- );
17517
- }
17584
+ logGitHubAuthHints(error);
17518
17585
  } else if (error instanceof UpdatePermissionError) {
17519
17586
  logger.error(error.message);
17520
17587
  logger.info("Tip: Run with elevated privileges (e.g., sudo rulesync update)");
@@ -17526,7 +17593,7 @@ async function updateCommand(currentVersion, options) {
17526
17593
  }
17527
17594
 
17528
17595
  // src/cli/index.ts
17529
- var getVersion = () => "6.6.0";
17596
+ var getVersion = () => "6.6.2";
17530
17597
  var main = async () => {
17531
17598
  const program = new import_commander.Command();
17532
17599
  const version = getVersion();
@@ -17626,7 +17693,7 @@ var main = async () => {
17626
17693
  ).option(
17627
17694
  "--modular-mcp",
17628
17695
  "Generate modular-mcp configuration for context compression (experimental)"
17629
- ).option("--dry-run", "Preview changes without writing files").option("--check", "Check if files are up to date (exits with code 1 if changes needed)").action(async (options) => {
17696
+ ).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(async (options) => {
17630
17697
  try {
17631
17698
  await generateCommand({
17632
17699
  targets: options.targets,
package/dist/index.js CHANGED
@@ -102,6 +102,7 @@ var Logger = class {
102
102
  var logger = new Logger();
103
103
 
104
104
  // src/lib/fetch.ts
105
+ import { Semaphore } from "es-toolkit/promise";
105
106
  import { join as join106 } from "path";
106
107
 
107
108
  // src/constants/rulesync-paths.ts
@@ -10386,6 +10387,9 @@ var ToolRule = class extends ToolFile {
10386
10387
  return false;
10387
10388
  }
10388
10389
  };
10390
+ function buildToolPath(toolDir, subDir, excludeToolDir) {
10391
+ return excludeToolDir ? subDir : join83(toolDir, subDir);
10392
+ }
10389
10393
 
10390
10394
  // src/features/rules/agentsmd-rule.ts
10391
10395
  var AgentsMdRule = class _AgentsMdRule extends ToolRule {
@@ -10403,7 +10407,7 @@ var AgentsMdRule = class _AgentsMdRule extends ToolRule {
10403
10407
  relativeFilePath: "AGENTS.md"
10404
10408
  },
10405
10409
  nonRoot: {
10406
- relativeDirPath: _options.excludeToolDir ? "memories" : join84(".agents", "memories")
10410
+ relativeDirPath: buildToolPath(".agents", "memories", _options.excludeToolDir)
10407
10411
  }
10408
10412
  };
10409
10413
  }
@@ -10643,7 +10647,7 @@ var AntigravityRule = class _AntigravityRule extends ToolRule {
10643
10647
  static getSettablePaths(_options = {}) {
10644
10648
  return {
10645
10649
  nonRoot: {
10646
- relativeDirPath: _options.excludeToolDir ? "rules" : join85(".agent", "rules")
10650
+ relativeDirPath: buildToolPath(".agent", "rules", _options.excludeToolDir)
10647
10651
  }
10648
10652
  };
10649
10653
  }
@@ -10819,7 +10823,7 @@ var AugmentcodeLegacyRule = class _AugmentcodeLegacyRule extends ToolRule {
10819
10823
  relativeFilePath: ".augment-guidelines"
10820
10824
  },
10821
10825
  nonRoot: {
10822
- relativeDirPath: _options.excludeToolDir ? "rules" : join86(".augment", "rules")
10826
+ relativeDirPath: buildToolPath(".augment", "rules", _options.excludeToolDir)
10823
10827
  }
10824
10828
  };
10825
10829
  }
@@ -10892,7 +10896,7 @@ var AugmentcodeRule = class _AugmentcodeRule extends ToolRule {
10892
10896
  static getSettablePaths(_options = {}) {
10893
10897
  return {
10894
10898
  nonRoot: {
10895
- relativeDirPath: _options.excludeToolDir ? "rules" : join87(".augment", "rules")
10899
+ relativeDirPath: buildToolPath(".augment", "rules", _options.excludeToolDir)
10896
10900
  }
10897
10901
  };
10898
10902
  }
@@ -10961,7 +10965,7 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
10961
10965
  if (global) {
10962
10966
  return {
10963
10967
  root: {
10964
- relativeDirPath: excludeToolDir ? "." : ".claude",
10968
+ relativeDirPath: buildToolPath(".claude", ".", excludeToolDir),
10965
10969
  relativeFilePath: "CLAUDE.md"
10966
10970
  }
10967
10971
  };
@@ -10972,7 +10976,7 @@ var ClaudecodeLegacyRule = class _ClaudecodeLegacyRule extends ToolRule {
10972
10976
  relativeFilePath: "CLAUDE.md"
10973
10977
  },
10974
10978
  nonRoot: {
10975
- relativeDirPath: excludeToolDir ? "memories" : join88(".claude", "memories")
10979
+ relativeDirPath: buildToolPath(".claude", "memories", excludeToolDir)
10976
10980
  }
10977
10981
  };
10978
10982
  }
@@ -11076,7 +11080,7 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
11076
11080
  if (global) {
11077
11081
  return {
11078
11082
  root: {
11079
- relativeDirPath: excludeToolDir ? "." : ".claude",
11083
+ relativeDirPath: buildToolPath(".claude", ".", excludeToolDir),
11080
11084
  relativeFilePath: "CLAUDE.md"
11081
11085
  }
11082
11086
  };
@@ -11087,7 +11091,7 @@ var ClaudecodeRule = class _ClaudecodeRule extends ToolRule {
11087
11091
  relativeFilePath: "CLAUDE.md"
11088
11092
  },
11089
11093
  nonRoot: {
11090
- relativeDirPath: excludeToolDir ? "rules" : join89(".claude", "rules")
11094
+ relativeDirPath: buildToolPath(".claude", "rules", excludeToolDir)
11091
11095
  }
11092
11096
  };
11093
11097
  }
@@ -11353,7 +11357,7 @@ var CodexcliRule = class _CodexcliRule extends ToolRule {
11353
11357
  if (global) {
11354
11358
  return {
11355
11359
  root: {
11356
- relativeDirPath: excludeToolDir ? "." : ".codex",
11360
+ relativeDirPath: buildToolPath(".codex", ".", excludeToolDir),
11357
11361
  relativeFilePath: "AGENTS.md"
11358
11362
  }
11359
11363
  };
@@ -11364,7 +11368,7 @@ var CodexcliRule = class _CodexcliRule extends ToolRule {
11364
11368
  relativeFilePath: "AGENTS.md"
11365
11369
  },
11366
11370
  nonRoot: {
11367
- relativeDirPath: excludeToolDir ? "memories" : join91(".codex", "memories")
11371
+ relativeDirPath: buildToolPath(".codex", "memories", excludeToolDir)
11368
11372
  }
11369
11373
  };
11370
11374
  }
@@ -11466,11 +11470,11 @@ var CopilotRule = class _CopilotRule extends ToolRule {
11466
11470
  static getSettablePaths(_options = {}) {
11467
11471
  return {
11468
11472
  root: {
11469
- relativeDirPath: _options.excludeToolDir ? "." : ".github",
11473
+ relativeDirPath: buildToolPath(".github", ".", _options.excludeToolDir),
11470
11474
  relativeFilePath: "copilot-instructions.md"
11471
11475
  },
11472
11476
  nonRoot: {
11473
- relativeDirPath: _options.excludeToolDir ? "instructions" : join92(".github", "instructions")
11477
+ relativeDirPath: buildToolPath(".github", "instructions", _options.excludeToolDir)
11474
11478
  }
11475
11479
  };
11476
11480
  }
@@ -11654,7 +11658,7 @@ var CursorRule = class _CursorRule extends ToolRule {
11654
11658
  static getSettablePaths(_options = {}) {
11655
11659
  return {
11656
11660
  nonRoot: {
11657
- relativeDirPath: _options.excludeToolDir ? "rules" : join93(".cursor", "rules")
11661
+ relativeDirPath: buildToolPath(".cursor", "rules", _options.excludeToolDir)
11658
11662
  }
11659
11663
  };
11660
11664
  }
@@ -11856,21 +11860,21 @@ var FactorydroidRule = class _FactorydroidRule extends ToolRule {
11856
11860
  if (options?.global) {
11857
11861
  return {
11858
11862
  root: {
11859
- relativeDirPath: options.excludeToolDir ? "." : ".factorydroid",
11863
+ relativeDirPath: buildToolPath(".factorydroid", ".", options.excludeToolDir),
11860
11864
  relativeFilePath: "AGENTS.md"
11861
11865
  },
11862
11866
  nonRoot: {
11863
- relativeDirPath: options.excludeToolDir ? "memories" : join94(".factorydroid", "memories")
11867
+ relativeDirPath: buildToolPath(".factorydroid", "memories", options.excludeToolDir)
11864
11868
  }
11865
11869
  };
11866
11870
  }
11867
11871
  return {
11868
11872
  root: {
11869
- relativeDirPath: options?.excludeToolDir ? "." : ".factorydroid",
11873
+ relativeDirPath: buildToolPath(".factorydroid", ".", options?.excludeToolDir),
11870
11874
  relativeFilePath: "AGENTS.md"
11871
11875
  },
11872
11876
  nonRoot: {
11873
- relativeDirPath: options?.excludeToolDir ? "memories" : join94(".factorydroid", "memories")
11877
+ relativeDirPath: buildToolPath(".factorydroid", "memories", options?.excludeToolDir)
11874
11878
  }
11875
11879
  };
11876
11880
  }
@@ -11951,7 +11955,7 @@ var GeminiCliRule = class _GeminiCliRule extends ToolRule {
11951
11955
  if (global) {
11952
11956
  return {
11953
11957
  root: {
11954
- relativeDirPath: excludeToolDir ? "." : ".gemini",
11958
+ relativeDirPath: buildToolPath(".gemini", ".", excludeToolDir),
11955
11959
  relativeFilePath: "GEMINI.md"
11956
11960
  }
11957
11961
  };
@@ -11962,7 +11966,7 @@ var GeminiCliRule = class _GeminiCliRule extends ToolRule {
11962
11966
  relativeFilePath: "GEMINI.md"
11963
11967
  },
11964
11968
  nonRoot: {
11965
- relativeDirPath: excludeToolDir ? "memories" : join95(".gemini", "memories")
11969
+ relativeDirPath: buildToolPath(".gemini", "memories", excludeToolDir)
11966
11970
  }
11967
11971
  };
11968
11972
  }
@@ -12056,11 +12060,11 @@ var JunieRule = class _JunieRule extends ToolRule {
12056
12060
  static getSettablePaths(_options = {}) {
12057
12061
  return {
12058
12062
  root: {
12059
- relativeDirPath: _options.excludeToolDir ? "." : ".junie",
12063
+ relativeDirPath: buildToolPath(".junie", ".", _options.excludeToolDir),
12060
12064
  relativeFilePath: "guidelines.md"
12061
12065
  },
12062
12066
  nonRoot: {
12063
- relativeDirPath: _options.excludeToolDir ? "memories" : join96(".junie", "memories")
12067
+ relativeDirPath: buildToolPath(".junie", "memories", _options.excludeToolDir)
12064
12068
  }
12065
12069
  };
12066
12070
  }
@@ -12131,7 +12135,7 @@ var KiloRule = class _KiloRule extends ToolRule {
12131
12135
  static getSettablePaths(_options = {}) {
12132
12136
  return {
12133
12137
  nonRoot: {
12134
- relativeDirPath: _options.excludeToolDir ? "rules" : join97(".kilocode", "rules")
12138
+ relativeDirPath: buildToolPath(".kilocode", "rules", _options.excludeToolDir)
12135
12139
  }
12136
12140
  };
12137
12141
  }
@@ -12198,7 +12202,7 @@ var KiroRule = class _KiroRule extends ToolRule {
12198
12202
  static getSettablePaths(_options = {}) {
12199
12203
  return {
12200
12204
  nonRoot: {
12201
- relativeDirPath: _options.excludeToolDir ? "steering" : join98(".kiro", "steering")
12205
+ relativeDirPath: buildToolPath(".kiro", "steering", _options.excludeToolDir)
12202
12206
  }
12203
12207
  };
12204
12208
  }
@@ -12271,7 +12275,7 @@ var OpenCodeRule = class _OpenCodeRule extends ToolRule {
12271
12275
  relativeFilePath: "AGENTS.md"
12272
12276
  },
12273
12277
  nonRoot: {
12274
- relativeDirPath: _options.excludeToolDir ? "memories" : join99(".opencode", "memories")
12278
+ relativeDirPath: buildToolPath(".opencode", "memories", _options.excludeToolDir)
12275
12279
  }
12276
12280
  };
12277
12281
  }
@@ -12346,7 +12350,7 @@ var QwencodeRule = class _QwencodeRule extends ToolRule {
12346
12350
  relativeFilePath: "QWEN.md"
12347
12351
  },
12348
12352
  nonRoot: {
12349
- relativeDirPath: _options.excludeToolDir ? "memories" : join100(".qwen", "memories")
12353
+ relativeDirPath: buildToolPath(".qwen", "memories", _options.excludeToolDir)
12350
12354
  }
12351
12355
  };
12352
12356
  }
@@ -12502,7 +12506,7 @@ var RooRule = class _RooRule extends ToolRule {
12502
12506
  static getSettablePaths(_options = {}) {
12503
12507
  return {
12504
12508
  nonRoot: {
12505
- relativeDirPath: _options.excludeToolDir ? "rules" : join102(".roo", "rules")
12509
+ relativeDirPath: buildToolPath(".roo", "rules", _options.excludeToolDir)
12506
12510
  }
12507
12511
  };
12508
12512
  }
@@ -12597,7 +12601,7 @@ var WarpRule = class _WarpRule extends ToolRule {
12597
12601
  relativeFilePath: "WARP.md"
12598
12602
  },
12599
12603
  nonRoot: {
12600
- relativeDirPath: _options.excludeToolDir ? "memories" : join103(".warp", "memories")
12604
+ relativeDirPath: buildToolPath(".warp", "memories", _options.excludeToolDir)
12601
12605
  }
12602
12606
  };
12603
12607
  }
@@ -12668,7 +12672,7 @@ var WindsurfRule = class _WindsurfRule extends ToolRule {
12668
12672
  static getSettablePaths(_options = {}) {
12669
12673
  return {
12670
12674
  nonRoot: {
12671
- relativeDirPath: _options.excludeToolDir ? "rules" : join104(".windsurf", "rules")
12675
+ relativeDirPath: buildToolPath(".windsurf", "rules", _options.excludeToolDir)
12672
12676
  }
12673
12677
  };
12674
12678
  }
@@ -13520,6 +13524,17 @@ var GitHubClientError = class extends Error {
13520
13524
  this.name = "GitHubClientError";
13521
13525
  }
13522
13526
  };
13527
+ function logGitHubAuthHints(error) {
13528
+ logger.error(`GitHub API Error: ${error.message}`);
13529
+ if (error.statusCode === 401 || error.statusCode === 403) {
13530
+ logger.info(
13531
+ "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories or better rate limits."
13532
+ );
13533
+ logger.info(
13534
+ "Tip: If you use GitHub CLI, you can use `GITHUB_TOKEN=$(gh auth token) rulesync fetch ...`"
13535
+ );
13536
+ }
13537
+ }
13523
13538
  var GitHubClient = class {
13524
13539
  octokit;
13525
13540
  hasToken;
@@ -13834,13 +13849,25 @@ function parseSource(source) {
13834
13849
  }
13835
13850
  return { provider: "github", ...parseShorthand(source) };
13836
13851
  }
13852
+ var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
13853
+ var GITLAB_HOSTS = /* @__PURE__ */ new Set(["gitlab.com", "www.gitlab.com"]);
13854
+ var MAX_RECURSION_DEPTH = 15;
13855
+ var FETCH_CONCURRENCY_LIMIT = 10;
13856
+ async function withSemaphore(semaphore, fn) {
13857
+ await semaphore.acquire();
13858
+ try {
13859
+ return await fn();
13860
+ } finally {
13861
+ semaphore.release();
13862
+ }
13863
+ }
13837
13864
  function parseUrl(url) {
13838
13865
  const urlObj = new URL(url);
13839
13866
  const host = urlObj.hostname.toLowerCase();
13840
13867
  let provider;
13841
- if (host === "github.com" || host.endsWith(".github.com")) {
13868
+ if (GITHUB_HOSTS.has(host)) {
13842
13869
  provider = "github";
13843
- } else if (host === "gitlab.com" || host.endsWith(".gitlab.com")) {
13870
+ } else if (GITLAB_HOSTS.has(host)) {
13844
13871
  provider = "gitlab";
13845
13872
  } else {
13846
13873
  throw new Error(
@@ -13973,13 +14000,15 @@ async function fetchFiles(params) {
13973
14000
  conflictStrategy
13974
14001
  });
13975
14002
  }
14003
+ const semaphore = new Semaphore(FETCH_CONCURRENCY_LIMIT);
13976
14004
  const filesToFetch = await collectFeatureFiles({
13977
14005
  client,
13978
14006
  owner: parsed.owner,
13979
14007
  repo: parsed.repo,
13980
14008
  basePath: resolvedPath,
13981
14009
  ref,
13982
- enabledFeatures
14010
+ enabledFeatures,
14011
+ semaphore
13983
14012
  });
13984
14013
  if (filesToFetch.length === 0) {
13985
14014
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -13992,9 +14021,8 @@ async function fetchFiles(params) {
13992
14021
  skipped: 0
13993
14022
  };
13994
14023
  }
13995
- const results = [];
13996
14024
  const outputBasePath = join106(baseDir, outputDir);
13997
- for (const { remotePath, relativePath, size } of filesToFetch) {
14025
+ for (const { relativePath, size } of filesToFetch) {
13998
14026
  checkPathTraversal({
13999
14027
  relativePath,
14000
14028
  intendedRootDir: outputBasePath
@@ -14004,20 +14032,25 @@ async function fetchFiles(params) {
14004
14032
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14005
14033
  );
14006
14034
  }
14007
- const localPath = join106(outputBasePath, relativePath);
14008
- const exists = await fileExists(localPath);
14009
- let status;
14010
- if (exists && conflictStrategy === "skip") {
14011
- status = "skipped";
14012
- logger.debug(`Skipping existing file: ${relativePath}`);
14013
- } else {
14014
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14035
+ }
14036
+ const results = await Promise.all(
14037
+ filesToFetch.map(async ({ remotePath, relativePath }) => {
14038
+ const localPath = join106(outputBasePath, relativePath);
14039
+ const exists = await fileExists(localPath);
14040
+ if (exists && conflictStrategy === "skip") {
14041
+ logger.debug(`Skipping existing file: ${relativePath}`);
14042
+ return { relativePath, status: "skipped" };
14043
+ }
14044
+ const content = await withSemaphore(
14045
+ semaphore,
14046
+ () => client.getFileContent(parsed.owner, parsed.repo, remotePath, ref)
14047
+ );
14015
14048
  await writeFileContent(localPath, content);
14016
- status = exists ? "overwritten" : "created";
14049
+ const status = exists ? "overwritten" : "created";
14017
14050
  logger.debug(`Wrote: ${relativePath} (${status})`);
14018
- }
14019
- results.push({ relativePath, status });
14020
- }
14051
+ return { relativePath, status };
14052
+ })
14053
+ );
14021
14054
  const summary = {
14022
14055
  source: `${parsed.owner}/${parsed.repo}`,
14023
14056
  ref,
@@ -14029,24 +14062,29 @@ async function fetchFiles(params) {
14029
14062
  return summary;
14030
14063
  }
14031
14064
  async function collectFeatureFiles(params) {
14032
- const { client, owner, repo, basePath, ref, enabledFeatures } = params;
14033
- const filesToFetch = [];
14034
- for (const feature of enabledFeatures) {
14035
- const featurePaths = FEATURE_PATHS[feature];
14036
- for (const featurePath of featurePaths) {
14065
+ const { client, owner, repo, basePath, ref, enabledFeatures, semaphore } = params;
14066
+ const tasks = enabledFeatures.flatMap(
14067
+ (feature) => FEATURE_PATHS[feature].map((featurePath) => ({ feature, featurePath }))
14068
+ );
14069
+ const results = await Promise.all(
14070
+ tasks.map(async ({ featurePath }) => {
14037
14071
  const fullPath = basePath === "." || basePath === "" ? featurePath : join106(basePath, featurePath);
14072
+ const collected = [];
14038
14073
  try {
14039
14074
  if (featurePath.includes(".")) {
14040
14075
  try {
14041
- const entries = await client.listDirectory(
14042
- owner,
14043
- repo,
14044
- basePath === "." || basePath === "" ? "." : basePath,
14045
- ref
14076
+ const entries = await withSemaphore(
14077
+ semaphore,
14078
+ () => client.listDirectory(
14079
+ owner,
14080
+ repo,
14081
+ basePath === "." || basePath === "" ? "." : basePath,
14082
+ ref
14083
+ )
14046
14084
  );
14047
14085
  const fileEntry = entries.find((e) => e.name === featurePath && e.type === "file");
14048
14086
  if (fileEntry) {
14049
- filesToFetch.push({
14087
+ collected.push({
14050
14088
  remotePath: fileEntry.path,
14051
14089
  relativePath: featurePath,
14052
14090
  size: fileEntry.size
@@ -14060,10 +14098,17 @@ async function collectFeatureFiles(params) {
14060
14098
  }
14061
14099
  }
14062
14100
  } else {
14063
- const dirFiles = await listDirectoryRecursive(client, owner, repo, fullPath, ref);
14101
+ const dirFiles = await listDirectoryRecursive({
14102
+ client,
14103
+ owner,
14104
+ repo,
14105
+ path: fullPath,
14106
+ ref,
14107
+ semaphore
14108
+ });
14064
14109
  for (const file of dirFiles) {
14065
14110
  const relativePath = basePath === "." || basePath === "" ? file.path : file.path.substring(basePath.length + 1);
14066
- filesToFetch.push({
14111
+ collected.push({
14067
14112
  remotePath: file.path,
14068
14113
  relativePath,
14069
14114
  size: file.size
@@ -14073,26 +14118,49 @@ async function collectFeatureFiles(params) {
14073
14118
  } catch (error) {
14074
14119
  if (isNotFoundError(error)) {
14075
14120
  logger.debug(`Feature not found: ${fullPath}`);
14076
- continue;
14121
+ return collected;
14077
14122
  }
14078
14123
  throw error;
14079
14124
  }
14080
- }
14081
- }
14082
- return filesToFetch;
14125
+ return collected;
14126
+ })
14127
+ );
14128
+ return results.flat();
14083
14129
  }
14084
- async function listDirectoryRecursive(client, owner, repo, path4, ref) {
14085
- const entries = await client.listDirectory(owner, repo, path4, ref);
14130
+ async function listDirectoryRecursive(params) {
14131
+ const { client, owner, repo, path: path4, ref, depth = 0, semaphore } = params;
14132
+ if (depth > MAX_RECURSION_DEPTH) {
14133
+ throw new Error(
14134
+ `Maximum recursion depth (${MAX_RECURSION_DEPTH}) exceeded while listing directory: ${path4}`
14135
+ );
14136
+ }
14137
+ const entries = await withSemaphore(
14138
+ semaphore,
14139
+ () => client.listDirectory(owner, repo, path4, ref)
14140
+ );
14086
14141
  const files = [];
14142
+ const directories = [];
14087
14143
  for (const entry of entries) {
14088
14144
  if (entry.type === "file") {
14089
14145
  files.push(entry);
14090
14146
  } else if (entry.type === "dir") {
14091
- const subFiles = await listDirectoryRecursive(client, owner, repo, entry.path, ref);
14092
- files.push(...subFiles);
14147
+ directories.push(entry);
14093
14148
  }
14094
14149
  }
14095
- return files;
14150
+ const subResults = await Promise.all(
14151
+ directories.map(
14152
+ (dir) => listDirectoryRecursive({
14153
+ client,
14154
+ owner,
14155
+ repo,
14156
+ path: dir.path,
14157
+ ref,
14158
+ depth: depth + 1,
14159
+ semaphore
14160
+ })
14161
+ )
14162
+ );
14163
+ return [...files, ...subResults.flat()];
14096
14164
  }
14097
14165
  async function fetchAndConvertToolFiles(params) {
14098
14166
  const {
@@ -14108,6 +14176,7 @@ async function fetchAndConvertToolFiles(params) {
14108
14176
  } = params;
14109
14177
  const tempDir = await createTempDirectory();
14110
14178
  logger.debug(`Created temp directory: ${tempDir}`);
14179
+ const semaphore = new Semaphore(FETCH_CONCURRENCY_LIMIT);
14111
14180
  try {
14112
14181
  const filesToFetch = await collectFeatureFiles({
14113
14182
  client,
@@ -14115,7 +14184,8 @@ async function fetchAndConvertToolFiles(params) {
14115
14184
  repo: parsed.repo,
14116
14185
  basePath: resolvedPath,
14117
14186
  ref,
14118
- enabledFeatures
14187
+ enabledFeatures,
14188
+ semaphore
14119
14189
  });
14120
14190
  if (filesToFetch.length === 0) {
14121
14191
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14128,25 +14198,31 @@ async function fetchAndConvertToolFiles(params) {
14128
14198
  skipped: 0
14129
14199
  };
14130
14200
  }
14131
- const toolPaths = getToolPathMapping(target);
14132
- const fetchedFiles = [];
14133
- for (const { remotePath, relativePath, size } of filesToFetch) {
14201
+ for (const { relativePath, size } of filesToFetch) {
14134
14202
  if (size > MAX_FILE_SIZE) {
14135
14203
  throw new GitHubClientError(
14136
14204
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14137
14205
  );
14138
14206
  }
14139
- const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14140
- checkPathTraversal({
14141
- relativePath: toolRelativePath,
14142
- intendedRootDir: tempDir
14143
- });
14144
- const localPath = join106(tempDir, toolRelativePath);
14145
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14146
- await writeFileContent(localPath, content);
14147
- fetchedFiles.push(toolRelativePath);
14148
- logger.debug(`Fetched to temp: ${toolRelativePath}`);
14149
14207
  }
14208
+ const toolPaths = getToolPathMapping(target);
14209
+ await Promise.all(
14210
+ filesToFetch.map(async ({ remotePath, relativePath }) => {
14211
+ const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14212
+ checkPathTraversal({
14213
+ relativePath: toolRelativePath,
14214
+ intendedRootDir: tempDir
14215
+ });
14216
+ const localPath = join106(tempDir, toolRelativePath);
14217
+ const content = await withSemaphore(
14218
+ semaphore,
14219
+ () => client.getFileContent(parsed.owner, parsed.repo, remotePath, ref)
14220
+ );
14221
+ await writeFileContent(localPath, content);
14222
+ logger.debug(`Fetched to temp: ${toolRelativePath}`);
14223
+ return toolRelativePath;
14224
+ })
14225
+ );
14150
14226
  const outputBasePath = join106(baseDir, outputDir);
14151
14227
  const { converted, convertedPaths } = await convertFetchedFilesToRulesync({
14152
14228
  tempDir,
@@ -14284,12 +14360,7 @@ async function fetchCommand(options) {
14284
14360
  }
14285
14361
  } catch (error) {
14286
14362
  if (error instanceof GitHubClientError) {
14287
- logger.error(`GitHub API Error: ${error.message}`);
14288
- if (error.statusCode === 401 || error.statusCode === 403) {
14289
- logger.info(
14290
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories."
14291
- );
14292
- }
14363
+ logGitHubAuthHints(error);
14293
14364
  } else {
14294
14365
  logger.error(formatError(error));
14295
14366
  }
@@ -17160,7 +17231,8 @@ var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17160
17231
  var ALLOWED_DOWNLOAD_DOMAINS = [
17161
17232
  "github.com",
17162
17233
  "objects.githubusercontent.com",
17163
- "github-releases.githubusercontent.com"
17234
+ "github-releases.githubusercontent.com",
17235
+ "release-assets.githubusercontent.com"
17164
17236
  ];
17165
17237
  var UpdatePermissionError = class extends Error {
17166
17238
  constructor(message) {
@@ -17486,12 +17558,7 @@ async function updateCommand(currentVersion, options) {
17486
17558
  logger.success(message);
17487
17559
  } catch (error) {
17488
17560
  if (error instanceof GitHubClientError) {
17489
- logger.error(`GitHub API Error: ${error.message}`);
17490
- if (error.statusCode === 401 || error.statusCode === 403) {
17491
- logger.info(
17492
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for better rate limits."
17493
- );
17494
- }
17561
+ logGitHubAuthHints(error);
17495
17562
  } else if (error instanceof UpdatePermissionError) {
17496
17563
  logger.error(error.message);
17497
17564
  logger.info("Tip: Run with elevated privileges (e.g., sudo rulesync update)");
@@ -17503,7 +17570,7 @@ async function updateCommand(currentVersion, options) {
17503
17570
  }
17504
17571
 
17505
17572
  // src/cli/index.ts
17506
- var getVersion = () => "6.6.0";
17573
+ var getVersion = () => "6.6.2";
17507
17574
  var main = async () => {
17508
17575
  const program = new Command();
17509
17576
  const version = getVersion();
@@ -17603,7 +17670,7 @@ var main = async () => {
17603
17670
  ).option(
17604
17671
  "--modular-mcp",
17605
17672
  "Generate modular-mcp configuration for context compression (experimental)"
17606
- ).option("--dry-run", "Preview changes without writing files").option("--check", "Check if files are up to date (exits with code 1 if changes needed)").action(async (options) => {
17673
+ ).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(async (options) => {
17607
17674
  try {
17608
17675
  await generateCommand({
17609
17676
  targets: options.targets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "6.6.0",
3
+ "version": "6.6.2",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",
@@ -72,6 +72,7 @@
72
72
  "eslint-plugin-zod-import": "0.3.0",
73
73
  "knip": "5.82.1",
74
74
  "lint-staged": "16.2.7",
75
+ "marked": "17.0.1",
75
76
  "oxfmt": "0.27.0",
76
77
  "oxlint": "1.42.0",
77
78
  "repomix": "1.11.1",