rulesync 6.6.1 → 6.6.3

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
@@ -13546,6 +13547,17 @@ var GitHubClientError = class extends Error {
13546
13547
  this.name = "GitHubClientError";
13547
13548
  }
13548
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
+ }
13549
13561
  var GitHubClient = class {
13550
13562
  octokit;
13551
13563
  hasToken;
@@ -13863,6 +13875,15 @@ function parseSource(source) {
13863
13875
  var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
13864
13876
  var GITLAB_HOSTS = /* @__PURE__ */ new Set(["gitlab.com", "www.gitlab.com"]);
13865
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
+ }
13866
13887
  function parseUrl(url) {
13867
13888
  const urlObj = new URL(url);
13868
13889
  const host = urlObj.hostname.toLowerCase();
@@ -14002,13 +14023,15 @@ async function fetchFiles(params) {
14002
14023
  conflictStrategy
14003
14024
  });
14004
14025
  }
14026
+ const semaphore = new import_promise.Semaphore(FETCH_CONCURRENCY_LIMIT);
14005
14027
  const filesToFetch = await collectFeatureFiles({
14006
14028
  client,
14007
14029
  owner: parsed.owner,
14008
14030
  repo: parsed.repo,
14009
14031
  basePath: resolvedPath,
14010
14032
  ref,
14011
- enabledFeatures
14033
+ enabledFeatures,
14034
+ semaphore
14012
14035
  });
14013
14036
  if (filesToFetch.length === 0) {
14014
14037
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14021,9 +14044,8 @@ async function fetchFiles(params) {
14021
14044
  skipped: 0
14022
14045
  };
14023
14046
  }
14024
- const results = [];
14025
14047
  const outputBasePath = (0, import_node_path107.join)(baseDir, outputDir);
