rulesync 3.28.0 → 3.28.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
@@ -203,7 +203,7 @@ npx rulesync generate --targets claudecode --features rules,subagents
203
203
  npx rulesync generate --targets "*" --features rules
204
204
 
205
205
  # Generate simulated commands and subagents
206
- npx rulesync generate --targets copilot,cursor,codexcli --features commands,subagents --simulated-commands --simulated-subagents
206
+ npx rulesync generate --targets copilot,cursor,codexcli --features commands,subagents --simulate-commands --simulate-subagents
207
207
 
208
208
  # Add generated files to .gitignore
209
209
  npx rulesync gitignore
@@ -213,11 +213,28 @@ npx rulesync gitignore
213
213
 
214
214
  You can configure Rulesync by creating a `rulesync.jsonc` file in the root of your project.
215
215
 
216
+ ### JSON Schema Support
217
+
218
+ Rulesync provides a JSON Schema for editor validation and autocompletion. Add the `$schema` property to your `rulesync.jsonc`:
219
+
220
+ ```jsonc
221
+ // rulesync.jsonc
222
+ {
223
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
224
+ "targets": ["claudecode"],
225
+ "features": ["rules"]
226
+ }
227
+ ```
228
+
229
+ ### Configuration Options
230
+
216
231
  Example:
217
232
 
218
233
  ```jsonc
219
234
  // rulesync.jsonc
220
235
  {
236
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
237
+
221
238
  // List of tools to generate configurations for. You can specify "*" to generate all tools.
222
239
  "targets": ["cursor", "claudecode", "geminicli", "opencode", "codexcli"],
223
240
 
@@ -466,9 +483,9 @@ Simulated commands, subagents and skills allow you to generate simulated feature
466
483
  npx rulesync generate \
467
484
  --targets copilot,cursor,codexcli \
468
485
  --features commands,subagents,skills \
469
- --simulated-commands \
470
- --simulated-subagents \
471
- --simulated-skills
486
+ --simulate-commands \
487
+ --simulate-subagents \
488
+ --simulate-skills
472
489
  ```
473
490
  3. Use simulated commands, subagents and skills in your prompts.
474
491
  - Prompt examples:
package/dist/index.cjs CHANGED
@@ -290,6 +290,10 @@ var ConfigParamsSchema = import_mini3.z.object({
290
290
  experimentalSimulateSubagents: (0, import_mini3.optional)(import_mini3.z.boolean())
291
291
  });
292
292
  var PartialConfigParamsSchema = import_mini3.z.partial(ConfigParamsSchema);
293
+ var ConfigFileSchema = import_mini3.z.object({
294
+ $schema: (0, import_mini3.optional)(import_mini3.z.string()),
295
+ ...import_mini3.z.partial(ConfigParamsSchema).shape
296
+ });
293
297
  var RequiredConfigParamsSchema = import_mini3.z.required(ConfigParamsSchema);
