rulesync 6.0.0 → 6.1.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/README.md CHANGED
@@ -272,6 +272,30 @@ Example:
272
272
  }
273
273
  ```
274
274
 
275
+ ### Local Configuration
276
+
277
+ Rulesync supports a local configuration file (`rulesync.local.jsonc`) for machine-specific or developer-specific settings. This file is automatically added to `.gitignore` by `rulesync gitignore` and should not be committed to the repository.
278
+
279
+ **Configuration Priority** (highest to lowest):
280
+
281
+ 1. CLI options
282
+ 2. `rulesync.local.jsonc`
283
+ 3. `rulesync.jsonc`
284
+ 4. Default values
285
+
286
+ Example usage:
287
+
288
+ ```jsonc
289
+ // rulesync.local.jsonc (not committed to git)
290
+ {
291
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
292
+ // Override targets for local development
293
+ "targets": ["claudecode"],
294
+ // Enable verbose output for debugging
295
+ "verbose": true,
296
+ }
297
+ ```
298
+
275
299
  ### Target Order and File Conflicts
276
300
 
277
301
  When multiple targets write to the same output file, **the last target in the array wins**. This is the "last-wins" behavior.
package/dist/index.cjs CHANGED
@@ -118,14 +118,29 @@ var logger = new Logger();
118
118
 
119
119
  // src/config/config-resolver.ts
120
120
  var import_jsonc_parser = require("jsonc-parser");
121
- var import_node_path2 = require("path");
121
+ var import_node_path3 = require("path");
122
+
123
+ // src/constants/rulesync-paths.ts
124
+ var import_node_path = require("path");
125
+ var RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
126
+ var RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
127
+ var RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
128
+ var RULESYNC_RULES_RELATIVE_DIR_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, "rules");
129
+ var RULESYNC_COMMANDS_RELATIVE_DIR_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, "commands");
130
+ var RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, "subagents");
131
+ var RULESYNC_MCP_RELATIVE_FILE_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
132
+ var RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
133
+ var RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
134
+ var RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
135
+ var RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
136
+ var RULESYNC_SKILLS_RELATIVE_DIR_PATH = (0, import_node_path.join)(RULESYNC_RELATIVE_DIR_PATH, "skills");
122
137
 
123
138
  // src/utils/file.ts
124
139
  var import_es_toolkit = require("es-toolkit");
125
140
  var import_globby = require("globby");
126
141
  var import_promises = require("fs/promises");
127
142
  var import_node_os = __toESM(require("os"), 1);
128
- var import_node_path = require("path");
143
+ var import_node_path2 = require("path");
129
144
  async function ensureDir(dirPath) {
130
145
  try {
131
146
  await (0, import_promises.stat)(dirPath);
@@ -137,7 +152,7 @@ async function readOrInitializeFileContent(filePath, initialContent = "") {
137
152
  if (await fileExists(filePath)) {
138
153
  return await readFileContent(filePath);
139
154
  } else {
140
- await ensureDir((0, import_node_path.dirname)(filePath));
155
+ await ensureDir((0, import_node_path2.dirname)(filePath));
141
156
  await writeFileContent(filePath, initialContent);
142
157
  return initialContent;
143
158
  }
@@ -150,16 +165,16 @@ function checkPathTraversal({
150
165
  if (segments.includes("..")) {
151
166
  throw new Error(`Path traversal detected: ${relativePath}`);
152
167
  }
153
- const resolved = (0, import_node_path.resolve)(intendedRootDir, relativePath);
154
- const rel = (0, import_node_path.relative)(intendedRootDir, resolved);
155
- if (rel.startsWith("..") || (0, import_node_path.resolve)(resolved) !== resolved) {
168
+ const resolved = (0, import_node_path2.resolve)(intendedRootDir, relativePath);
169
+ const rel = (0, import_node_path2.relative)(intendedRootDir, resolved);
170
+ if (rel.startsWith("..") || (0, import_node_path2.resolve)(resolved) !== resolved) {
156
171
  throw new Error(`Path traversal detected: ${relativePath}`);
157
172
  }
158
173
  }
159
174
  function resolvePath(relativePath, baseDir) {
160
175
  if (!baseDir) return relativePath;
161
176
  checkPathTraversal({ relativePath, intendedRootDir: baseDir });
162
- return (0, import_node_path.resolve)(baseDir, relativePath);
177
+ return (0, import_node_path2.resolve)(baseDir, relativePath);
163
178
  }
164
179
  async function directoryExists(dirPath) {
165
180
  try {
@@ -185,7 +200,7 @@ function addTrailingNewline(content) {
185
200
  }
186
201
  async function writeFileContent(filepath, content) {
187
202
  logger.debug(`Writing file: ${filepath}`);
188
- await ensureDir((0, import_node_path.dirname)(filepath));
203
+ await ensureDir((0, import_node_path2.dirname)(filepath));
189
204
  await (0, import_promises.writeFile)(filepath, content, "utf-8");
190
205
  }
191
206
  async function fileExists(filepath) {
@@ -413,7 +428,7 @@ var getDefaults = () => ({
413
428
  verbose: false,
414
429
  delete: false,
415
430
  baseDirs: [process.cwd()],
416
- configPath: "rulesync.jsonc",
431
+ configPath: RULESYNC_CONFIG_RELATIVE_FILE_PATH,
417
432
  global: false,
418
433
  silent: false,
419
434
  simulateCommands: false,
@@ -421,6 +436,36 @@ var getDefaults = () => ({
421
436
  simulateSkills: false,
422
437
  modularMcp: false
423
438
  });
439
+ var loadConfigFromFile = async (filePath) => {
440
+ if (!await fileExists(filePath)) {
441
+ return {};
442
+ }
443
+ try {
444
+ const fileContent = await readFileContent(filePath);
445
+ const jsonData = (0, import_jsonc_parser.parse)(fileContent);
446
+ const parsed = ConfigFileSchema.parse(jsonData);
447
+ const { $schema: _schema, ...configParams } = parsed;
448
+ return configParams;
449
+ } catch (error) {
450
+ logger.error(`Failed to load config file "${filePath}": ${formatError(error)}`);
451
+ throw error;
452
+ }
453
+ };
454
+ var mergeConfigs = (baseConfig, localConfig) => {
455
+ return {
456
+ targets: localConfig.targets ?? baseConfig.targets,
457
+ features: localConfig.features ?? baseConfig.features,
458
+ verbose: localConfig.verbose ?? baseConfig.verbose,
459
+ delete: localConfig.delete ?? baseConfig.delete,
460
+ baseDirs: localConfig.baseDirs ?? baseConfig.baseDirs,
461
+ global: localConfig.global ?? baseConfig.global,
462
+ silent: localConfig.silent ?? baseConfig.silent,
463
+ simulateCommands: localConfig.simulateCommands ?? baseConfig.simulateCommands,
464
+ simulateSubagents: localConfig.simulateSubagents ?? baseConfig.simulateSubagents,
465
+ simulateSkills: localConfig.simulateSkills ?? baseConfig.simulateSkills,
466
+ modularMcp: localConfig.modularMcp ?? baseConfig.modularMcp
467
+ };
468
+ };
424
469
  var ConfigResolver = class {
425
470
  static async resolve({
426
471
  targets,
@@ -437,19 +482,11 @@ var ConfigResolver = class {
437
482
  modularMcp
438
483
  }) {
439
484
  const validatedConfigPath = resolvePath(configPath, process.cwd());
440
- let configByFile = {};
441
- if (await fileExists(validatedConfigPath)) {
442
- try {
443
- const fileContent = await readFileContent(validatedConfigPath);
444
- const jsonData = (0, import_jsonc_parser.parse)(fileContent);
445
- const parsed = ConfigFileSchema.parse(jsonData);
446
- const { $schema: _schema, ...configParams2 } = parsed;
447
- configByFile = configParams2;
448
- } catch (error) {
449
- logger.error(`Failed to load config file: ${formatError(error)}`);
450
- throw error;
451
- }
452
- }
485
+ const baseConfig = await loadConfigFromFile(validatedConfigPath);
486
+ const configDir = (0, import_node_path3.dirname)(validatedConfigPath);
487
+ const localConfigPath = (0, import_node_path3.join)(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
488
+ const localConfig = await loadConfigFromFile(localConfigPath);
489
+ const configByFile = mergeConfigs(baseConfig, localConfig);
453
490
  const resolvedGlobal = global ?? configByFile.global ?? getDefaults().global;
454
491
  const resolvedSimulateCommands = simulateCommands ?? configByFile.simulateCommands ?? getDefaults().simulateCommands;
455
492
  const resolvedSimulateSubagents = simulateSubagents ?? configByFile.simulateSubagents ?? getDefaults().simulateSubagents;
@@ -480,7 +517,7 @@ function getBaseDirsInLightOfGlobal({
480
517
  if (global) {
481
518
  return [getHomeDirectory()];
482
519
  }
483
- const resolvedBaseDirs = baseDirs.map((baseDir) => (0, import_node_path2.resolve)(baseDir));
520
+ const resolvedBaseDirs = baseDirs.map((baseDir) => (0, import_node_path3.resolve)(baseDir));
484
521
  resolvedBaseDirs.forEach((baseDir) => {
485
522
  validateBaseDir(baseDir);
486
523
  });
@@ -491,20 +528,6 @@ function getBaseDirsInLightOfGlobal({
491
528
  var import_es_toolkit4 = require("es-toolkit");
492
529
  var import_node_path98 = require("path");
493
530
 
494
- // src/constants/rulesync-paths.ts
495
- var import_node_path3 = require("path");
496
- var RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
497
- var RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
498
- var RULESYNC_RULES_RELATIVE_DIR_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, "rules");
499
- var RULESYNC_COMMANDS_RELATIVE_DIR_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, "commands");
500
- var RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, "subagents");
501
- var RULESYNC_MCP_RELATIVE_FILE_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
502
- var RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
503
- var RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
504
- var RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
505
- var RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
506
- var RULESYNC_SKILLS_RELATIVE_DIR_PATH = (0, import_node_path3.join)(RULESYNC_RELATIVE_DIR_PATH, "skills");
507
-
508
531
  // src/features/commands/commands-processor.ts
509
532
  var import_node_path19 = require("path");
510
533
  var import_mini12 = require("zod/mini");
@@ -4318,6 +4341,46 @@ var CopilotMcp = class _CopilotMcp extends ToolMcp {
4318
4341
 
4319
4342
  // src/features/mcp/cursor-mcp.ts
4320
4343
  var import_node_path39 = require("path");
4344
+ var CURSOR_ENV_VAR_PATTERN = /\$\{env:([^}]+)\}/g;
4345
+ function isMcpServers(value) {
4346
+ return value !== void 0 && value !== null && typeof value === "object";
4347
+ }
4348
+ function convertEnvFromCursorFormat(mcpServers) {
4349
+ return Object.fromEntries(
4350
+ Object.entries(mcpServers).map(([name, config]) => [
4351
+ name,
4352
+ {
4353
+ ...config,
4354
+ ...config.env && {
4355
+ env: Object.fromEntries(
4356
+ Object.entries(config.env).map(([k, v]) => [
4357
+ k,
4358
+ v.replace(CURSOR_ENV_VAR_PATTERN, "${$1}")
4359
+ ])
4360
+ )
4361
+ }
4362
+ }
4363
+ ])
4364
+ );
4365
+ }
4366
+ function convertEnvToCursorFormat(mcpServers) {
4367
+ return Object.fromEntries(
4368
+ Object.entries(mcpServers).map(([name, config]) => [
4369
+ name,
4370
+ {
4371
+ ...config,
4372
+ ...config.env && {
4373
+ env: Object.fromEntries(
4374
+ Object.entries(config.env).map(([k, v]) => [
4375
+ k,
4376
+ v.replace(/\$\{(?!env:)([^}:]+)\}/g, "${env:$1}")
4377
+ ])
4378
+ )
4379
+ }
4380
+ }
4381
+ ])
4382
+ );
4383
+ }
4321
4384
  var CursorMcp = class _CursorMcp extends ToolMcp {
4322
4385
  json;
4323
4386
  constructor(params) {
@@ -4358,8 +4421,10 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
4358
4421
  validate = true
4359
4422
  }) {
4360
4423
  const json = rulesyncMcp.getJson();
4424
+ const mcpServers = isMcpServers(json.mcpServers) ? json.mcpServers : {};
4425
+ const transformedServers = convertEnvToCursorFormat(mcpServers);
4361
4426
  const cursorConfig = {
4362
- mcpServers: json.mcpServers || {}
4427
+ mcpServers: transformedServers
4363
4428
  };
4364
4429
  const fileContent = JSON.stringify(cursorConfig, null, 2);
4365
4430
  return new _CursorMcp({
@@ -4371,11 +4436,17 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
4371
4436
  });
4372
4437
  }
4373
4438
  toRulesyncMcp() {
4439
+ const mcpServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
4440
+ const transformedServers = convertEnvFromCursorFormat(mcpServers);
4441
+ const transformedJson = {
4442
+ ...this.json,
4443
+ mcpServers: transformedServers
4444
+ };
4374
4445
  return new RulesyncMcp({
4375
4446
  baseDir: this.baseDir,
4376
4447
  relativeDirPath: this.relativeDirPath,
4377
4448
  relativeFilePath: "rulesync.mcp.json",
4378
- fileContent: this.fileContent,
4449
+ fileContent: JSON.stringify(transformedJson),
4379
4450
  validate: true
4380
4451
  });
4381
4452
  }
@@ -9046,11 +9117,10 @@ var RulesyncRule = class _RulesyncRule extends RulesyncFile {
9046
9117
  agentsmd: result.data.agentsmd,
9047
9118
  cursor: result.data.cursor
9048
9119
  };
9049
- const filename = (0, import_node_path75.basename)(filePath);
9050
9120
  return new _RulesyncRule({
9051
9121
  baseDir: process.cwd(),
9052
9122
  relativeDirPath: this.getSettablePaths().recommended.relativeDirPath,
9053
- relativeFilePath: filename,
9123
+ relativeFilePath,
9054
9124
  frontmatter: validatedFrontmatter,
9055
9125
  body: content.trim(),
9056
9126
  validate
@@ -10590,7 +10660,7 @@ var CursorRule = class _CursorRule extends ToolRule {
10590
10660
  return new _CursorRule({
10591
10661
  baseDir,
10592
10662
  relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
10593
- relativeFilePath: (0, import_node_path86.basename)(relativeFilePath),
10663
+ relativeFilePath,
10594
10664
  frontmatter: result.data,
10595
10665
  body: content.trim(),
10596
10666
  validate
@@ -11860,10 +11930,17 @@ var RulesProcessor = class extends FeatureProcessor {
11860
11930
  * Load and parse rulesync rule files from .rulesync/rules/ directory
11861
11931
  */
11862
11932
  async loadRulesyncFiles() {
11863
- const files = await findFilesByGlobs((0, import_node_path97.join)(RULESYNC_RULES_RELATIVE_DIR_PATH, "*.md"));
11933
+ const rulesyncBaseDir = (0, import_node_path97.join)(this.baseDir, RULESYNC_RULES_RELATIVE_DIR_PATH);
11934
+ const files = await findFilesByGlobs((0, import_node_path97.join)(rulesyncBaseDir, "**", "*.md"));
11864
11935
  logger.debug(`Found ${files.length} rulesync files`);
11865
11936
  const rulesyncRules = await Promise.all(
11866
- files.map((file) => RulesyncRule.fromFile({ relativeFilePath: (0, import_node_path97.basename)(file) }))
11937
+ files.map((file) => {
11938
+ const relativeFilePath = (0, import_node_path97.relative)(rulesyncBaseDir, file);
11939
+ checkPathTraversal({ relativePath: relativeFilePath, intendedRootDir: rulesyncBaseDir });
11940
+ return RulesyncRule.fromFile({
11941
+ relativeFilePath
11942
+ });
11943
+ })
11867
11944
  );
11868
11945
  const rootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root);
11869
11946
  if (rootRules.length > 1) {
@@ -11966,27 +12043,35 @@ var RulesProcessor = class extends FeatureProcessor {
11966
12043
  if (!settablePaths.nonRoot) {
11967
12044
  return [];
11968
12045
  }
12046
+ const nonRootBaseDir = (0, import_node_path97.join)(this.baseDir, settablePaths.nonRoot.relativeDirPath);
11969
12047
  const nonRootFilePaths = await findFilesByGlobs(
11970
- (0, import_node_path97.join)(this.baseDir, settablePaths.nonRoot.relativeDirPath, `*.${factory.meta.extension}`)
12048
+ (0, import_node_path97.join)(nonRootBaseDir, "**", `*.${factory.meta.extension}`)
11971
12049
  );
11972
12050
  if (forDeletion) {
11973
- return nonRootFilePaths.map(
11974
- (filePath) => factory.class.forDeletion({
12051
+ return nonRootFilePaths.map((filePath) => {
12052
+ const relativeFilePath = (0, import_node_path97.relative)(nonRootBaseDir, filePath);
12053
+ checkPathTraversal({
12054
+ relativePath: relativeFilePath,
12055
+ intendedRootDir: nonRootBaseDir
12056
+ });
12057
+ return factory.class.forDeletion({
11975
12058
  baseDir: this.baseDir,
11976
12059
  relativeDirPath: settablePaths.nonRoot?.relativeDirPath ?? ".",
11977
- relativeFilePath: (0, import_node_path97.basename)(filePath),
12060
+ relativeFilePath,
11978
12061
  global: this.global
11979
- })
11980
- ).filter((rule) => rule.isDeletable());
12062
+ });
12063
+ }).filter((rule) => rule.isDeletable());
11981
12064
  }
11982
12065
  return await Promise.all(
11983
- nonRootFilePaths.map(
11984
- (filePath) => factory.class.fromFile({
12066
+ nonRootFilePaths.map((filePath) => {
12067
+ const relativeFilePath = (0, import_node_path97.relative)(nonRootBaseDir, filePath);
12068
+ checkPathTraversal({ relativePath: relativeFilePath, intendedRootDir: nonRootBaseDir });
12069
+ return factory.class.fromFile({
11985
12070
  baseDir: this.baseDir,
11986
- relativeFilePath: (0, import_node_path97.basename)(filePath),
12071
+ relativeFilePath,
11987
12072
  global: this.global
11988
- })
11989
- )
12073
+ });
12074
+ })
11990
12075
  );
11991
12076
  })();
11992
12077
  logger.debug(`Found ${nonRootToolRules.length} non-root tool rule files`);
@@ -12497,6 +12582,7 @@ var RULESYNC_IGNORE_ENTRIES = [
12497
12582
  // Others
12498
12583
  "**/modular-mcp.json",
12499
12584
  ".rulesync/rules/*.local.md",
12585
+ "rulesync.local.jsonc",
12500
12586
  "!.rulesync/.aiignore"
12501
12587
  ];
12502
12588
  var isRulesyncHeader = (line) => {
@@ -12778,6 +12864,7 @@ async function createConfigFile() {
12778
12864
  baseDirs: ["."],
12779
12865
  delete: true,
12780
12866
  verbose: false,
12867
+ silent: false,
12781
12868
  global: false,
12782
12869
  simulateCommands: false,
12783
12870
  simulateSubagents: false,
@@ -14193,7 +14280,7 @@ async function mcpCommand({ version }) {
14193
14280
  }
14194
14281
 
14195
14282
  // src/cli/index.ts
14196
- var getVersion = () => "6.0.0";
14283
+ var getVersion = () => "6.1.0";
14197
14284
  var main = async () => {
14198
14285
  const program = new import_commander.Command();
14199
14286
  const version = getVersion();