14026
- for (const { remotePath, relativePath, size } of filesToFetch) {
14048
+ for (const { relativePath, size } of filesToFetch) {
14027
14049
  checkPathTraversal({
14028
14050
  relativePath,
14029
14051
  intendedRootDir: outputBasePath
@@ -14033,20 +14055,25 @@ async function fetchFiles(params) {
14033
14055
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14034
14056
  );
14035
14057
  }
14036
- const localPath = (0, import_node_path107.join)(outputBasePath, relativePath);
14037
- const exists = await fileExists(localPath);
14038
- let status;
14039
- if (exists && conflictStrategy === "skip") {
14040
- status = "skipped";
14041
- logger.debug(`Skipping existing file: ${relativePath}`);
14042
- } else {
14043
- 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
+ );
14044
14071
  await writeFileContent(localPath, content);
14045
- status = exists ? "overwritten" : "created";
14072
+ const status = exists ? "overwritten" : "created";
14046
14073
  logger.debug(`Wrote: ${relativePath} (${status})`);
14047
- }
14048
- results.push({ relativePath, status });
14049
- }
14074
+ return { relativePath, status };
14075
+ })
14076
+ );
14050
14077
  const summary = {
14051
14078
  source: `${parsed.owner}/${parsed.repo}`,
14052
14079
  ref,
@@ -14058,24 +14085,29 @@ async function fetchFiles(params) {
14058
14085
  return summary;
14059
14086
  }
14060
14087
  async function collectFeatureFiles(params) {
14061
- const { client, owner, repo, basePath, ref, enabledFeatures } = params;
14062
- const filesToFetch = [];
14063
- for (const feature of enabledFeatures) {
14064
- const featurePaths = FEATURE_PATHS[feature];
14065
- 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 }) => {
14066
14094
  const fullPath = basePath === "." || basePath === "" ? featurePath : (0, import_node_path107.join)(basePath, featurePath);
14095
+ const collected = [];
14067
14096
  try {
14068
14097
  if (featurePath.includes(".")) {
14069
14098
  try {
14070
- const entries = await client.listDirectory(
14071
- owner,
14072
- repo,
14073
- basePath === "." || basePath === "" ? "." : basePath,
14074
- ref
14099
+ const entries = await withSemaphore(
14100
+ semaphore,
14101
+ () => client.listDirectory(
14102
+ owner,
14103
+ repo,
14104
+ basePath === "." || basePath === "" ? "." : basePath,
14105
+ ref
14106
+ )
14075
14107
  );
14076
14108
  const fileEntry = entries.find((e) => e.name === featurePath && e.type === "file");
14077
14109
  if (fileEntry) {
14078
- filesToFetch.push({
14110
+ collected.push({
14079
14111
  remotePath: fileEntry.path,
14080
14112
  relativePath: featurePath,
14081
14113
  size: fileEntry.size
@@ -14089,10 +14121,17 @@ async function collectFeatureFiles(params) {
14089
14121
  }
14090
14122
  }
14091
14123
  } else {
14092
- 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
+ });
14093
14132
  for (const file of dirFiles) {
14094
14133
  const relativePath = basePath === "." || basePath === "" ? file.path : file.path.substring(basePath.length + 1);
14095
- filesToFetch.push({
14134
+ collected.push({
14096
14135
  remotePath: file.path,
14097
14136
  relativePath,
14098
14137
  size: file.size
@@ -14102,38 +14141,49 @@ async function collectFeatureFiles(params) {
14102
14141
  } catch (error) {
14103
14142
  if (isNotFoundError(error)) {
14104
14143
  logger.debug(`Feature not found: ${fullPath}`);
14105
- continue;
14144
+ return collected;
14106
14145
  }
14107
14146
  throw error;
14108
14147
  }
14109
- }
14110
- }
14111
- return filesToFetch;
14148
+ return collected;
14149
+ })
14150
+ );
14151
+ return results.flat();
14112
14152
  }
14113
- async function listDirectoryRecursive(client, owner, repo, path4, ref, depth = 0) {
14153
+ async function listDirectoryRecursive(params) {
14154
+ const { client, owner, repo, path: path4, ref, depth = 0, semaphore } = params;
14114
14155
  if (depth > MAX_RECURSION_DEPTH) {
14115
14156
  throw new Error(
14116
14157
  `Maximum recursion depth (${MAX_RECURSION_DEPTH}) exceeded while listing directory: ${path4}`
14117
14158
  );
14118
14159
  }
14119
- const entries = await client.listDirectory(owner, repo, path4, ref);
14160
+ const entries = await withSemaphore(
14161
+ semaphore,
14162
+ () => client.listDirectory(owner, repo, path4, ref)
14163
+ );
14120
14164
  const files = [];
14165
+ const directories = [];
14121
14166
  for (const entry of entries) {
14122
14167
  if (entry.type === "file") {
14123
14168
  files.push(entry);
14124
14169
  } else if (entry.type === "dir") {
14125
- const subFiles = await listDirectoryRecursive(
14170
+ directories.push(entry);
14171
+ }
14172
+ }
14173
+ const subResults = await Promise.all(
14174
+ directories.map(
14175
+ (dir) => listDirectoryRecursive({
14126
14176
  client,
14127
14177
  owner,
14128
14178
  repo,
14129
- entry.path,
14179
+ path: dir.path,
14130
14180
  ref,
14131
- depth + 1
14132
- );
14133
- files.push(...subFiles);
14134
- }
14135
- }
14136
- return files;
14181
+ depth: depth + 1,
14182
+ semaphore
14183
+ })
14184
+ )
14185
+ );
14186
+ return [...files, ...subResults.flat()];
14137
14187
  }
14138
14188
  async function fetchAndConvertToolFiles(params) {
14139
14189
  const {
@@ -14149,6 +14199,7 @@ async function fetchAndConvertToolFiles(params) {
14149
14199
  } = params;
14150
14200
  const tempDir = await createTempDirectory();
14151
14201
  logger.debug(`Created temp directory: ${tempDir}`);
14202
+ const semaphore = new import_promise.Semaphore(FETCH_CONCURRENCY_LIMIT);
14152
14203
  try {
14153
14204
  const filesToFetch = await collectFeatureFiles({
14154
14205
  client,
@@ -14156,7 +14207,8 @@ async function fetchAndConvertToolFiles(params) {
14156
14207
  repo: parsed.repo,
14157
14208
  basePath: resolvedPath,
14158
14209
  ref,
14159
- enabledFeatures
14210
+ enabledFeatures,
14211
+ semaphore
14160
14212
  });
14161
14213
  if (filesToFetch.length === 0) {
14162
14214
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14169,25 +14221,31 @@ async function fetchAndConvertToolFiles(params) {
14169
14221
  skipped: 0
14170
14222
  };
14171
14223
  }
14172
- const toolPaths = getToolPathMapping(target);
14173
- const fetchedFiles = [];
14174
- for (const { remotePath, relativePath, size } of filesToFetch) {
14224
+ for (const { relativePath, size } of filesToFetch) {
14175
14225
  if (size > MAX_FILE_SIZE) {
14176
14226
  throw new GitHubClientError(
14177
14227
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14178
14228
  );
14179
14229
  }
14180
- const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14181
- checkPathTraversal({
14182
- relativePath: toolRelativePath,
14183
- intendedRootDir: tempDir
14184
- });
14185
- const localPath = (0, import_node_path107.join)(tempDir, toolRelativePath);
14186
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14187
- await writeFileContent(localPath, content);
14188
- fetchedFiles.push(toolRelativePath);
14189
- logger.debug(`Fetched to temp: ${toolRelativePath}`);
14190
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
+ );
14191
14249
  const outputBasePath = (0, import_node_path107.join)(baseDir, outputDir);
14192
14250
  const { converted, convertedPaths } = await convertFetchedFilesToRulesync({
14193
14251
  tempDir,
@@ -14325,12 +14383,7 @@ async function fetchCommand(options) {
14325
14383
  }
14326
14384
  } catch (error) {
14327
14385
  if (error instanceof GitHubClientError) {
14328
- logger.error(`GitHub API Error: ${error.message}`);
14329
- if (error.statusCode === 401 || error.statusCode === 403) {
14330
- logger.info(
14331
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories."
14332
- );
14333
- }
14386
+ logGitHubAuthHints(error);
14334
14387
  } else {
14335
14388
  logger.error(formatError(error));
14336
14389
  }
@@ -17201,7 +17254,8 @@ var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17201
17254
  var ALLOWED_DOWNLOAD_DOMAINS = [
17202
17255
  "github.com",
17203
17256
  "objects.githubusercontent.com",
17204
- "github-releases.githubusercontent.com"
17257
+ "github-releases.githubusercontent.com",
17258
+ "release-assets.githubusercontent.com"
17205
17259
  ];
17206
17260
  var UpdatePermissionError = class extends Error {
17207
17261
  constructor(message) {
@@ -17527,12 +17581,7 @@ async function updateCommand(currentVersion, options) {
17527
17581
  logger.success(message);
17528
17582
  } catch (error) {
17529
17583
  if (error instanceof GitHubClientError) {
17530
- logger.error(`GitHub API Error: ${error.message}`);
17531
- if (error.statusCode === 401 || error.statusCode === 403) {
17532
- logger.info(
17533
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for better rate limits."
17534
- );
17535
- }
17584
+ logGitHubAuthHints(error);
17536
17585
  } else if (error instanceof UpdatePermissionError) {
17537
17586
  logger.error(error.message);
17538
17587
  logger.info("Tip: Run with elevated privileges (e.g., sudo rulesync update)");
@@ -17544,7 +17593,7 @@ async function updateCommand(currentVersion, options) {
17544
17593
  }
17545
17594
 
17546
17595
  // src/cli/index.ts
17547
- var getVersion = () => "6.6.1";
17596
+ var getVersion = () => "6.6.3";
17548
17597
  var main = async () => {
17549
17598
  const program = new import_commander.Command();
17550
17599
  const version = getVersion();
@@ -17644,7 +17693,7 @@ var main = async () => {
17644
17693
  ).option(
17645
17694
  "--modular-mcp",
17646
17695
  "Generate modular-mcp configuration for context compression (experimental)"
17647
- ).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) => {
17648
17697
  try {
17649
17698
  await generateCommand({
17650
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
@@ -13523,6 +13524,17 @@ var GitHubClientError = class extends Error {
13523
13524
  this.name = "GitHubClientError";
13524
13525
  }
13525
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
+ }
13526
13538
  var GitHubClient = class {
13527
13539
  octokit;
13528
13540
  hasToken;
@@ -13840,6 +13852,15 @@ function parseSource(source) {
13840
13852
  var GITHUB_HOSTS = /* @__PURE__ */ new Set(["github.com", "www.github.com"]);
13841
13853
  var GITLAB_HOSTS = /* @__PURE__ */ new Set(["gitlab.com", "www.gitlab.com"]);
13842
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
+ }
13843
13864
  function parseUrl(url) {
13844
13865
  const urlObj = new URL(url);
13845
13866
  const host = urlObj.hostname.toLowerCase();
@@ -13979,13 +14000,15 @@ async function fetchFiles(params) {
13979
14000
  conflictStrategy
13980
14001
  });
13981
14002
  }
14003
+ const semaphore = new Semaphore(FETCH_CONCURRENCY_LIMIT);
13982
14004
  const filesToFetch = await collectFeatureFiles({
13983
14005
  client,
13984
14006
  owner: parsed.owner,
13985
14007
  repo: parsed.repo,
13986
14008
  basePath: resolvedPath,
13987
14009
  ref,
13988
- enabledFeatures
14010
+ enabledFeatures,
14011
+ semaphore
13989
14012
  });
13990
14013
  if (filesToFetch.length === 0) {
13991
14014
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -13998,9 +14021,8 @@ async function fetchFiles(params) {
13998
14021
  skipped: 0
13999
14022
  };
14000
14023
  }
14001
- const results = [];
14002
14024
  const outputBasePath = join106(baseDir, outputDir);
14003
- for (const { remotePath, relativePath, size } of filesToFetch) {
14025
+ for (const { relativePath, size } of filesToFetch) {
14004
14026
  checkPathTraversal({
14005
14027
  relativePath,
14006
14028
  intendedRootDir: outputBasePath
@@ -14010,20 +14032,25 @@ async function fetchFiles(params) {
14010
14032
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14011
14033
  );
14012
14034
  }
14013
- const localPath = join106(outputBasePath, relativePath);
14014
- const exists = await fileExists(localPath);
14015
- let status;
14016
- if (exists && conflictStrategy === "skip") {
14017
- status = "skipped";
14018
- logger.debug(`Skipping existing file: ${relativePath}`);
14019
- } else {
14020
- 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
+ );
14021
14048
  await writeFileContent(localPath, content);
14022
- status = exists ? "overwritten" : "created";
14049
+ const status = exists ? "overwritten" : "created";
14023
14050
  logger.debug(`Wrote: ${relativePath} (${status})`);
14024
- }
14025
- results.push({ relativePath, status });
14026
- }
14051
+ return { relativePath, status };
14052
+ })
14053
+ );
14027
14054
  const summary = {
14028
14055
  source: `${parsed.owner}/${parsed.repo}`,
14029
14056
  ref,
@@ -14035,24 +14062,29 @@ async function fetchFiles(params) {
14035
14062
  return summary;
14036
14063
  }
14037
14064
  async function collectFeatureFiles(params) {
14038
- const { client, owner, repo, basePath, ref, enabledFeatures } = params;
14039
- const filesToFetch = [];
14040
- for (const feature of enabledFeatures) {
14041
- const featurePaths = FEATURE_PATHS[feature];
14042
- 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 }) => {
14043
14071
  const fullPath = basePath === "." || basePath === "" ? featurePath : join106(basePath, featurePath);
14072
+ const collected = [];
14044
14073
  try {
14045
14074
  if (featurePath.includes(".")) {
14046
14075
  try {
14047
- const entries = await client.listDirectory(
14048
- owner,
14049
- repo,
14050
- basePath === "." || basePath === "" ? "." : basePath,
14051
- ref
14076
+ const entries = await withSemaphore(
14077
+ semaphore,
14078
+ () => client.listDirectory(
14079
+ owner,
14080
+ repo,
14081
+ basePath === "." || basePath === "" ? "." : basePath,
14082
+ ref
14083
+ )
14052
14084
  );
14053
14085
  const fileEntry = entries.find((e) => e.name === featurePath && e.type === "file");
14054
14086
  if (fileEntry) {
14055
- filesToFetch.push({
14087
+ collected.push({
14056
14088
  remotePath: fileEntry.path,
14057
14089
  relativePath: featurePath,
14058
14090
  size: fileEntry.size
@@ -14066,10 +14098,17 @@ async function collectFeatureFiles(params) {
14066
14098
  }
14067
14099
  }
14068
14100
  } else {
14069
- 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
+ });
14070
14109
  for (const file of dirFiles) {
14071
14110
  const relativePath = basePath === "." || basePath === "" ? file.path : file.path.substring(basePath.length + 1);
14072
- filesToFetch.push({
14111
+ collected.push({
14073
14112
  remotePath: file.path,
14074
14113
  relativePath,
14075
14114
  size: file.size
@@ -14079,38 +14118,49 @@ async function collectFeatureFiles(params) {
14079
14118
  } catch (error) {
14080
14119
  if (isNotFoundError(error)) {
14081
14120
  logger.debug(`Feature not found: ${fullPath}`);
14082
- continue;
14121
+ return collected;
14083
14122
  }
14084
14123
  throw error;
14085
14124
  }
14086
- }
14087
- }
14088
- return filesToFetch;
14125
+ return collected;
14126
+ })
14127
+ );
14128
+ return results.flat();
14089
14129
  }
14090
- async function listDirectoryRecursive(client, owner, repo, path4, ref, depth = 0) {
14130
+ async function listDirectoryRecursive(params) {
14131
+ const { client, owner, repo, path: path4, ref, depth = 0, semaphore } = params;
14091
14132
  if (depth > MAX_RECURSION_DEPTH) {
14092
14133
  throw new Error(
14093
14134
  `Maximum recursion depth (${MAX_RECURSION_DEPTH}) exceeded while listing directory: ${path4}`
14094
14135
  );
14095
14136
  }
14096
- const entries = await client.listDirectory(owner, repo, path4, ref);
14137
+ const entries = await withSemaphore(
14138
+ semaphore,
14139
+ () => client.listDirectory(owner, repo, path4, ref)
14140
+ );
14097
14141
  const files = [];
14142
+ const directories = [];
14098
14143
  for (const entry of entries) {
14099
14144
  if (entry.type === "file") {
14100
14145
  files.push(entry);
14101
14146
  } else if (entry.type === "dir") {
14102
- const subFiles = await listDirectoryRecursive(
14147
+ directories.push(entry);
14148
+ }
14149
+ }
14150
+ const subResults = await Promise.all(
14151
+ directories.map(
14152
+ (dir) => listDirectoryRecursive({
14103
14153
  client,
14104
14154
  owner,
14105
14155
  repo,
14106
- entry.path,
14156
+ path: dir.path,
14107
14157
  ref,
14108
- depth + 1
14109
- );
14110
- files.push(...subFiles);
14111
- }
14112
- }
14113
- return files;
14158
+ depth: depth + 1,
14159
+ semaphore
14160
+ })
14161
+ )
14162
+ );
14163
+ return [...files, ...subResults.flat()];
14114
14164
  }
14115
14165
  async function fetchAndConvertToolFiles(params) {
14116
14166
  const {
@@ -14126,6 +14176,7 @@ async function fetchAndConvertToolFiles(params) {
14126
14176
  } = params;
14127
14177
  const tempDir = await createTempDirectory();
14128
14178
  logger.debug(`Created temp directory: ${tempDir}`);
14179
+ const semaphore = new Semaphore(FETCH_CONCURRENCY_LIMIT);
14129
14180
  try {
14130
14181
  const filesToFetch = await collectFeatureFiles({
14131
14182
  client,
@@ -14133,7 +14184,8 @@ async function fetchAndConvertToolFiles(params) {
14133
14184
  repo: parsed.repo,
14134
14185
  basePath: resolvedPath,
14135
14186
  ref,
14136
- enabledFeatures
14187
+ enabledFeatures,
14188
+ semaphore
14137
14189
  });
14138
14190
  if (filesToFetch.length === 0) {
14139
14191
  logger.warn(`No files found matching enabled features: ${enabledFeatures.join(", ")}`);
@@ -14146,25 +14198,31 @@ async function fetchAndConvertToolFiles(params) {
14146
14198
  skipped: 0
14147
14199
  };
14148
14200
  }
14149
- const toolPaths = getToolPathMapping(target);
14150
- const fetchedFiles = [];
14151
- for (const { remotePath, relativePath, size } of filesToFetch) {
14201
+ for (const { relativePath, size } of filesToFetch) {
14152
14202
  if (size > MAX_FILE_SIZE) {
14153
14203
  throw new GitHubClientError(
14154
14204
  `File "${relativePath}" exceeds maximum size limit (${(size / 1024 / 1024).toFixed(2)}MB > ${MAX_FILE_SIZE / 1024 / 1024}MB)`
14155
14205
  );
14156
14206
  }
14157
- const toolRelativePath = mapToToolPath(relativePath, toolPaths);
14158
- checkPathTraversal({
14159
- relativePath: toolRelativePath,
14160
- intendedRootDir: tempDir
14161
- });
14162
- const localPath = join106(tempDir, toolRelativePath);
14163
- const content = await client.getFileContent(parsed.owner, parsed.repo, remotePath, ref);
14164
- await writeFileContent(localPath, content);
14165
- fetchedFiles.push(toolRelativePath);
14166
- logger.debug(`Fetched to temp: ${toolRelativePath}`);
14167
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
+ );
14168
14226
  const outputBasePath = join106(baseDir, outputDir);
14169
14227
  const { converted, convertedPaths } = await convertFetchedFilesToRulesync({
14170
14228
  tempDir,
@@ -14302,12 +14360,7 @@ async function fetchCommand(options) {
14302
14360
  }
14303
14361
  } catch (error) {
14304
14362
  if (error instanceof GitHubClientError) {
14305
- logger.error(`GitHub API Error: ${error.message}`);
14306
- if (error.statusCode === 401 || error.statusCode === 403) {
14307
- logger.info(
14308
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for private repositories."
14309
- );
14310
- }
14363
+ logGitHubAuthHints(error);
14311
14364
  } else {
14312
14365
  logger.error(formatError(error));
14313
14366
  }
@@ -17178,7 +17231,8 @@ var MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
17178
17231
  var ALLOWED_DOWNLOAD_DOMAINS = [
17179
17232
  "github.com",
17180
17233
  "objects.githubusercontent.com",
17181
- "github-releases.githubusercontent.com"
17234
+ "github-releases.githubusercontent.com",
17235
+ "release-assets.githubusercontent.com"
17182
17236
  ];
17183
17237
  var UpdatePermissionError = class extends Error {
17184
17238
  constructor(message) {
@@ -17504,12 +17558,7 @@ async function updateCommand(currentVersion, options) {
17504
17558
  logger.success(message);
17505
17559
  } catch (error) {
17506
17560
  if (error instanceof GitHubClientError) {
17507
- logger.error(`GitHub API Error: ${error.message}`);
17508
- if (error.statusCode === 401 || error.statusCode === 403) {
17509
- logger.info(
17510
- "Tip: Set GITHUB_TOKEN or GH_TOKEN environment variable for better rate limits."
17511
- );
17512
- }
17561
+ logGitHubAuthHints(error);
17513
17562
  } else if (error instanceof UpdatePermissionError) {
17514
17563
  logger.error(error.message);
17515
17564
  logger.info("Tip: Run with elevated privileges (e.g., sudo rulesync update)");
@@ -17521,7 +17570,7 @@ async function updateCommand(currentVersion, options) {
17521
17570
  }
17522
17571
 
17523
17572
  // src/cli/index.ts
17524
- var getVersion = () => "6.6.1";
17573
+ var getVersion = () => "6.6.3";
17525
17574
  var main = async () => {
17526
17575
  const program = new Command();
17527
17576
  const version = getVersion();
@@ -17621,7 +17670,7 @@ var main = async () => {
17621
17670
  ).option(
17622
17671
  "--modular-mcp",
17623
17672
  "Generate modular-mcp configuration for context compression (experimental)"
17624
- ).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) => {
17625
17674
  try {
17626
17675
  await generateCommand({
17627
17676
  targets: options.targets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "6.6.1",
3
+ "version": "6.6.3",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",
@@ -36,16 +36,16 @@
36
36
  "pre-commit": "pnpm exec lint-staged"
37
37
  },
38
38
  "dependencies": {
39
- "@modelcontextprotocol/sdk": "1.25.3",
39
+ "@modelcontextprotocol/sdk": "1.26.0",
40
40
  "@octokit/request-error": "7.1.0",
41
41
  "@octokit/rest": "22.0.1",
42
42
  "@toon-format/toon": "2.1.0",
43
43
  "@valibot/to-json-schema": "1.5.0",
44
- "commander": "14.0.2",
44
+ "commander": "14.0.3",
45
45
  "consola": "3.4.2",
46
- "effect": "3.19.15",
46
+ "effect": "3.19.16",
47
47
  "es-toolkit": "1.44.0",
48
- "fastmcp": "3.30.1",
48
+ "fastmcp": "3.31.0",
49
49
  "globby": "16.1.0",
50
50
  "gray-matter": "4.0.3",
51
51
  "js-yaml": "4.1.1",
@@ -55,26 +55,26 @@
55
55
  "zod": "4.3.6"
56
56
  },
57
57
  "devDependencies": {
58
- "@anthropic-ai/claude-agent-sdk": "0.2.23",
58
+ "@anthropic-ai/claude-agent-sdk": "0.2.31",
59
59
  "@eslint/js": "9.39.2",
60
60
  "@secretlint/secretlint-rule-preset-recommend": "11.3.1",
61
61
  "@tsconfig/node24": "24.0.4",
62
62
  "@types/js-yaml": "4.0.9",
63
- "@types/node": "25.1.0",
64
- "@typescript/native-preview": "7.0.0-dev.20260128.1",
63
+ "@types/node": "25.2.0",
64
+ "@typescript/native-preview": "7.0.0-dev.20260204.1",
65
65
  "@vitest/coverage-v8": "4.0.18",
66
- "cspell": "9.6.2",
66
+ "cspell": "9.6.4",
67
67
  "eslint": "9.39.2",
68
68
  "eslint-plugin-import": "2.32.0",
69
69
  "eslint-plugin-no-type-assertion": "1.3.0",
70
- "eslint-plugin-oxlint": "1.42.0",
70
+ "eslint-plugin-oxlint": "1.43.0",
71
71
  "eslint-plugin-strict-dependencies": "1.3.30",
72
72
  "eslint-plugin-zod-import": "0.3.0",
73
- "knip": "5.82.1",
73
+ "knip": "5.83.0",
74
74
  "lint-staged": "16.2.7",
75
75
  "marked": "17.0.1",
76
- "oxfmt": "0.27.0",
77
- "oxlint": "1.42.0",
76
+ "oxfmt": "0.28.0",
77
+ "oxlint": "1.43.0",
78
78
  "repomix": "1.11.1",
79
79
  "secretlint": "11.3.1",
80
80
  "simple-git-hooks": "2.13.1",