294
298
  var Config = class {
295
299
  baseDirs;
@@ -419,7 +423,9 @@ var ConfigResolver = class {
419
423
  try {
420
424
  const fileContent = await readFileContent(validatedConfigPath);
421
425
  const jsonData = (0, import_jsonc_parser.parse)(fileContent);
422
- configByFile = PartialConfigParamsSchema.parse(jsonData);
426
+ const parsed = ConfigFileSchema.parse(jsonData);
427
+ const { $schema: _schema, ...configParams2 } = parsed;
428
+ configByFile = configParams2;
423
429
  } catch (error) {
424
430
  logger.error(`Failed to load config file: ${formatError(error)}`);
425
431
  throw error;
@@ -665,6 +671,13 @@ var AiFile = class {
665
671
  setFileContent(newFileContent) {
666
672
  this.fileContent = newFileContent;
667
673
  }
674
+ /**
675
+ * Returns whether this file can be deleted by rulesync.
676
+ * Override in subclasses that should not be deleted (e.g., user-managed config files).
677
+ */
678
+ isDeletable() {
679
+ return true;
680
+ }
668
681
  };
669
682
 
670
683
  // src/features/commands/tool-command.ts
@@ -899,7 +912,7 @@ var RulesyncFile = class extends AiFile {
899
912
  };
900
913
 
901
914
  // src/features/commands/rulesync-command.ts
902
- var RulesyncCommandFrontmatterSchema = import_mini5.z.object({
915
+ var RulesyncCommandFrontmatterSchema = import_mini5.z.looseObject({
903
916
  targets: RulesyncTargetsSchema,
904
917
  description: import_mini5.z.string()
905
918
  });
@@ -1100,7 +1113,7 @@ var AntigravityCommand = class _AntigravityCommand extends ToolCommand {
1100
1113
  // src/features/commands/claudecode-command.ts
1101
1114
  var import_node_path9 = require("path");
1102
1115
  var import_mini7 = require("zod/mini");
1103
- var ClaudecodeCommandFrontmatterSchema = import_mini7.z.object({
1116
+ var ClaudecodeCommandFrontmatterSchema = import_mini7.z.looseObject({
1104
1117
  description: import_mini7.z.string()
1105
1118
  });
1106
1119
  var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
@@ -1134,9 +1147,12 @@ var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
1134
1147
  return this.frontmatter;
1135
1148
  }
1136
1149
  toRulesyncCommand() {
1150
+ const { description, ...restFields } = this.frontmatter;
1137
1151
  const rulesyncFrontmatter = {
1138
1152
  targets: ["*"],
1139
- description: this.frontmatter.description
1153
+ description,
1154
+ // Preserve extra fields in claudecode section
1155
+ ...Object.keys(restFields).length > 0 && { claudecode: restFields }
1140
1156
  };
1141
1157
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1142
1158
  return new RulesyncCommand({
@@ -1157,8 +1173,10 @@ var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
1157
1173
  global = false
1158
1174
  }) {
1159
1175
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1176
+ const claudecodeFields = rulesyncFrontmatter.claudecode ?? {};
1160
1177
  const claudecodeFrontmatter = {
1161
- description: rulesyncFrontmatter.description
1178
+ description: rulesyncFrontmatter.description,
1179
+ ...claudecodeFields
1162
1180
  };
1163
1181
  const body = rulesyncCommand.getBody();
1164
1182
  const paths = this.getSettablePaths({ global });
@@ -1295,7 +1313,7 @@ var CodexcliCommand = class _CodexcliCommand extends ToolCommand {
1295
1313
  // src/features/commands/copilot-command.ts
1296
1314
  var import_node_path11 = require("path");
1297
1315
  var import_mini8 = require("zod/mini");
1298
- var CopilotCommandFrontmatterSchema = import_mini8.z.object({
1316
+ var CopilotCommandFrontmatterSchema = import_mini8.z.looseObject({
1299
1317
  mode: import_mini8.z.literal("agent"),
1300
1318
  description: import_mini8.z.string()
1301
1319
  });
@@ -1330,9 +1348,12 @@ var CopilotCommand = class _CopilotCommand extends ToolCommand {
1330
1348
  return this.frontmatter;
1331
1349
  }
1332
1350
  toRulesyncCommand() {
1351
+ const { mode: _mode, description, ...restFields } = this.frontmatter;
1333
1352
  const rulesyncFrontmatter = {
1334
1353
  targets: ["*"],
1335
- description: this.frontmatter.description
1354
+ description,
1355
+ // Preserve extra fields in copilot section (excluding mode which is fixed)
1356
+ ...Object.keys(restFields).length > 0 && { copilot: restFields }
1336
1357
  };
1337
1358
  const originalFilePath = this.relativeFilePath;
1338
1359
  const relativeFilePath = originalFilePath.replace(/\.prompt\.md$/, ".md");
@@ -1369,9 +1390,11 @@ var CopilotCommand = class _CopilotCommand extends ToolCommand {
1369
1390
  }) {
1370
1391
  const paths = this.getSettablePaths();
1371
1392
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1393
+ const copilotFields = rulesyncFrontmatter.copilot ?? {};
1372
1394
  const copilotFrontmatter = {
1373
1395
  mode: "agent",
1374
- description: rulesyncFrontmatter.description
1396
+ description: rulesyncFrontmatter.description,
1397
+ ...copilotFields
1375
1398
  };
1376
1399
  const body = rulesyncCommand.getBody();
1377
1400
  const originalFilePath = rulesyncCommand.getRelativeFilePath();
@@ -1490,7 +1513,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1490
1513
  var import_node_path13 = require("path");
1491
1514
  var import_smol_toml = require("smol-toml");
1492
1515
  var import_mini9 = require("zod/mini");
1493
- var GeminiCliCommandFrontmatterSchema = import_mini9.z.object({
1516
+ var GeminiCliCommandFrontmatterSchema = import_mini9.z.looseObject({
1494
1517
  description: import_mini9.z.optional(import_mini9.z.string()),
1495
1518
  prompt: import_mini9.z.string()
1496
1519
  });
@@ -1518,8 +1541,8 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1518
1541
  );
1519
1542
  }
1520
1543
  return {
1521
- description: result.data.description || "",
1522
- prompt: result.data.prompt
1544
+ ...result.data,
1545
+ description: result.data.description || ""
1523
1546
  };
1524
1547
  } catch (error) {
1525
1548
  throw new Error(`Failed to parse TOML command file: ${error}`, { cause: error });
@@ -1535,9 +1558,12 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1535
1558
  };
1536
1559
  }
1537
1560
  toRulesyncCommand() {
1561
+ const { description, prompt: _prompt, ...restFields } = this.frontmatter;
1538
1562
  const rulesyncFrontmatter = {
1539
1563
  targets: ["geminicli"],
1540
- description: this.frontmatter.description
1564
+ description: description ?? "",
1565
+ // Preserve extra fields in geminicli section (excluding prompt which is the body)
1566
+ ...Object.keys(restFields).length > 0 && { geminicli: restFields }
1541
1567
  };
1542
1568
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1543
1569
  return new RulesyncCommand({
@@ -1558,9 +1584,11 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1558
1584
  global = false
1559
1585
  }) {
1560
1586
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1587
+ const geminicliFields = rulesyncFrontmatter.geminicli ?? {};
1561
1588
  const geminiFrontmatter = {
1562
1589
  description: rulesyncFrontmatter.description,
1563
- prompt: rulesyncCommand.getBody()
1590
+ prompt: rulesyncCommand.getBody(),
1591
+ ...geminicliFields
1564
1592
  };
1565
1593
  const tomlContent = `description = "${geminiFrontmatter.description}"
1566
1594
  prompt = """
@@ -1611,7 +1639,7 @@ ${geminiFrontmatter.prompt}
1611
1639
  // src/features/commands/roo-command.ts
1612
1640
  var import_node_path14 = require("path");
1613
1641
  var import_mini10 = require("zod/mini");
1614
- var RooCommandFrontmatterSchema = import_mini10.z.object({
1642
+ var RooCommandFrontmatterSchema = import_mini10.z.looseObject({
1615
1643
  description: import_mini10.z.string(),
1616
1644
  "argument-hint": (0, import_mini10.optional)(import_mini10.z.string())
1617
1645
  });
@@ -1646,9 +1674,12 @@ var RooCommand = class _RooCommand extends ToolCommand {
1646
1674
  return this.frontmatter;
1647
1675
  }
1648
1676
  toRulesyncCommand() {
1677
+ const { description, ...restFields } = this.frontmatter;
1649
1678
  const rulesyncFrontmatter = {
1650
1679
  targets: ["roo"],
1651
- description: this.frontmatter.description
1680
+ description,
1681
+ // Preserve extra fields in roo section
1682
+ ...Object.keys(restFields).length > 0 && { roo: restFields }
1652
1683
  };
1653
1684
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1654
1685
  return new RulesyncCommand({
@@ -1668,8 +1699,10 @@ var RooCommand = class _RooCommand extends ToolCommand {
1668
1699
  validate = true
1669
1700
  }) {
1670
1701
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1702
+ const rooFields = rulesyncFrontmatter.roo ?? {};
1671
1703
  const rooFrontmatter = {
1672
- description: rulesyncFrontmatter.description
1704
+ description: rulesyncFrontmatter.description,
1705
+ ...rooFields
1673
1706
  };
1674
1707
  const body = rulesyncCommand.getBody();
1675
1708
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
@@ -2304,6 +2337,13 @@ var ClaudecodeIgnore = class _ClaudecodeIgnore extends ToolIgnore {
2304
2337
  relativeFilePath: "settings.local.json"
2305
2338
  };
2306
2339
  }
2340
+ /**
2341
+ * ClaudecodeIgnore uses settings.local.json which is a user-managed config file.
2342
+ * It should not be deleted by rulesync.
2343
+ */
2344
+ isDeletable() {
2345
+ return false;
2346
+ }
2307
2347
  toRulesyncIgnore() {
2308
2348
  const rulesyncPatterns = this.patterns.map((pattern) => {
2309
2349
  if (pattern.startsWith("Read(") && pattern.endsWith(")")) {
@@ -2794,7 +2834,7 @@ var IgnoreProcessor = class extends FeatureProcessor {
2794
2834
  try {
2795
2835
  const toolIgnores = await this.loadToolIgnores();
2796
2836
  if (forDeletion) {
2797
- return toolIgnores.filter((toolFile) => !(toolFile instanceof ClaudecodeIgnore));
2837
+ return toolIgnores.filter((toolFile) => toolFile.isDeletable());
2798
2838
  }
2799
2839
  return toolIgnores;
2800
2840
  } catch (error) {
@@ -3296,6 +3336,14 @@ var ClaudecodeMcp = class _ClaudecodeMcp extends ToolMcp {
3296
3336
  getJson() {
3297
3337
  return this.json;
3298
3338
  }
3339
+ /**
3340
+ * In global mode, ~/.claude/.claude.json should not be deleted
3341
+ * as it may contain other user settings.
3342
+ * In local mode, .mcp.json can be safely deleted.
3343
+ */
3344
+ isDeletable() {
3345
+ return !this.global;
3346
+ }
3299
3347
  static getSettablePaths({ global } = {}) {
3300
3348
  if (global) {
3301
3349
  return {
@@ -3878,6 +3926,12 @@ var OpencodeMcp = class _OpencodeMcp extends ToolMcp {
3878
3926
  getJson() {
3879
3927
  return this.json;
3880
3928
  }
3929
+ /**
3930
+ * opencode.json may contain other settings, so it should not be deleted.
3931
+ */
3932
+ isDeletable() {
3933
+ return false;
3934
+ }
3881
3935
  static getSettablePaths({ global } = {}) {
3882
3936
  if (global) {
3883
3937
  return {
@@ -4162,11 +4216,7 @@ var McpProcessor = class extends FeatureProcessor {
4162
4216
  })();
4163
4217
  logger.info(`Successfully loaded ${toolMcps.length} ${this.toolTarget} MCP files`);
4164
4218
  if (forDeletion) {
4165
- let filteredMcps = toolMcps.filter((toolFile) => !(toolFile instanceof OpencodeMcp));
4166
- if (this.global) {
4167
- filteredMcps = filteredMcps.filter((toolFile) => !(toolFile instanceof ClaudecodeMcp));
4168
- }
4169
- return filteredMcps;
4219
+ return toolMcps.filter((toolFile) => toolFile.isDeletable());
4170
4220
  }
4171
4221
  return toolMcps;
4172
4222
  } catch (error) {
@@ -4547,20 +4597,13 @@ var SimulatedSkill = class extends ToolSkill {
4547
4597
  name: rulesyncFrontmatter.name,
4548
4598
  description: rulesyncFrontmatter.description
4549
4599
  };
4550
- const otherFiles = rulesyncSkill.getOtherFiles();
4551
- if (otherFiles.length > 0) {
4552
- logger.warn(
4553
- `Skill "${rulesyncFrontmatter.name}" has ${otherFiles.length} additional file(s) that will be ignored for simulated skill generation.`
4554
- );
4555
- }
4556
4600
  return {
4557
4601
  baseDir: rulesyncSkill.getBaseDir(),
4558
4602
  relativeDirPath: this.getSettablePaths().relativeDirPath,
4559
4603
  dirName: rulesyncSkill.getDirName(),
4560
4604
  frontmatter: simulatedFrontmatter,
4561
4605
  body: rulesyncSkill.getBody(),
4562
- otherFiles: [],
4563
- // Simulated skills ignore otherFiles
4606
+ otherFiles: rulesyncSkill.getOtherFiles(),
4564
4607
  validate
4565
4608
  };
4566
4609
  }
@@ -4582,14 +4625,19 @@ var SimulatedSkill = class extends ToolSkill {
4582
4625
  if (!result.success) {
4583
4626
  throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
4584
4627
  }
4628
+ const otherFiles = await this.collectOtherFiles(
4629
+ baseDir,
4630
+ actualRelativeDirPath,
4631
+ dirName,
4632
+ SKILL_FILE_NAME
4633
+ );
4585
4634
  return {
4586
4635
  baseDir,
4587
4636
  relativeDirPath: actualRelativeDirPath,
4588
4637
  dirName,
4589
4638
  frontmatter: result.data,
4590
4639
  body: content.trim(),
4591
- otherFiles: [],
4592
- // Simulated skills ignore otherFiles
4640
+ otherFiles,
4593
4641
  validate: true
4594
4642
  };
4595
4643
  }
@@ -5603,16 +5651,10 @@ var import_mini24 = require("zod/mini");
5603
5651
  // src/features/subagents/rulesync-subagent.ts
5604
5652
  var import_node_path58 = require("path");
5605
5653
  var import_mini23 = require("zod/mini");
5606
- var RulesyncSubagentModelSchema = import_mini23.z.enum(["opus", "sonnet", "haiku", "inherit"]);
5607
- var RulesyncSubagentFrontmatterSchema = import_mini23.z.object({
5654
+ var RulesyncSubagentFrontmatterSchema = import_mini23.z.looseObject({
5608
5655
  targets: RulesyncTargetsSchema,
5609
5656
  name: import_mini23.z.string(),
5610
- description: import_mini23.z.string(),
5611
- claudecode: import_mini23.z.optional(
5612
- import_mini23.z.object({
5613
- model: RulesyncSubagentModelSchema
5614
- })
5615
- )
5657
+ description: import_mini23.z.string()
5616
5658
  });
5617
5659
  var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
5618
5660
  frontmatter;
@@ -5683,7 +5725,7 @@ var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
5683
5725
  };
5684
5726
 
5685
5727
  // src/features/subagents/claudecode-subagent.ts
5686
- var ClaudecodeSubagentFrontmatterSchema = import_mini24.z.object({
5728
+ var ClaudecodeSubagentFrontmatterSchema = import_mini24.z.looseObject({
5687
5729
  name: import_mini24.z.string(),
5688
5730
  description: import_mini24.z.string(),
5689
5731
  model: import_mini24.z.optional(import_mini24.z.enum(["opus", "sonnet", "haiku", "inherit"]))
@@ -5718,15 +5760,17 @@ var ClaudecodeSubagent = class _ClaudecodeSubagent extends ToolSubagent {
5718
5760
  return this.body;
5719
5761
  }
5720
5762
  toRulesyncSubagent() {
5763
+ const { name, description, model, ...restFields } = this.frontmatter;
5764
+ const claudecodeSection = {
5765
+ ...model && { model },
5766
+ ...restFields
5767
+ };
5721
5768
  const rulesyncFrontmatter = {
5722
5769
  targets: ["claudecode"],
5723
- name: this.frontmatter.name,
5724
- description: this.frontmatter.description,
5725
- ...this.frontmatter.model && {
5726
- claudecode: {
5727
- model: this.frontmatter.model
5728
- }
5729
- }
5770
+ name,
5771
+ description,
5772
+ // Only include claudecode section if there are fields
5773
+ ...Object.keys(claudecodeSection).length > 0 && { claudecode: claudecodeSection }
5730
5774
  };
5731
5775
  return new RulesyncSubagent({
5732
5776
  baseDir: ".",
@@ -5745,11 +5789,17 @@ var ClaudecodeSubagent = class _ClaudecodeSubagent extends ToolSubagent {
5745
5789
  global = false
5746
5790
  }) {
5747
5791
  const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
5748
- const claudecodeFrontmatter = {
5792
+ const claudecodeSection = rulesyncFrontmatter.claudecode ?? {};
5793
+ const rawClaudecodeFrontmatter = {
5749
5794
  name: rulesyncFrontmatter.name,
5750
5795
  description: rulesyncFrontmatter.description,
5751
- model: rulesyncFrontmatter.claudecode?.model
5796
+ ...claudecodeSection
5752
5797
  };
5798
+ const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
5799
+ if (!result.success) {
5800
+ throw new Error(`Invalid claudecode subagent frontmatter: ${formatError(result.error)}`);
5801
+ }
5802
+ const claudecodeFrontmatter = result.data;
5753
5803
  const body = rulesyncSubagent.getBody();
5754
5804
  const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
5755
5805
  const paths = this.getSettablePaths({ global });
@@ -8638,7 +8688,7 @@ var RulesProcessor = class extends FeatureProcessor {
8638
8688
  `@${rule.getRelativePathFromCwd()} description: "${escapedDescription}" globs: "${globsText}"`
8639
8689
  );
8640
8690
  }
8641
- return lines.join("\n") + "\n";
8691
+ return lines.join("\n") + "\n\n";
8642
8692
  }
8643
8693
  generateAdditionalConventionsSection({
8644
8694
  commands,
@@ -10218,7 +10268,7 @@ async function mcpCommand({ version }) {
10218
10268
  }
10219
10269
 
10220
10270
  // src/cli/index.ts
10221
- var getVersion = () => "3.28.0";
10271
+ var getVersion = () => "3.28.2";
10222
10272
  var main = async () => {
10223
10273
  const program = new import_commander.Command();
10224
10274
  const version = getVersion();
package/dist/index.js CHANGED
@@ -267,6 +267,10 @@ var ConfigParamsSchema = z3.object({
267
267
  experimentalSimulateSubagents: optional(z3.boolean())
268
268
  });
269
269
  var PartialConfigParamsSchema = z3.partial(ConfigParamsSchema);
270
+ var ConfigFileSchema = z3.object({
271
+ $schema: optional(z3.string()),
272
+ ...z3.partial(ConfigParamsSchema).shape
273
+ });
270
274
  var RequiredConfigParamsSchema = z3.required(ConfigParamsSchema);
271
275
  var Config = class {
272
276
  baseDirs;
@@ -396,7 +400,9 @@ var ConfigResolver = class {
396
400
  try {
397
401
  const fileContent = await readFileContent(validatedConfigPath);
398
402
  const jsonData = parseJsonc(fileContent);
399
- configByFile = PartialConfigParamsSchema.parse(jsonData);
403
+ const parsed = ConfigFileSchema.parse(jsonData);
404
+ const { $schema: _schema, ...configParams2 } = parsed;
405
+ configByFile = configParams2;
400
406
  } catch (error) {
401
407
  logger.error(`Failed to load config file: ${formatError(error)}`);
402
408
  throw error;
@@ -642,6 +648,13 @@ var AiFile = class {
642
648
  setFileContent(newFileContent) {
643
649
  this.fileContent = newFileContent;
644
650
  }
651
+ /**
652
+ * Returns whether this file can be deleted by rulesync.
653
+ * Override in subclasses that should not be deleted (e.g., user-managed config files).
654
+ */
655
+ isDeletable() {
656
+ return true;
657
+ }
645
658
  };
646
659
 
647
660
  // src/features/commands/tool-command.ts
@@ -876,7 +889,7 @@ var RulesyncFile = class extends AiFile {
876
889
  };
877
890
 
878
891
  // src/features/commands/rulesync-command.ts
879
- var RulesyncCommandFrontmatterSchema = z5.object({
892
+ var RulesyncCommandFrontmatterSchema = z5.looseObject({
880
893
  targets: RulesyncTargetsSchema,
881
894
  description: z5.string()
882
895
  });
@@ -1077,7 +1090,7 @@ var AntigravityCommand = class _AntigravityCommand extends ToolCommand {
1077
1090
  // src/features/commands/claudecode-command.ts
1078
1091
  import { basename as basename6, join as join7 } from "path";
1079
1092
  import { z as z7 } from "zod/mini";
1080
- var ClaudecodeCommandFrontmatterSchema = z7.object({
1093
+ var ClaudecodeCommandFrontmatterSchema = z7.looseObject({
1081
1094
  description: z7.string()
1082
1095
  });
1083
1096
  var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
@@ -1111,9 +1124,12 @@ var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
1111
1124
  return this.frontmatter;
1112
1125
  }
1113
1126
  toRulesyncCommand() {
1127
+ const { description, ...restFields } = this.frontmatter;
1114
1128
  const rulesyncFrontmatter = {
1115
1129
  targets: ["*"],
1116
- description: this.frontmatter.description
1130
+ description,
1131
+ // Preserve extra fields in claudecode section
1132
+ ...Object.keys(restFields).length > 0 && { claudecode: restFields }
1117
1133
  };
1118
1134
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1119
1135
  return new RulesyncCommand({
@@ -1134,8 +1150,10 @@ var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand {
1134
1150
  global = false
1135
1151
  }) {
1136
1152
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1153
+ const claudecodeFields = rulesyncFrontmatter.claudecode ?? {};
1137
1154
  const claudecodeFrontmatter = {
1138
- description: rulesyncFrontmatter.description
1155
+ description: rulesyncFrontmatter.description,
1156
+ ...claudecodeFields
1139
1157
  };
1140
1158
  const body = rulesyncCommand.getBody();
1141
1159
  const paths = this.getSettablePaths({ global });
@@ -1272,7 +1290,7 @@ var CodexcliCommand = class _CodexcliCommand extends ToolCommand {
1272
1290
  // src/features/commands/copilot-command.ts
1273
1291
  import { basename as basename8, join as join9 } from "path";
1274
1292
  import { z as z8 } from "zod/mini";
1275
- var CopilotCommandFrontmatterSchema = z8.object({
1293
+ var CopilotCommandFrontmatterSchema = z8.looseObject({
1276
1294
  mode: z8.literal("agent"),
1277
1295
  description: z8.string()
1278
1296
  });
@@ -1307,9 +1325,12 @@ var CopilotCommand = class _CopilotCommand extends ToolCommand {
1307
1325
  return this.frontmatter;
1308
1326
  }
1309
1327
  toRulesyncCommand() {
1328
+ const { mode: _mode, description, ...restFields } = this.frontmatter;
1310
1329
  const rulesyncFrontmatter = {
1311
1330
  targets: ["*"],
1312
- description: this.frontmatter.description
1331
+ description,
1332
+ // Preserve extra fields in copilot section (excluding mode which is fixed)
1333
+ ...Object.keys(restFields).length > 0 && { copilot: restFields }
1313
1334
  };
1314
1335
  const originalFilePath = this.relativeFilePath;
1315
1336
  const relativeFilePath = originalFilePath.replace(/\.prompt\.md$/, ".md");
@@ -1346,9 +1367,11 @@ var CopilotCommand = class _CopilotCommand extends ToolCommand {
1346
1367
  }) {
1347
1368
  const paths = this.getSettablePaths();
1348
1369
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1370
+ const copilotFields = rulesyncFrontmatter.copilot ?? {};
1349
1371
  const copilotFrontmatter = {
1350
1372
  mode: "agent",
1351
- description: rulesyncFrontmatter.description
1373
+ description: rulesyncFrontmatter.description,
1374
+ ...copilotFields
1352
1375
  };
1353
1376
  const body = rulesyncCommand.getBody();
1354
1377
  const originalFilePath = rulesyncCommand.getRelativeFilePath();
@@ -1467,7 +1490,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1467
1490
  import { basename as basename10, join as join11 } from "path";
1468
1491
  import { parse as parseToml } from "smol-toml";
1469
1492
  import { z as z9 } from "zod/mini";
1470
- var GeminiCliCommandFrontmatterSchema = z9.object({
1493
+ var GeminiCliCommandFrontmatterSchema = z9.looseObject({
1471
1494
  description: z9.optional(z9.string()),
1472
1495
  prompt: z9.string()
1473
1496
  });
@@ -1495,8 +1518,8 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1495
1518
  );
1496
1519
  }
1497
1520
  return {
1498
- description: result.data.description || "",
1499
- prompt: result.data.prompt
1521
+ ...result.data,
1522
+ description: result.data.description || ""
1500
1523
  };
1501
1524
  } catch (error) {
1502
1525
  throw new Error(`Failed to parse TOML command file: ${error}`, { cause: error });
@@ -1512,9 +1535,12 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1512
1535
  };
1513
1536
  }
1514
1537
  toRulesyncCommand() {
1538
+ const { description, prompt: _prompt, ...restFields } = this.frontmatter;
1515
1539
  const rulesyncFrontmatter = {
1516
1540
  targets: ["geminicli"],
1517
- description: this.frontmatter.description
1541
+ description: description ?? "",
1542
+ // Preserve extra fields in geminicli section (excluding prompt which is the body)
1543
+ ...Object.keys(restFields).length > 0 && { geminicli: restFields }
1518
1544
  };
1519
1545
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1520
1546
  return new RulesyncCommand({
@@ -1535,9 +1561,11 @@ var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand {
1535
1561
  global = false
1536
1562
  }) {
1537
1563
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1564
+ const geminicliFields = rulesyncFrontmatter.geminicli ?? {};
1538
1565
  const geminiFrontmatter = {
1539
1566
  description: rulesyncFrontmatter.description,
1540
- prompt: rulesyncCommand.getBody()
1567
+ prompt: rulesyncCommand.getBody(),
1568
+ ...geminicliFields
1541
1569
  };
1542
1570
  const tomlContent = `description = "${geminiFrontmatter.description}"
1543
1571
  prompt = """
@@ -1588,7 +1616,7 @@ ${geminiFrontmatter.prompt}
1588
1616
  // src/features/commands/roo-command.ts
1589
1617
  import { basename as basename11, join as join12 } from "path";
1590
1618
  import { optional as optional2, z as z10 } from "zod/mini";
1591
- var RooCommandFrontmatterSchema = z10.object({
1619
+ var RooCommandFrontmatterSchema = z10.looseObject({
1592
1620
  description: z10.string(),
1593
1621
  "argument-hint": optional2(z10.string())
1594
1622
  });
@@ -1623,9 +1651,12 @@ var RooCommand = class _RooCommand extends ToolCommand {
1623
1651
  return this.frontmatter;
1624
1652
  }
1625
1653
  toRulesyncCommand() {
1654
+ const { description, ...restFields } = this.frontmatter;
1626
1655
  const rulesyncFrontmatter = {
1627
1656
  targets: ["roo"],
1628
- description: this.frontmatter.description
1657
+ description,
1658
+ // Preserve extra fields in roo section
1659
+ ...Object.keys(restFields).length > 0 && { roo: restFields }
1629
1660
  };
1630
1661
  const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
1631
1662
  return new RulesyncCommand({
@@ -1645,8 +1676,10 @@ var RooCommand = class _RooCommand extends ToolCommand {
1645
1676
  validate = true
1646
1677
  }) {
1647
1678
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
1679
+ const rooFields = rulesyncFrontmatter.roo ?? {};
1648
1680
  const rooFrontmatter = {
1649
- description: rulesyncFrontmatter.description
1681
+ description: rulesyncFrontmatter.description,
1682
+ ...rooFields
1650
1683
  };
1651
1684
  const body = rulesyncCommand.getBody();
1652
1685
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
@@ -2281,6 +2314,13 @@ var ClaudecodeIgnore = class _ClaudecodeIgnore extends ToolIgnore {
2281
2314
  relativeFilePath: "settings.local.json"
2282
2315
  };
2283
2316
  }
2317
+ /**
2318
+ * ClaudecodeIgnore uses settings.local.json which is a user-managed config file.
2319
+ * It should not be deleted by rulesync.
2320
+ */
2321
+ isDeletable() {
2322
+ return false;
2323
+ }
2284
2324
  toRulesyncIgnore() {
2285
2325
  const rulesyncPatterns = this.patterns.map((pattern) => {
2286
2326
  if (pattern.startsWith("Read(") && pattern.endsWith(")")) {
@@ -2771,7 +2811,7 @@ var IgnoreProcessor = class extends FeatureProcessor {
2771
2811
  try {
2772
2812
  const toolIgnores = await this.loadToolIgnores();
2773
2813
  if (forDeletion) {
2774
- return toolIgnores.filter((toolFile) => !(toolFile instanceof ClaudecodeIgnore));
2814
+ return toolIgnores.filter((toolFile) => toolFile.isDeletable());
2775
2815
  }
2776
2816
  return toolIgnores;
2777
2817
  } catch (error) {
@@ -3273,6 +3313,14 @@ var ClaudecodeMcp = class _ClaudecodeMcp extends ToolMcp {
3273
3313
  getJson() {
3274
3314
  return this.json;
3275
3315
  }
3316
+ /**
3317
+ * In global mode, ~/.claude/.claude.json should not be deleted
3318
+ * as it may contain other user settings.
3319
+ * In local mode, .mcp.json can be safely deleted.
3320
+ */
3321
+ isDeletable() {
3322
+ return !this.global;
3323
+ }
3276
3324
  static getSettablePaths({ global } = {}) {
3277
3325
  if (global) {
3278
3326
  return {
@@ -3855,6 +3903,12 @@ var OpencodeMcp = class _OpencodeMcp extends ToolMcp {
3855
3903
  getJson() {
3856
3904
  return this.json;
3857
3905
  }
3906
+ /**
3907
+ * opencode.json may contain other settings, so it should not be deleted.
3908
+ */
3909
+ isDeletable() {
3910
+ return false;
3911
+ }
3858
3912
  static getSettablePaths({ global } = {}) {
3859
3913
  if (global) {
3860
3914
  return {
@@ -4139,11 +4193,7 @@ var McpProcessor = class extends FeatureProcessor {
4139
4193
  })();
4140
4194
  logger.info(`Successfully loaded ${toolMcps.length} ${this.toolTarget} MCP files`);
4141
4195
  if (forDeletion) {
4142
- let filteredMcps = toolMcps.filter((toolFile) => !(toolFile instanceof OpencodeMcp));
4143
- if (this.global) {
4144
- filteredMcps = filteredMcps.filter((toolFile) => !(toolFile instanceof ClaudecodeMcp));
4145
- }
4146
- return filteredMcps;
4196
+ return toolMcps.filter((toolFile) => toolFile.isDeletable());
4147
4197
  }
4148
4198
  return toolMcps;
4149
4199
  } catch (error) {
@@ -4524,20 +4574,13 @@ var SimulatedSkill = class extends ToolSkill {
4524
4574
  name: rulesyncFrontmatter.name,
4525
4575
  description: rulesyncFrontmatter.description
4526
4576
  };
4527
- const otherFiles = rulesyncSkill.getOtherFiles();
4528
- if (otherFiles.length > 0) {
4529
- logger.warn(
4530
- `Skill "${rulesyncFrontmatter.name}" has ${otherFiles.length} additional file(s) that will be ignored for simulated skill generation.`
4531
- );
4532
- }
4533
4577
  return {
4534
4578
  baseDir: rulesyncSkill.getBaseDir(),
4535
4579
  relativeDirPath: this.getSettablePaths().relativeDirPath,
4536
4580
  dirName: rulesyncSkill.getDirName(),
4537
4581
  frontmatter: simulatedFrontmatter,
4538
4582
  body: rulesyncSkill.getBody(),
4539
- otherFiles: [],
4540
- // Simulated skills ignore otherFiles
4583
+ otherFiles: rulesyncSkill.getOtherFiles(),
4541
4584
  validate
4542
4585
  };
4543
4586
  }
@@ -4559,14 +4602,19 @@ var SimulatedSkill = class extends ToolSkill {
4559
4602
  if (!result.success) {
4560
4603
  throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
4561
4604
  }
4605
+ const otherFiles = await this.collectOtherFiles(
4606
+ baseDir,
4607
+ actualRelativeDirPath,
4608
+ dirName,
4609
+ SKILL_FILE_NAME
4610
+ );
4562
4611
  return {
4563
4612
  baseDir,
4564
4613
  relativeDirPath: actualRelativeDirPath,
4565
4614
  dirName,
4566
4615
  frontmatter: result.data,
4567
4616
  body: content.trim(),
4568
- otherFiles: [],
4569
- // Simulated skills ignore otherFiles
4617
+ otherFiles,
4570
4618
  validate: true
4571
4619
  };
4572
4620
  }
@@ -5580,16 +5628,10 @@ import { z as z24 } from "zod/mini";
5580
5628
  // src/features/subagents/rulesync-subagent.ts
5581
5629
  import { basename as basename16, join as join56 } from "path";
5582
5630
  import { z as z23 } from "zod/mini";
5583
- var RulesyncSubagentModelSchema = z23.enum(["opus", "sonnet", "haiku", "inherit"]);
5584
- var RulesyncSubagentFrontmatterSchema = z23.object({
5631
+ var RulesyncSubagentFrontmatterSchema = z23.looseObject({
5585
5632
  targets: RulesyncTargetsSchema,
5586
5633
  name: z23.string(),
5587
- description: z23.string(),
5588
- claudecode: z23.optional(
5589
- z23.object({
5590
- model: RulesyncSubagentModelSchema
5591
- })
5592
- )
5634
+ description: z23.string()
5593
5635
  });
5594
5636
  var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
5595
5637
  frontmatter;
@@ -5660,7 +5702,7 @@ var RulesyncSubagent = class _RulesyncSubagent extends RulesyncFile {
5660
5702
  };
5661
5703
 
5662
5704
  // src/features/subagents/claudecode-subagent.ts
5663
- var ClaudecodeSubagentFrontmatterSchema = z24.object({
5705
+ var ClaudecodeSubagentFrontmatterSchema = z24.looseObject({
5664
5706
  name: z24.string(),
5665
5707
  description: z24.string(),
5666
5708
  model: z24.optional(z24.enum(["opus", "sonnet", "haiku", "inherit"]))
@@ -5695,15 +5737,17 @@ var ClaudecodeSubagent = class _ClaudecodeSubagent extends ToolSubagent {
5695
5737
  return this.body;
5696
5738
  }
5697
5739
  toRulesyncSubagent() {
5740
+ const { name, description, model, ...restFields } = this.frontmatter;
5741
+ const claudecodeSection = {
5742
+ ...model && { model },
5743
+ ...restFields
5744
+ };
5698
5745
  const rulesyncFrontmatter = {
5699
5746
  targets: ["claudecode"],
5700
- name: this.frontmatter.name,
5701
- description: this.frontmatter.description,
5702
- ...this.frontmatter.model && {
5703
- claudecode: {
5704
- model: this.frontmatter.model
5705
- }
5706
- }
5747
+ name,
5748
+ description,
5749
+ // Only include claudecode section if there are fields
5750
+ ...Object.keys(claudecodeSection).length > 0 && { claudecode: claudecodeSection }
5707
5751
  };
5708
5752
  return new RulesyncSubagent({
5709
5753
  baseDir: ".",
@@ -5722,11 +5766,17 @@ var ClaudecodeSubagent = class _ClaudecodeSubagent extends ToolSubagent {
5722
5766
  global = false
5723
5767
  }) {
5724
5768
  const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
5725
- const claudecodeFrontmatter = {
5769
+ const claudecodeSection = rulesyncFrontmatter.claudecode ?? {};
5770
+ const rawClaudecodeFrontmatter = {
5726
5771
  name: rulesyncFrontmatter.name,
5727
5772
  description: rulesyncFrontmatter.description,
5728
- model: rulesyncFrontmatter.claudecode?.model
5773
+ ...claudecodeSection
5729
5774
  };
5775
+ const result = ClaudecodeSubagentFrontmatterSchema.safeParse(rawClaudecodeFrontmatter);
5776
+ if (!result.success) {
5777
+ throw new Error(`Invalid claudecode subagent frontmatter: ${formatError(result.error)}`);
5778
+ }
5779
+ const claudecodeFrontmatter = result.data;
5730
5780
  const body = rulesyncSubagent.getBody();
5731
5781
  const fileContent = stringifyFrontmatter(body, claudecodeFrontmatter);
5732
5782
  const paths = this.getSettablePaths({ global });
@@ -8615,7 +8665,7 @@ var RulesProcessor = class extends FeatureProcessor {
8615
8665
  `@${rule.getRelativePathFromCwd()} description: "${escapedDescription}" globs: "${globsText}"`
8616
8666
  );
8617
8667
  }
8618
- return lines.join("\n") + "\n";
8668
+ return lines.join("\n") + "\n\n";
8619
8669
  }
8620
8670
  generateAdditionalConventionsSection({
8621
8671
  commands,
@@ -10195,7 +10245,7 @@ async function mcpCommand({ version }) {
10195
10245
  }
10196
10246
 
10197
10247
  // src/cli/index.ts
10198
- var getVersion = () => "3.28.0";
10248
+ var getVersion = () => "3.28.2";
10199
10249
  var main = async () => {
10200
10250
  const program = new Command();
10201
10251
  const version = getVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "3.28.0",
3
+ "version": "3.28.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",
@@ -49,15 +49,15 @@
49
49
  "zod": "4.1.13"
50
50
  },
51
51
  "devDependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "0.1.53",
53
- "@biomejs/biome": "2.3.7",
52
+ "@anthropic-ai/claude-agent-sdk": "0.1.55",
53
+ "@biomejs/biome": "2.3.8",
54
54
  "@eslint/js": "9.39.1",
55
55
  "@secretlint/secretlint-rule-preset-recommend": "11.2.5",
56
56
  "@tsconfig/node24": "24.0.3",
57
57
  "@types/js-yaml": "4.0.9",
58
58
  "@types/node": "24.10.1",
59
- "@typescript/native-preview": "7.0.0-dev.20251125.1",
60
- "@vitest/coverage-v8": "4.0.13",
59
+ "@typescript/native-preview": "7.0.0-dev.20251130.1",
60
+ "@vitest/coverage-v8": "4.0.14",
61
61
  "cspell": "9.3.2",
62
62
  "eslint": "9.39.1",
63
63
  "eslint-plugin-import": "2.32.0",
@@ -70,12 +70,12 @@
70
70
  "oxlint": "1.30.0",
71
71
  "secretlint": "11.2.5",
72
72
  "simple-git-hooks": "2.13.1",
73
- "sort-package-json": "3.4.0",
73
+ "sort-package-json": "3.5.0",
74
74
  "tsup": "8.5.1",
75
- "tsx": "4.20.6",
75
+ "tsx": "4.21.0",
76
76
  "typescript": "5.9.3",
77
77
  "typescript-eslint": "8.48.0",
78
- "vitest": "4.0.13"
78
+ "vitest": "4.0.14"
79
79
  },
80
80
  "engines": {
81
81
  "node": ">=22.0.0"
@@ -98,6 +98,7 @@
98
98
  "eslint:fix": "eslint . --fix --max-warnings 0 --cache",
99
99
  "fix": "pnpm run bcheck:fix && pnpm run oxlint:fix && pnpm run eslint:fix",
100
100
  "generate": "pnpm run dev generate",
101
+ "generate:schema": "tsx scripts/generate-json-schema.ts",
101
102
  "knip": "knip",
102
103
  "oxlint": "oxlint . --max-warnings 0",
103
104
  "oxlint:fix": "oxlint . --fix --max-warnings 0",