rulesync 8.1.0 → 8.2.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.
@@ -33,12 +33,13 @@ var FeaturesSchema = z.array(FeatureSchema);
33
33
  var FeatureOptionsSchema = z.record(z.string(), z.unknown());
34
34
  var FeatureValueSchema = z.union([z.boolean(), FeatureOptionsSchema]);
35
35
  var PerFeatureConfigSchema = z.record(z.string(), FeatureValueSchema);
36
+ var PerTargetFeaturesValueSchema = z.union([
37
+ z.array(z.enum(ALL_FEATURES_WITH_WILDCARD)),
38
+ PerFeatureConfigSchema
39
+ ]);
36
40
  var RulesyncFeaturesSchema = z.union([
37
41
  z.array(z.enum(ALL_FEATURES_WITH_WILDCARD)),
38
- z.record(
39
- z.string(),
40
- z.union([z.array(z.enum(ALL_FEATURES_WITH_WILDCARD)), PerFeatureConfigSchema])
41
- )
42
+ z.record(z.string(), PerTargetFeaturesValueSchema)
42
43
  ]);
43
44
  var isFeatureValueEnabled = (value) => {
44
45
  if (value === true) return true;
@@ -81,6 +82,14 @@ var ALL_TOOL_TARGETS_WITH_WILDCARD = [...ALL_TOOL_TARGETS, "*"];
81
82
  var ToolTargetSchema = z2.enum(ALL_TOOL_TARGETS);
82
83
  var ToolTargetsSchema = z2.array(ToolTargetSchema);
83
84
  var RulesyncTargetsSchema = z2.array(z2.enum(ALL_TOOL_TARGETS_WITH_WILDCARD));
85
+ var RulesyncConfigTargetsObjectSchema = z2.record(z2.string(), PerTargetFeaturesValueSchema);
86
+ var RulesyncConfigTargetsSchema = z2.union([
87
+ RulesyncTargetsSchema,
88
+ RulesyncConfigTargetsObjectSchema
89
+ ]);
90
+ var isRulesyncConfigTargetsObject = (value) => {
91
+ return !Array.isArray(value);
92
+ };
84
93
 
85
94
  // src/config/config-resolver.ts
86
95
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
@@ -338,7 +347,7 @@ var SourceEntrySchema = z3.object({
338
347
  });
339
348
  var ConfigParamsSchema = z3.object({
340
349
  baseDirs: z3.array(z3.string()),
341
- targets: RulesyncTargetsSchema,
350
+ targets: RulesyncConfigTargetsSchema,
342
351
  features: RulesyncFeaturesSchema,
343
352
  verbose: z3.boolean(),
344
353
  delete: z3.boolean(),
@@ -365,10 +374,42 @@ var CONFLICTING_TARGET_PAIRS = [
365
374
  ["claudecode", "claudecode-legacy"]
366
375
  ];
367
376
  var LEGACY_TARGETS = ["augmentcode-legacy", "claudecode-legacy"];
377
+ var assertTargetsFeaturesExclusive = ({
378
+ targets,
379
+ features
380
+ }) => {
381
+ const targetsIsObject = targets !== void 0 && !Array.isArray(targets);
382
+ const featuresIsObject = features !== void 0 && !Array.isArray(features);
383
+ if (targetsIsObject && features !== void 0) {
384
+ throw new Error(
385
+ "Invalid config: when 'targets' is in object form, 'features' must be omitted. Declare per-target features inside the 'targets' object instead."
386
+ );
387
+ }
388
+ if (featuresIsObject && targets !== void 0) {
389
+ throw new Error(
390
+ "Invalid config: when 'features' is in object form, 'targets' must be omitted. Migrate to the 'targets' object form, e.g. `targets: { claudecode: [...] }`."
391
+ );
392
+ }
393
+ };
394
+ var assertTargetsOrFeaturesProvided = ({
395
+ targets,
396
+ features
397
+ }) => {
398
+ if (targets === void 0 && features === void 0) {
399
+ throw new Error("Invalid config: at least one of 'targets' or 'features' must be provided.");
400
+ }
401
+ };
368
402
  var Config = class _Config {
369
403
  baseDirs;
370
404
  targets;
371
405
  features;
406
+ /**
407
+ * Cached list of validated `ToolTarget` keys for the object form of
408
+ * `targets`. Populated in the constructor after `validateObjectFormTargetKeys`
409
+ * so `getTargets()` does not rebuild the `ALL_TOOL_TARGETS` set on every call.
410
+ * Undefined when `this.targets` is in array form.
411
+ */
412
+ objectFormTargetKeys;
372
413
  verbose;
373
414
  delete;
374
415
  global;
@@ -396,13 +437,19 @@ var Config = class _Config {
396
437
  check,
397
438
  sources
398
439
  }) {
399
- this.validateConflictingTargets(targets);
440
+ assertTargetsFeaturesExclusive({ targets, features });
441
+ assertTargetsOrFeaturesProvided({ targets, features });
442
+ const resolvedTargets = targets ?? [];
443
+ const resolvedFeatures = features ?? [];
444
+ this.validateObjectFormTargetKeys(resolvedTargets);
445
+ this.validateConflictingTargets(resolvedTargets);
400
446
  if (dryRun && check) {
401
447
  throw new Error("--dry-run and --check cannot be used together");
402
448
  }
403
449
  this.baseDirs = baseDirs;
404
- this.targets = targets;
405
- this.features = features;
450
+ this.targets = resolvedTargets;
451
+ this.features = resolvedFeatures;
452
+ this.objectFormTargetKeys = isRulesyncConfigTargetsObject(resolvedTargets) ? _Config.filterValidToolTargets(Object.keys(resolvedTargets)) : void 0;
406
453
  this.verbose = verbose;
407
454
  this.delete = isDelete;
408
455
  this.global = global ?? false;
@@ -415,11 +462,37 @@ var Config = class _Config {
415
462
  this.check = check ?? false;
416
463
  this.sources = sources ?? [];
417
464
  }
465
+ /**
466
+ * Rejects unknown keys (and the special `*` key) in the object form of
467
+ * `targets`. For the array form this is already enforced at the Zod schema
468
+ * level via `z.enum(ALL_TOOL_TARGETS_WITH_WILDCARD)`; for the object form
469
+ * `z.record(z.string(), ...)` intentionally accepts any string key (to work
470
+ * around zod's `z.record(z.enum(...))` requiring ALL enum members), so
471
+ * runtime validation lives here instead.
472
+ */
473
+ validateObjectFormTargetKeys(targets) {
474
+ if (Array.isArray(targets)) return;
475
+ const validTargets = new Set(ALL_TOOL_TARGETS);
476
+ for (const key of Object.keys(targets)) {
477
+ if (key === "*") {
478
+ throw new Error(
479
+ "Invalid target '*' in object form: wildcard is only supported in the array form `targets: ['*']`. Per-target options cannot be attached to a wildcard."
480
+ );
481
+ }
482
+ if (!validTargets.has(key)) {
483
+ throw new Error(`Unknown target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
484
+ }
485
+ }
486
+ }
418
487
  validateConflictingTargets(targets) {
488
+ const has = (target) => {
489
+ if (Array.isArray(targets)) {
490
+ return targets.includes(target);
491
+ }
492
+ return Object.prototype.hasOwnProperty.call(targets, target);
493
+ };
419
494
  for (const [target1, target2] of CONFLICTING_TARGET_PAIRS) {
420
- const hasTarget1 = targets.includes(target1);
421
- const hasTarget2 = targets.includes(target2);
422
- if (hasTarget1 && hasTarget2) {
495
+ if (has(target1) && has(target2)) {
423
496
  throw new Error(
424
497
  `Conflicting targets: '${target1}' and '${target2}' cannot be used together. Please choose one.`
425
498
  );
@@ -429,16 +502,45 @@ var Config = class _Config {
429
502
  getBaseDirs() {
430
503
  return this.baseDirs;
431
504
  }
505
+ /**
506
+ * Filter an arbitrary string-key list down to the known `ToolTarget` set,
507
+ * skipping `*` (which is only meaningful as an array element, not a key).
508
+ */
509
+ static filterValidToolTargets(keys) {
510
+ const validTargets = new Set(ALL_TOOL_TARGETS);
511
+ const result = [];
512
+ for (const key of keys) {
513
+ if (key === "*") continue;
514
+ if (!validTargets.has(key)) continue;
515
+ result.push(key);
516
+ }
517
+ return result;
518
+ }
432
519
  getTargets() {
433
- if (this.targets.includes("*")) {
520
+ if (this.objectFormTargetKeys !== void 0) {
521
+ return this.objectFormTargetKeys;
522
+ }
523
+ const arrayTargets = Array.isArray(this.targets) ? this.targets : [];
524
+ if (!Array.isArray(this.features)) {
525
+ return _Config.filterValidToolTargets(Object.keys(this.features));
526
+ }
527
+ if (arrayTargets.includes("*")) {
434
528
  return ALL_TOOL_TARGETS.filter(
435
529
  // eslint-disable-next-line no-type-assertion/no-type-assertion
436
530
  (target) => !LEGACY_TARGETS.includes(target)
437
531
  );
438
532
  }
439
- return this.targets.filter((target) => target !== "*");
533
+ return arrayTargets.filter((target) => target !== "*");
440
534
  }
441
535
  getFeatures(target) {
536
+ if (isRulesyncConfigTargetsObject(this.targets)) {
537
+ if (target) {
538
+ const value = this.targets[target];
539
+ if (!value) return [];
540
+ return _Config.normalizeTargetFeatures(value);
541
+ }
542
+ return _Config.collectAllFeatures(Object.values(this.targets));
543
+ }
442
544
  if (!Array.isArray(this.features)) {
443
545
  const perTargetFeatures = this.features;
444
546
  if (target) {
@@ -448,20 +550,7 @@ var Config = class _Config {
448
550
  }
449
551
  return _Config.normalizeTargetFeatures(targetFeatures);
450
552
  }
451
- const allFeatures = [];
452
- for (const features of Object.values(perTargetFeatures)) {
453
- if (!features) continue;
454
- const normalized = _Config.normalizeTargetFeatures(features);
455
- for (const feature of normalized) {
456
- if (!allFeatures.includes(feature)) {
457
- allFeatures.push(feature);
458
- }
459
- }
460
- if (allFeatures.length === ALL_FEATURES.length) {
461
- return allFeatures;
462
- }
463
- }
464
- return allFeatures;
553
+ return _Config.collectAllFeatures(Object.values(perTargetFeatures));
465
554
  }
466
555
  if (this.features.includes("*")) {
467
556
  return [...ALL_FEATURES];
@@ -489,23 +578,40 @@ var Config = class _Config {
489
578
  }
490
579
  return enabled;
491
580
  }
581
+ /**
582
+ * Collect the union of features across all per-target values.
583
+ * Used when `getFeatures()` is called without a target in object mode.
584
+ */
585
+ static collectAllFeatures(values) {
586
+ const allFeatures = [];
587
+ for (const value of values) {
588
+ if (!value) continue;
589
+ const normalized = _Config.normalizeTargetFeatures(value);
590
+ for (const feature of normalized) {
591
+ if (!allFeatures.includes(feature)) {
592
+ allFeatures.push(feature);
593
+ }
594
+ }
595
+ if (allFeatures.length === ALL_FEATURES.length) {
596
+ return allFeatures;
597
+ }
598
+ }
599
+ return allFeatures;
600
+ }
492
601
  /**
493
602
  * Returns the per-feature options object for a given target/feature, if any.
494
603
  * Returns `undefined` when no per-feature options were provided or when the
495
604
  * feature is not enabled for the given target.
496
605
  */
497
606
  getFeatureOptions(target, feature) {
498
- if (Array.isArray(this.features)) {
607
+ const value = isRulesyncConfigTargetsObject(this.targets) ? this.targets[target] : !Array.isArray(this.features) ? this.features[target] : void 0;
608
+ if (!value || Array.isArray(value)) {
499
609
  return void 0;
500
610
  }
501
- const targetFeatures = this.features[target];
502
- if (!targetFeatures || Array.isArray(targetFeatures)) {
503
- return void 0;
504
- }
505
- const perFeature = targetFeatures;
506
- const value = perFeature[feature];
507
- if (value && typeof value === "object" && isFeatureValueEnabled(value)) {
508
- return value;
611
+ const perFeature = value;
612
+ const featureValue = perFeature[feature];
613
+ if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) {
614
+ return featureValue;
509
615
  }
510
616
  return void 0;
511
617
  }
@@ -513,6 +619,13 @@ var Config = class _Config {
513
619
  * Check if per-target features configuration is being used.
514
620
  */
515
621
  hasPerTargetFeatures() {
622
+ return isRulesyncConfigTargetsObject(this.targets) || !Array.isArray(this.features);
623
+ }
624
+ /**
625
+ * Returns true if the deprecated object form under `features` is in use.
626
+ * Callers can use this to emit a migration warning.
627
+ */
628
+ hasDeprecatedFeaturesObjectForm() {
516
629
  return !Array.isArray(this.features);
517
630
  }
518
631
  getVerbose() {
@@ -557,6 +670,17 @@ var Config = class _Config {
557
670
  }
558
671
  };
559
672
 
673
+ // src/config/deprecation-warnings.ts
674
+ var deprecationWarningEmitted = false;
675
+ var emitFeaturesObjectFormDeprecationWarning = () => {
676
+ if (deprecationWarningEmitted) return;
677
+ if (process.env.RULESYNC_SILENT_DEPRECATION) return;
678
+ deprecationWarningEmitted = true;
679
+ console.warn(
680
+ "[rulesync] DEPRECATED: 'features' object form is deprecated. Use the new 'targets' object form instead: `targets: { claudecode: { rules: true, ignore: { fileMode: 'local' } } }`. See https://github.com/dyoshikawa/rulesync/blob/main/docs/guide/configuration.md for the migration guide."
681
+ );
682
+ };
683
+
560
684
  // src/config/config-resolver.ts
561
685
  var getDefaults = () => ({
562
686
  targets: ["agentsmd"],
@@ -583,6 +707,10 @@ var loadConfigFromFile = async (filePath) => {
583
707
  const jsonData = parseJsonc(fileContent);
584
708
  const parsed = ConfigFileSchema.parse(jsonData);
585
709
  const { $schema: _schema, ...configParams } = parsed;
710
+ assertTargetsFeaturesExclusive({
711
+ targets: configParams.targets,
712
+ features: configParams.features
713
+ });
586
714
  return configParams;
587
715
  };
588
716
  var mergeConfigs = (baseConfig, localConfig) => {
@@ -626,14 +754,35 @@ var ConfigResolver = class {
626
754
  const localConfigPath = join3(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
627
755
  const localConfig = await loadConfigFromFile(localConfigPath);
628
756
  const configByFile = mergeConfigs(baseConfig, localConfig);
757
+ try {
758
+ assertTargetsFeaturesExclusive({
759
+ targets: configByFile.targets,
760
+ features: configByFile.features
761
+ });
762
+ } catch (error) {
763
+ const detail = error instanceof Error ? error.message : String(error);
764
+ throw new Error(
765
+ `${detail} (detected after merging '${validatedConfigPath}' with '${localConfigPath}' \u2014 the two files combined produce the invalid combination; remove the conflicting field from one of them).`,
766
+ { cause: error }
767
+ );
768
+ }
629
769
  const resolvedGlobal = global ?? configByFile.global ?? getDefaults().global;
630
770
  const resolvedSimulateCommands = simulateCommands ?? configByFile.simulateCommands ?? getDefaults().simulateCommands;
631
771
  const resolvedSimulateSubagents = simulateSubagents ?? configByFile.simulateSubagents ?? getDefaults().simulateSubagents;
632
772
  const resolvedSimulateSkills = simulateSkills ?? configByFile.simulateSkills ?? getDefaults().simulateSkills;
633
773
  const resolvedGitignoreTargetsOnly = gitignoreTargetsOnly ?? configByFile.gitignoreTargetsOnly ?? getDefaults().gitignoreTargetsOnly;
774
+ const userProvidedFeatures = features ?? configByFile.features;
775
+ const userProvidedTargets = targets ?? configByFile.targets;
776
+ const targetsIsObject = userProvidedTargets !== void 0 && !Array.isArray(userProvidedTargets);
777
+ const featuresIsObject = userProvidedFeatures !== void 0 && !Array.isArray(userProvidedFeatures);
778
+ if (featuresIsObject) {
779
+ emitFeaturesObjectFormDeprecationWarning();
780
+ }
781
+ const resolvedFeatures = userProvidedFeatures ?? (targetsIsObject ? void 0 : getDefaults().features);
782
+ const resolvedTargets = userProvidedTargets ?? (featuresIsObject ? void 0 : getDefaults().targets);
634
783
  const configParams = {
635
- targets: targets ?? configByFile.targets ?? getDefaults().targets,
636
- features: features ?? configByFile.features ?? getDefaults().features,
784
+ targets: resolvedTargets,
785
+ features: resolvedFeatures,
637
786
  verbose: verbose ?? configByFile.verbose ?? getDefaults().verbose,
638
787
  delete: isDelete ?? configByFile.delete ?? getDefaults().delete,
639
788
  baseDirs: getBaseDirsInLightOfGlobal({
@@ -44,12 +44,13 @@ var FeaturesSchema = import_mini.z.array(FeatureSchema);
44
44
  var FeatureOptionsSchema = import_mini.z.record(import_mini.z.string(), import_mini.z.unknown());
45
45
  var FeatureValueSchema = import_mini.z.union([import_mini.z.boolean(), FeatureOptionsSchema]);
46
46
  var PerFeatureConfigSchema = import_mini.z.record(import_mini.z.string(), FeatureValueSchema);
47
+ var PerTargetFeaturesValueSchema = import_mini.z.union([
48
+ import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)),
49
+ PerFeatureConfigSchema
50
+ ]);
47
51
  var RulesyncFeaturesSchema = import_mini.z.union([
48
52
  import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)),
49
- import_mini.z.record(
50
- import_mini.z.string(),
51
- import_mini.z.union([import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)), PerFeatureConfigSchema])
52
- )
53
+ import_mini.z.record(import_mini.z.string(), PerTargetFeaturesValueSchema)
53
54
  ]);
54
55
  var isFeatureValueEnabled = (value) => {
55
56
  if (value === true) return true;
@@ -951,6 +952,14 @@ var ALL_TOOL_TARGETS_WITH_WILDCARD = [...ALL_TOOL_TARGETS, "*"];
951
952
  var ToolTargetSchema = import_mini3.z.enum(ALL_TOOL_TARGETS);
952
953
  var ToolTargetsSchema = import_mini3.z.array(ToolTargetSchema);
953
954
  var RulesyncTargetsSchema = import_mini3.z.array(import_mini3.z.enum(ALL_TOOL_TARGETS_WITH_WILDCARD));
955
+ var RulesyncConfigTargetsObjectSchema = import_mini3.z.record(import_mini3.z.string(), PerTargetFeaturesValueSchema);
956
+ var RulesyncConfigTargetsSchema = import_mini3.z.union([
957
+ RulesyncTargetsSchema,
958
+ RulesyncConfigTargetsObjectSchema
959
+ ]);
960
+ var isRulesyncConfigTargetsObject = (value) => {
961
+ return !Array.isArray(value);
962
+ };
954
963
 
955
964
  // src/features/commands/rulesync-command.ts
956
965
  var RulesyncCommandFrontmatterSchema = import_mini4.z.looseObject({
@@ -19994,7 +20003,7 @@ var SourceEntrySchema = import_mini69.z.object({
19994
20003
  });
19995
20004
  var ConfigParamsSchema = import_mini69.z.object({
19996
20005
  baseDirs: import_mini69.z.array(import_mini69.z.string()),
19997
- targets: RulesyncTargetsSchema,
20006
+ targets: RulesyncConfigTargetsSchema,
19998
20007
  features: RulesyncFeaturesSchema,
19999
20008
  verbose: import_mini69.z.boolean(),
20000
20009
  delete: import_mini69.z.boolean(),
@@ -20021,10 +20030,42 @@ var CONFLICTING_TARGET_PAIRS = [
20021
20030
  ["claudecode", "claudecode-legacy"]
20022
20031
  ];
20023
20032
  var LEGACY_TARGETS = ["augmentcode-legacy", "claudecode-legacy"];
20033
+ var assertTargetsFeaturesExclusive = ({
20034
+ targets,
20035
+ features
20036
+ }) => {
20037
+ const targetsIsObject = targets !== void 0 && !Array.isArray(targets);
20038
+ const featuresIsObject = features !== void 0 && !Array.isArray(features);
20039
+ if (targetsIsObject && features !== void 0) {
20040
+ throw new Error(
20041
+ "Invalid config: when 'targets' is in object form, 'features' must be omitted. Declare per-target features inside the 'targets' object instead."
20042
+ );
20043
+ }
20044
+ if (featuresIsObject && targets !== void 0) {
20045
+ throw new Error(
20046
+ "Invalid config: when 'features' is in object form, 'targets' must be omitted. Migrate to the 'targets' object form, e.g. `targets: { claudecode: [...] }`."
20047
+ );
20048
+ }
20049
+ };
20050
+ var assertTargetsOrFeaturesProvided = ({
20051
+ targets,
20052
+ features
20053
+ }) => {
20054
+ if (targets === void 0 && features === void 0) {
20055
+ throw new Error("Invalid config: at least one of 'targets' or 'features' must be provided.");
20056
+ }
20057
+ };
20024
20058
  var Config = class _Config {
20025
20059
  baseDirs;
20026
20060
  targets;
20027
20061
  features;
20062
+ /**
20063
+ * Cached list of validated `ToolTarget` keys for the object form of
20064
+ * `targets`. Populated in the constructor after `validateObjectFormTargetKeys`
20065
+ * so `getTargets()` does not rebuild the `ALL_TOOL_TARGETS` set on every call.
20066
+ * Undefined when `this.targets` is in array form.
20067
+ */
20068
+ objectFormTargetKeys;
20028
20069
  verbose;
20029
20070
  delete;
20030
20071
  global;
@@ -20052,13 +20093,19 @@ var Config = class _Config {
20052
20093
  check,
20053
20094
  sources
20054
20095
  }) {
20055
- this.validateConflictingTargets(targets);
20096
+ assertTargetsFeaturesExclusive({ targets, features });
20097
+ assertTargetsOrFeaturesProvided({ targets, features });
20098
+ const resolvedTargets = targets ?? [];
20099
+ const resolvedFeatures = features ?? [];
20100
+ this.validateObjectFormTargetKeys(resolvedTargets);
20101
+ this.validateConflictingTargets(resolvedTargets);
20056
20102
  if (dryRun && check) {
20057
20103
  throw new Error("--dry-run and --check cannot be used together");
20058
20104
  }
20059
20105
  this.baseDirs = baseDirs;
20060
- this.targets = targets;
20061
- this.features = features;
20106
+ this.targets = resolvedTargets;
20107
+ this.features = resolvedFeatures;
20108
+ this.objectFormTargetKeys = isRulesyncConfigTargetsObject(resolvedTargets) ? _Config.filterValidToolTargets(Object.keys(resolvedTargets)) : void 0;
20062
20109
  this.verbose = verbose;
20063
20110
  this.delete = isDelete;
20064
20111
  this.global = global ?? false;
@@ -20071,11 +20118,37 @@ var Config = class _Config {
20071
20118
  this.check = check ?? false;
20072
20119
  this.sources = sources ?? [];
20073
20120
  }
20121
+ /**
20122
+ * Rejects unknown keys (and the special `*` key) in the object form of
20123
+ * `targets`. For the array form this is already enforced at the Zod schema
20124
+ * level via `z.enum(ALL_TOOL_TARGETS_WITH_WILDCARD)`; for the object form
20125
+ * `z.record(z.string(), ...)` intentionally accepts any string key (to work
20126
+ * around zod's `z.record(z.enum(...))` requiring ALL enum members), so
20127
+ * runtime validation lives here instead.
20128
+ */
20129
+ validateObjectFormTargetKeys(targets) {
20130
+ if (Array.isArray(targets)) return;
20131
+ const validTargets = new Set(ALL_TOOL_TARGETS);
20132
+ for (const key of Object.keys(targets)) {
20133
+ if (key === "*") {
20134
+ throw new Error(
20135
+ "Invalid target '*' in object form: wildcard is only supported in the array form `targets: ['*']`. Per-target options cannot be attached to a wildcard."
20136
+ );
20137
+ }
20138
+ if (!validTargets.has(key)) {
20139
+ throw new Error(`Unknown target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
20140
+ }
20141
+ }
20142
+ }
20074
20143
  validateConflictingTargets(targets) {
20144
+ const has = (target) => {
20145
+ if (Array.isArray(targets)) {
20146
+ return targets.includes(target);
20147
+ }
20148
+ return Object.prototype.hasOwnProperty.call(targets, target);
20149
+ };
20075
20150
  for (const [target1, target2] of CONFLICTING_TARGET_PAIRS) {
20076
- const hasTarget1 = targets.includes(target1);
20077
- const hasTarget2 = targets.includes(target2);
20078
- if (hasTarget1 && hasTarget2) {
20151
+ if (has(target1) && has(target2)) {
20079
20152
  throw new Error(
20080
20153
  `Conflicting targets: '${target1}' and '${target2}' cannot be used together. Please choose one.`
20081
20154
  );
@@ -20085,16 +20158,45 @@ var Config = class _Config {
20085
20158
  getBaseDirs() {
20086
20159
  return this.baseDirs;
20087
20160
  }
20161
+ /**
20162
+ * Filter an arbitrary string-key list down to the known `ToolTarget` set,
20163
+ * skipping `*` (which is only meaningful as an array element, not a key).
20164
+ */
20165
+ static filterValidToolTargets(keys) {
20166
+ const validTargets = new Set(ALL_TOOL_TARGETS);
20167
+ const result = [];
20168
+ for (const key of keys) {
20169
+ if (key === "*") continue;
20170
+ if (!validTargets.has(key)) continue;
20171
+ result.push(key);
20172
+ }
20173
+ return result;
20174
+ }
20088
20175
  getTargets() {
20089
- if (this.targets.includes("*")) {
20176
+ if (this.objectFormTargetKeys !== void 0) {
20177
+ return this.objectFormTargetKeys;
20178
+ }
20179
+ const arrayTargets = Array.isArray(this.targets) ? this.targets : [];
20180
+ if (!Array.isArray(this.features)) {
20181
+ return _Config.filterValidToolTargets(Object.keys(this.features));
20182
+ }
20183
+ if (arrayTargets.includes("*")) {
20090
20184
  return ALL_TOOL_TARGETS.filter(
20091
20185
  // eslint-disable-next-line no-type-assertion/no-type-assertion
20092
20186
  (target) => !LEGACY_TARGETS.includes(target)
20093
20187
  );
20094
20188
  }
20095
- return this.targets.filter((target) => target !== "*");
20189
+ return arrayTargets.filter((target) => target !== "*");
20096
20190
  }
20097
20191
  getFeatures(target) {
20192
+ if (isRulesyncConfigTargetsObject(this.targets)) {
20193
+ if (target) {
20194
+ const value = this.targets[target];
20195
+ if (!value) return [];
20196
+ return _Config.normalizeTargetFeatures(value);
20197
+ }
20198
+ return _Config.collectAllFeatures(Object.values(this.targets));
20199
+ }
20098
20200
  if (!Array.isArray(this.features)) {
20099
20201
  const perTargetFeatures = this.features;
20100
20202
  if (target) {
@@ -20104,20 +20206,7 @@ var Config = class _Config {
20104
20206
  }
20105
20207
  return _Config.normalizeTargetFeatures(targetFeatures);
20106
20208
  }
20107
- const allFeatures = [];
20108
- for (const features of Object.values(perTargetFeatures)) {
20109
- if (!features) continue;
20110
- const normalized = _Config.normalizeTargetFeatures(features);
20111
- for (const feature of normalized) {
20112
- if (!allFeatures.includes(feature)) {
20113
- allFeatures.push(feature);
20114
- }
20115
- }
20116
- if (allFeatures.length === ALL_FEATURES.length) {
20117
- return allFeatures;
20118
- }
20119
- }
20120
- return allFeatures;
20209
+ return _Config.collectAllFeatures(Object.values(perTargetFeatures));
20121
20210
  }
20122
20211
  if (this.features.includes("*")) {
20123
20212
  return [...ALL_FEATURES];
@@ -20145,23 +20234,40 @@ var Config = class _Config {
20145
20234
  }
20146
20235
  return enabled;
20147
20236
  }
20237
+ /**
20238
+ * Collect the union of features across all per-target values.
20239
+ * Used when `getFeatures()` is called without a target in object mode.
20240
+ */
20241
+ static collectAllFeatures(values) {
20242
+ const allFeatures = [];
20243
+ for (const value of values) {
20244
+ if (!value) continue;
20245
+ const normalized = _Config.normalizeTargetFeatures(value);
20246
+ for (const feature of normalized) {
20247
+ if (!allFeatures.includes(feature)) {
20248
+ allFeatures.push(feature);
20249
+ }
20250
+ }
20251
+ if (allFeatures.length === ALL_FEATURES.length) {
20252
+ return allFeatures;
20253
+ }
20254
+ }
20255
+ return allFeatures;
20256
+ }
20148
20257
  /**
20149
20258
  * Returns the per-feature options object for a given target/feature, if any.
20150
20259
  * Returns `undefined` when no per-feature options were provided or when the
20151
20260
  * feature is not enabled for the given target.
20152
20261
  */
20153
20262
  getFeatureOptions(target, feature) {
20154
- if (Array.isArray(this.features)) {
20155
- return void 0;
20156
- }
20157
- const targetFeatures = this.features[target];
20158
- if (!targetFeatures || Array.isArray(targetFeatures)) {
20263
+ const value = isRulesyncConfigTargetsObject(this.targets) ? this.targets[target] : !Array.isArray(this.features) ? this.features[target] : void 0;
20264
+ if (!value || Array.isArray(value)) {
20159
20265
  return void 0;
20160
20266
  }
20161
- const perFeature = targetFeatures;
20162
- const value = perFeature[feature];
20163
- if (value && typeof value === "object" && isFeatureValueEnabled(value)) {
20164
- return value;
20267
+ const perFeature = value;
20268
+ const featureValue = perFeature[feature];
20269
+ if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) {
20270
+ return featureValue;
20165
20271
  }
20166
20272
  return void 0;
20167
20273
  }
@@ -20169,6 +20275,13 @@ var Config = class _Config {
20169
20275
  * Check if per-target features configuration is being used.
20170
20276
  */
20171
20277
  hasPerTargetFeatures() {
20278
+ return isRulesyncConfigTargetsObject(this.targets) || !Array.isArray(this.features);
20279
+ }
20280
+ /**
20281
+ * Returns true if the deprecated object form under `features` is in use.
20282
+ * Callers can use this to emit a migration warning.
20283
+ */
20284
+ hasDeprecatedFeaturesObjectForm() {
20172
20285
  return !Array.isArray(this.features);
20173
20286
  }
20174
20287
  getVerbose() {
@@ -20213,6 +20326,17 @@ var Config = class _Config {
20213
20326
  }
20214
20327
  };
20215
20328
 
20329
+ // src/config/deprecation-warnings.ts
20330
+ var deprecationWarningEmitted = false;
20331
+ var emitFeaturesObjectFormDeprecationWarning = () => {
20332
+ if (deprecationWarningEmitted) return;
20333
+ if (process.env.RULESYNC_SILENT_DEPRECATION) return;
20334
+ deprecationWarningEmitted = true;
20335
+ console.warn(
20336
+ "[rulesync] DEPRECATED: 'features' object form is deprecated. Use the new 'targets' object form instead: `targets: { claudecode: { rules: true, ignore: { fileMode: 'local' } } }`. See https://github.com/dyoshikawa/rulesync/blob/main/docs/guide/configuration.md for the migration guide."
20337
+ );
20338
+ };
20339
+
20216
20340
  // src/config/config-resolver.ts
20217
20341
  var getDefaults = () => ({
20218
20342
  targets: ["agentsmd"],
@@ -20239,6 +20363,10 @@ var loadConfigFromFile = async (filePath) => {
20239
20363
  const jsonData = (0, import_jsonc_parser4.parse)(fileContent);
20240
20364
  const parsed = ConfigFileSchema.parse(jsonData);
20241
20365
  const { $schema: _schema, ...configParams } = parsed;
20366
+ assertTargetsFeaturesExclusive({
20367
+ targets: configParams.targets,
20368
+ features: configParams.features
20369
+ });
20242
20370
  return configParams;
20243
20371
  };
20244
20372
  var mergeConfigs = (baseConfig, localConfig) => {
@@ -20282,14 +20410,35 @@ var ConfigResolver = class {
20282
20410
  const localConfigPath = (0, import_node_path134.join)(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
20283
20411
  const localConfig = await loadConfigFromFile(localConfigPath);
20284
20412
  const configByFile = mergeConfigs(baseConfig, localConfig);
20413
+ try {
20414
+ assertTargetsFeaturesExclusive({
20415
+ targets: configByFile.targets,
20416
+ features: configByFile.features
20417
+ });
20418
+ } catch (error) {
20419
+ const detail = error instanceof Error ? error.message : String(error);
20420
+ throw new Error(
20421
+ `${detail} (detected after merging '${validatedConfigPath}' with '${localConfigPath}' \u2014 the two files combined produce the invalid combination; remove the conflicting field from one of them).`,
20422
+ { cause: error }
20423
+ );
20424
+ }
20285
20425
  const resolvedGlobal = global ?? configByFile.global ?? getDefaults().global;
20286
20426
  const resolvedSimulateCommands = simulateCommands ?? configByFile.simulateCommands ?? getDefaults().simulateCommands;
20287
20427
  const resolvedSimulateSubagents = simulateSubagents ?? configByFile.simulateSubagents ?? getDefaults().simulateSubagents;
20288
20428
  const resolvedSimulateSkills = simulateSkills ?? configByFile.simulateSkills ?? getDefaults().simulateSkills;
20289
20429
  const resolvedGitignoreTargetsOnly = gitignoreTargetsOnly ?? configByFile.gitignoreTargetsOnly ?? getDefaults().gitignoreTargetsOnly;
20430
+ const userProvidedFeatures = features ?? configByFile.features;
20431
+ const userProvidedTargets = targets ?? configByFile.targets;
20432
+ const targetsIsObject = userProvidedTargets !== void 0 && !Array.isArray(userProvidedTargets);
20433
+ const featuresIsObject = userProvidedFeatures !== void 0 && !Array.isArray(userProvidedFeatures);
20434
+ if (featuresIsObject) {
20435
+ emitFeaturesObjectFormDeprecationWarning();
20436
+ }
20437
+ const resolvedFeatures = userProvidedFeatures ?? (targetsIsObject ? void 0 : getDefaults().features);
20438
+ const resolvedTargets = userProvidedTargets ?? (featuresIsObject ? void 0 : getDefaults().targets);
20290
20439
  const configParams = {
20291
- targets: targets ?? configByFile.targets ?? getDefaults().targets,
20292
- features: features ?? configByFile.features ?? getDefaults().features,
20440
+ targets: resolvedTargets,
20441
+ features: resolvedFeatures,
20293
20442
  verbose: verbose ?? configByFile.verbose ?? getDefaults().verbose,
20294
20443
  delete: isDelete ?? configByFile.delete ?? getDefaults().delete,
20295
20444
  baseDirs: getBaseDirsInLightOfGlobal({
@@ -22510,6 +22659,12 @@ async function importCommand(logger5, options) {
22510
22659
  if (!options.targets) {
22511
22660
  throw new CLIError("No tools found in --targets", ErrorCodes.IMPORT_FAILED);
22512
22661
  }
22662
+ if (!Array.isArray(options.targets)) {
22663
+ throw new CLIError(
22664
+ "--targets object form is not supported on the command line",
22665
+ ErrorCodes.IMPORT_FAILED
22666
+ );
22667
+ }
22513
22668
  if (options.targets.length > 1) {
22514
22669
  throw new CLIError("Only one tool can be imported at a time", ErrorCodes.IMPORT_FAILED);
22515
22670
  }
@@ -25210,7 +25365,11 @@ var resolveGitignoreTargets = async ({
25210
25365
  }
25211
25366
  const config = await ConfigResolver.resolve({});
25212
25367
  if (config.getGitignoreTargetsOnly()) {
25213
- return config.getTargets();
25368
+ const targets = config.getTargets();
25369
+ if (targets.includes("agentsmd")) {
25370
+ return targets;
25371
+ }
25372
+ return [...targets, "agentsmd"];
25214
25373
  }
25215
25374
  return void 0;
25216
25375
  };
@@ -25617,7 +25776,7 @@ function wrapCommand({
25617
25776
  }
25618
25777
 
25619
25778
  // src/cli/index.ts
25620
- var getVersion = () => "8.1.0";
25779
+ var getVersion = () => "8.2.0";
25621
25780
  function wrapCommand2(name, errorCode, handler) {
25622
25781
  return wrapCommand({ name, errorCode, handler, getVersion });
25623
25782
  }
package/dist/cli/index.js CHANGED
@@ -73,7 +73,7 @@ import {
73
73
  stringifyFrontmatter,
74
74
  toPosixPath,
75
75
  writeFileContent
76
- } from "../chunk-FLZZ3LEK.js";
76
+ } from "../chunk-K4IN6URH.js";
77
77
 
78
78
  // src/cli/index.ts
79
79
  import { Command } from "commander";
@@ -1552,6 +1552,12 @@ async function importCommand(logger5, options) {
1552
1552
  if (!options.targets) {
1553
1553
  throw new CLIError("No tools found in --targets", ErrorCodes.IMPORT_FAILED);
1554
1554
  }
1555
+ if (!Array.isArray(options.targets)) {
1556
+ throw new CLIError(
1557
+ "--targets object form is not supported on the command line",
1558
+ ErrorCodes.IMPORT_FAILED
1559
+ );
1560
+ }
1555
1561
  if (options.targets.length > 1) {
1556
1562
  throw new CLIError("Only one tool can be imported at a time", ErrorCodes.IMPORT_FAILED);
1557
1563
  }
@@ -4106,7 +4112,11 @@ var resolveGitignoreTargets = async ({
4106
4112
  }
4107
4113
  const config = await ConfigResolver.resolve({});
4108
4114
  if (config.getGitignoreTargetsOnly()) {
4109
- return config.getTargets();
4115
+ const targets = config.getTargets();
4116
+ if (targets.includes("agentsmd")) {
4117
+ return targets;
4118
+ }
4119
+ return [...targets, "agentsmd"];
4110
4120
  }
4111
4121
  return void 0;
4112
4122
  };
@@ -4513,7 +4523,7 @@ function wrapCommand({
4513
4523
  }
4514
4524
 
4515
4525
  // src/cli/index.ts
4516
- var getVersion = () => "8.1.0";
4526
+ var getVersion = () => "8.2.0";
4517
4527
  function wrapCommand2(name, errorCode, handler) {
4518
4528
  return wrapCommand({ name, errorCode, handler, getVersion });
4519
4529
  }
package/dist/index.cjs CHANGED
@@ -262,12 +262,13 @@ var FeaturesSchema = import_mini.z.array(FeatureSchema);
262
262
  var FeatureOptionsSchema = import_mini.z.record(import_mini.z.string(), import_mini.z.unknown());
263
263
  var FeatureValueSchema = import_mini.z.union([import_mini.z.boolean(), FeatureOptionsSchema]);
264
264
  var PerFeatureConfigSchema = import_mini.z.record(import_mini.z.string(), FeatureValueSchema);
265
+ var PerTargetFeaturesValueSchema = import_mini.z.union([
266
+ import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)),
267
+ PerFeatureConfigSchema
268
+ ]);
265
269
  var RulesyncFeaturesSchema = import_mini.z.union([
266
270
  import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)),
267
- import_mini.z.record(
268
- import_mini.z.string(),
269
- import_mini.z.union([import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD)), PerFeatureConfigSchema])
270
- )
271
+ import_mini.z.record(import_mini.z.string(), PerTargetFeaturesValueSchema)
271
272
  ]);
272
273
  var isFeatureValueEnabled = (value) => {
273
274
  if (value === true) return true;
@@ -310,6 +311,14 @@ var ALL_TOOL_TARGETS_WITH_WILDCARD = [...ALL_TOOL_TARGETS, "*"];
310
311
  var ToolTargetSchema = import_mini2.z.enum(ALL_TOOL_TARGETS);
311
312
  var ToolTargetsSchema = import_mini2.z.array(ToolTargetSchema);
312
313
  var RulesyncTargetsSchema = import_mini2.z.array(import_mini2.z.enum(ALL_TOOL_TARGETS_WITH_WILDCARD));
314
+ var RulesyncConfigTargetsObjectSchema = import_mini2.z.record(import_mini2.z.string(), PerTargetFeaturesValueSchema);
315
+ var RulesyncConfigTargetsSchema = import_mini2.z.union([
316
+ RulesyncTargetsSchema,
317
+ RulesyncConfigTargetsObjectSchema
318
+ ]);
319
+ var isRulesyncConfigTargetsObject = (value) => {
320
+ return !Array.isArray(value);
321
+ };
313
322
 
314
323
  // src/utils/validation.ts
315
324
  function findControlCharacter(value) {
@@ -346,7 +355,7 @@ var SourceEntrySchema = import_mini3.z.object({
346
355
  });
347
356
  var ConfigParamsSchema = import_mini3.z.object({
348
357
  baseDirs: import_mini3.z.array(import_mini3.z.string()),
349
- targets: RulesyncTargetsSchema,
358
+ targets: RulesyncConfigTargetsSchema,
350
359
  features: RulesyncFeaturesSchema,
351
360
  verbose: import_mini3.z.boolean(),
352
361
  delete: import_mini3.z.boolean(),
@@ -373,10 +382,42 @@ var CONFLICTING_TARGET_PAIRS = [
373
382
  ["claudecode", "claudecode-legacy"]
374
383
  ];
375
384
  var LEGACY_TARGETS = ["augmentcode-legacy", "claudecode-legacy"];
385
+ var assertTargetsFeaturesExclusive = ({
386
+ targets,
387
+ features
388
+ }) => {
389
+ const targetsIsObject = targets !== void 0 && !Array.isArray(targets);
390
+ const featuresIsObject = features !== void 0 && !Array.isArray(features);
391
+ if (targetsIsObject && features !== void 0) {
392
+ throw new Error(
393
+ "Invalid config: when 'targets' is in object form, 'features' must be omitted. Declare per-target features inside the 'targets' object instead."
394
+ );
395
+ }
396
+ if (featuresIsObject && targets !== void 0) {
397
+ throw new Error(
398
+ "Invalid config: when 'features' is in object form, 'targets' must be omitted. Migrate to the 'targets' object form, e.g. `targets: { claudecode: [...] }`."
399
+ );
400
+ }
401
+ };
402
+ var assertTargetsOrFeaturesProvided = ({
403
+ targets,
404
+ features
405
+ }) => {
406
+ if (targets === void 0 && features === void 0) {
407
+ throw new Error("Invalid config: at least one of 'targets' or 'features' must be provided.");
408
+ }
409
+ };
376
410
  var Config = class _Config {
377
411
  baseDirs;
378
412
  targets;
379
413
  features;
414
+ /**
415
+ * Cached list of validated `ToolTarget` keys for the object form of
416
+ * `targets`. Populated in the constructor after `validateObjectFormTargetKeys`
417
+ * so `getTargets()` does not rebuild the `ALL_TOOL_TARGETS` set on every call.
418
+ * Undefined when `this.targets` is in array form.
419
+ */
420
+ objectFormTargetKeys;
380
421
  verbose;
381
422
  delete;
382
423
  global;
@@ -404,13 +445,19 @@ var Config = class _Config {
404
445
  check,
405
446
  sources
406
447
  }) {
407
- this.validateConflictingTargets(targets);
448
+ assertTargetsFeaturesExclusive({ targets, features });
449
+ assertTargetsOrFeaturesProvided({ targets, features });
450
+ const resolvedTargets = targets ?? [];
451
+ const resolvedFeatures = features ?? [];
452
+ this.validateObjectFormTargetKeys(resolvedTargets);
453
+ this.validateConflictingTargets(resolvedTargets);
408
454
  if (dryRun && check) {
409
455
  throw new Error("--dry-run and --check cannot be used together");
410
456
  }
411
457
  this.baseDirs = baseDirs;
412
- this.targets = targets;
413
- this.features = features;
458
+ this.targets = resolvedTargets;
459
+ this.features = resolvedFeatures;
460
+ this.objectFormTargetKeys = isRulesyncConfigTargetsObject(resolvedTargets) ? _Config.filterValidToolTargets(Object.keys(resolvedTargets)) : void 0;
414
461
  this.verbose = verbose;
415
462
  this.delete = isDelete;
416
463
  this.global = global ?? false;
@@ -423,11 +470,37 @@ var Config = class _Config {
423
470
  this.check = check ?? false;
424
471
  this.sources = sources ?? [];
425
472
  }
473
+ /**
474
+ * Rejects unknown keys (and the special `*` key) in the object form of
475
+ * `targets`. For the array form this is already enforced at the Zod schema
476
+ * level via `z.enum(ALL_TOOL_TARGETS_WITH_WILDCARD)`; for the object form
477
+ * `z.record(z.string(), ...)` intentionally accepts any string key (to work
478
+ * around zod's `z.record(z.enum(...))` requiring ALL enum members), so
479
+ * runtime validation lives here instead.
480
+ */
481
+ validateObjectFormTargetKeys(targets) {
482
+ if (Array.isArray(targets)) return;
483
+ const validTargets = new Set(ALL_TOOL_TARGETS);
484
+ for (const key of Object.keys(targets)) {
485
+ if (key === "*") {
486
+ throw new Error(
487
+ "Invalid target '*' in object form: wildcard is only supported in the array form `targets: ['*']`. Per-target options cannot be attached to a wildcard."
488
+ );
489
+ }
490
+ if (!validTargets.has(key)) {
491
+ throw new Error(`Unknown target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
492
+ }
493
+ }
494
+ }
426
495
  validateConflictingTargets(targets) {
496
+ const has = (target) => {
497
+ if (Array.isArray(targets)) {
498
+ return targets.includes(target);
499
+ }
500
+ return Object.prototype.hasOwnProperty.call(targets, target);
501
+ };
427
502
  for (const [target1, target2] of CONFLICTING_TARGET_PAIRS) {
428
- const hasTarget1 = targets.includes(target1);
429
- const hasTarget2 = targets.includes(target2);
430
- if (hasTarget1 && hasTarget2) {
503
+ if (has(target1) && has(target2)) {
431
504
  throw new Error(
432
505
  `Conflicting targets: '${target1}' and '${target2}' cannot be used together. Please choose one.`
433
506
  );
@@ -437,16 +510,45 @@ var Config = class _Config {
437
510
  getBaseDirs() {
438
511
  return this.baseDirs;
439
512
  }
513
+ /**
514
+ * Filter an arbitrary string-key list down to the known `ToolTarget` set,
515
+ * skipping `*` (which is only meaningful as an array element, not a key).
516
+ */
517
+ static filterValidToolTargets(keys) {
518
+ const validTargets = new Set(ALL_TOOL_TARGETS);
519
+ const result = [];
520
+ for (const key of keys) {
521
+ if (key === "*") continue;
522
+ if (!validTargets.has(key)) continue;
523
+ result.push(key);
524
+ }
525
+ return result;
526
+ }
440
527
  getTargets() {
441
- if (this.targets.includes("*")) {
528
+ if (this.objectFormTargetKeys !== void 0) {
529
+ return this.objectFormTargetKeys;
530
+ }
531
+ const arrayTargets = Array.isArray(this.targets) ? this.targets : [];
532
+ if (!Array.isArray(this.features)) {
533
+ return _Config.filterValidToolTargets(Object.keys(this.features));
534
+ }
535
+ if (arrayTargets.includes("*")) {
442
536
  return ALL_TOOL_TARGETS.filter(
443
537
  // eslint-disable-next-line no-type-assertion/no-type-assertion
444
538
  (target) => !LEGACY_TARGETS.includes(target)
445
539
  );
446
540
  }
447
- return this.targets.filter((target) => target !== "*");
541
+ return arrayTargets.filter((target) => target !== "*");
448
542
  }
449
543
  getFeatures(target) {
544
+ if (isRulesyncConfigTargetsObject(this.targets)) {
545
+ if (target) {
546
+ const value = this.targets[target];
547
+ if (!value) return [];
548
+ return _Config.normalizeTargetFeatures(value);
549
+ }
550
+ return _Config.collectAllFeatures(Object.values(this.targets));
551
+ }
450
552
  if (!Array.isArray(this.features)) {
451
553
  const perTargetFeatures = this.features;
452
554
  if (target) {
@@ -456,20 +558,7 @@ var Config = class _Config {
456
558
  }
457
559
  return _Config.normalizeTargetFeatures(targetFeatures);
458
560
  }
459
- const allFeatures = [];
460
- for (const features of Object.values(perTargetFeatures)) {
461
- if (!features) continue;
462
- const normalized = _Config.normalizeTargetFeatures(features);
463
- for (const feature of normalized) {
464
- if (!allFeatures.includes(feature)) {
465
- allFeatures.push(feature);
466
- }
467
- }
468
- if (allFeatures.length === ALL_FEATURES.length) {
469
- return allFeatures;
470
- }
471
- }
472
- return allFeatures;
561
+ return _Config.collectAllFeatures(Object.values(perTargetFeatures));
473
562
  }
474
563
  if (this.features.includes("*")) {
475
564
  return [...ALL_FEATURES];
@@ -497,23 +586,40 @@ var Config = class _Config {
497
586
  }
498
587
  return enabled;
499
588
  }
589
+ /**
590
+ * Collect the union of features across all per-target values.
591
+ * Used when `getFeatures()` is called without a target in object mode.
592
+ */
593
+ static collectAllFeatures(values) {
594
+ const allFeatures = [];
595
+ for (const value of values) {
596
+ if (!value) continue;
597
+ const normalized = _Config.normalizeTargetFeatures(value);
598
+ for (const feature of normalized) {
599
+ if (!allFeatures.includes(feature)) {
600
+ allFeatures.push(feature);
601
+ }
602
+ }
603
+ if (allFeatures.length === ALL_FEATURES.length) {
604
+ return allFeatures;
605
+ }
606
+ }
607
+ return allFeatures;
608
+ }
500
609
  /**
501
610
  * Returns the per-feature options object for a given target/feature, if any.
502
611
  * Returns `undefined` when no per-feature options were provided or when the
503
612
  * feature is not enabled for the given target.
504
613
  */
505
614
  getFeatureOptions(target, feature) {
506
- if (Array.isArray(this.features)) {
615
+ const value = isRulesyncConfigTargetsObject(this.targets) ? this.targets[target] : !Array.isArray(this.features) ? this.features[target] : void 0;
616
+ if (!value || Array.isArray(value)) {
507
617
  return void 0;
508
618
  }
509
- const targetFeatures = this.features[target];
510
- if (!targetFeatures || Array.isArray(targetFeatures)) {
511
- return void 0;
512
- }
513
- const perFeature = targetFeatures;
514
- const value = perFeature[feature];
515
- if (value && typeof value === "object" && isFeatureValueEnabled(value)) {
516
- return value;
619
+ const perFeature = value;
620
+ const featureValue = perFeature[feature];
621
+ if (featureValue && typeof featureValue === "object" && isFeatureValueEnabled(featureValue)) {
622
+ return featureValue;
517
623
  }
518
624
  return void 0;
519
625
  }
@@ -521,6 +627,13 @@ var Config = class _Config {
521
627
  * Check if per-target features configuration is being used.
522
628
  */
523
629
  hasPerTargetFeatures() {
630
+ return isRulesyncConfigTargetsObject(this.targets) || !Array.isArray(this.features);
631
+ }
632
+ /**
633
+ * Returns true if the deprecated object form under `features` is in use.
634
+ * Callers can use this to emit a migration warning.
635
+ */
636
+ hasDeprecatedFeaturesObjectForm() {
524
637
  return !Array.isArray(this.features);
525
638
  }
526
639
  getVerbose() {
@@ -565,6 +678,17 @@ var Config = class _Config {
565
678
  }
566
679
  };
567
680
 
681
+ // src/config/deprecation-warnings.ts
682
+ var deprecationWarningEmitted = false;
683
+ var emitFeaturesObjectFormDeprecationWarning = () => {
684
+ if (deprecationWarningEmitted) return;
685
+ if (process.env.RULESYNC_SILENT_DEPRECATION) return;
686
+ deprecationWarningEmitted = true;
687
+ console.warn(
688
+ "[rulesync] DEPRECATED: 'features' object form is deprecated. Use the new 'targets' object form instead: `targets: { claudecode: { rules: true, ignore: { fileMode: 'local' } } }`. See https://github.com/dyoshikawa/rulesync/blob/main/docs/guide/configuration.md for the migration guide."
689
+ );
690
+ };
691
+
568
692
  // src/config/config-resolver.ts
569
693
  var getDefaults = () => ({
570
694
  targets: ["agentsmd"],
@@ -591,6 +715,10 @@ var loadConfigFromFile = async (filePath) => {
591
715
  const jsonData = (0, import_jsonc_parser.parse)(fileContent);
592
716
  const parsed = ConfigFileSchema.parse(jsonData);
593
717
  const { $schema: _schema, ...configParams } = parsed;
718
+ assertTargetsFeaturesExclusive({
719
+ targets: configParams.targets,
720
+ features: configParams.features
721
+ });
594
722
  return configParams;
595
723
  };
596
724
  var mergeConfigs = (baseConfig, localConfig) => {
@@ -634,14 +762,35 @@ var ConfigResolver = class {
634
762
  const localConfigPath = (0, import_node_path4.join)(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
635
763
  const localConfig = await loadConfigFromFile(localConfigPath);
636
764
  const configByFile = mergeConfigs(baseConfig, localConfig);
765
+ try {
766
+ assertTargetsFeaturesExclusive({
767
+ targets: configByFile.targets,
768
+ features: configByFile.features
769
+ });
770
+ } catch (error) {
771
+ const detail = error instanceof Error ? error.message : String(error);
772
+ throw new Error(
773
+ `${detail} (detected after merging '${validatedConfigPath}' with '${localConfigPath}' \u2014 the two files combined produce the invalid combination; remove the conflicting field from one of them).`,
774
+ { cause: error }
775
+ );
776
+ }
637
777
  const resolvedGlobal = global ?? configByFile.global ?? getDefaults().global;
638
778
  const resolvedSimulateCommands = simulateCommands ?? configByFile.simulateCommands ?? getDefaults().simulateCommands;
639
779
  const resolvedSimulateSubagents = simulateSubagents ?? configByFile.simulateSubagents ?? getDefaults().simulateSubagents;
640
780
  const resolvedSimulateSkills = simulateSkills ?? configByFile.simulateSkills ?? getDefaults().simulateSkills;
641
781
  const resolvedGitignoreTargetsOnly = gitignoreTargetsOnly ?? configByFile.gitignoreTargetsOnly ?? getDefaults().gitignoreTargetsOnly;
782
+ const userProvidedFeatures = features ?? configByFile.features;
783
+ const userProvidedTargets = targets ?? configByFile.targets;
784
+ const targetsIsObject = userProvidedTargets !== void 0 && !Array.isArray(userProvidedTargets);
785
+ const featuresIsObject = userProvidedFeatures !== void 0 && !Array.isArray(userProvidedFeatures);
786
+ if (featuresIsObject) {
787
+ emitFeaturesObjectFormDeprecationWarning();
788
+ }
789
+ const resolvedFeatures = userProvidedFeatures ?? (targetsIsObject ? void 0 : getDefaults().features);
790
+ const resolvedTargets = userProvidedTargets ?? (featuresIsObject ? void 0 : getDefaults().targets);
642
791
  const configParams = {
643
- targets: targets ?? configByFile.targets ?? getDefaults().targets,
644
- features: features ?? configByFile.features ?? getDefaults().features,
792
+ targets: resolvedTargets,
793
+ features: resolvedFeatures,
645
794
  verbose: verbose ?? configByFile.verbose ?? getDefaults().verbose,
646
795
  delete: isDelete ?? configByFile.delete ?? getDefaults().delete,
647
796
  baseDirs: getBaseDirsInLightOfGlobal({
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  checkRulesyncDirExists,
7
7
  generate,
8
8
  importFromTool
9
- } from "./chunk-FLZZ3LEK.js";
9
+ } from "./chunk-K4IN6URH.js";
10
10
 
11
11
  // src/index.ts
12
12
  async function generate2(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "8.1.0",
3
+ "version": "8.2.0",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",
@@ -93,6 +93,7 @@
93
93
  "simple-git-hooks": "2.13.1",
94
94
  "sort-package-json": "3.6.1",
95
95
  "tsup": "8.5.1",
96
+ "tsx": "4.21.0",
96
97
  "typescript": "5.9.3",
97
98
  "typescript-eslint": "8.57.1",
98
99
  "vite": "7.3.1",
@@ -113,7 +114,7 @@
113
114
  "cicheck:code": "pnpm run check && pnpm run test",
114
115
  "cicheck:content": "pnpm run cspell && pnpm run secretlint",
115
116
  "cspell": "cspell --no-progress --gitignore .",
116
- "dev": "node --experimental-transform-types --experimental-loader ./scripts/strip-types-loader.mjs src/cli/index.ts",
117
+ "dev": "tsx src/cli/index.ts",
117
118
  "docs:build": "vitepress build docs",
118
119
  "docs:dev": "vitepress dev docs",
119
120
  "docs:preview": "vitepress preview docs",
@@ -123,13 +124,13 @@
123
124
  "fmt": "oxfmt .",
124
125
  "fmt:check": "oxfmt --check .",
125
126
  "generate": "pnpm run dev generate",
126
- "generate:schema": "node --experimental-transform-types --experimental-loader ./scripts/strip-types-loader.mjs scripts/generate-json-schema.ts",
127
+ "generate:schema": "tsx scripts/generate-json-schema.ts",
127
128
  "knip": "knip",
128
129
  "oxlint": "oxlint . --max-warnings 0",
129
130
  "oxlint:fix": "oxlint . --fix --max-warnings 0",
130
131
  "secretlint": "secretlint --secretlintignore .gitignore \"**/*\"",
131
132
  "sort": "sort-package-json",
132
- "task": "node --experimental-transform-types --experimental-loader ./scripts/strip-types-loader.mjs scripts/run-tasks.ts",
133
+ "task": "tsx scripts/run-tasks.ts",
133
134
  "test": "vitest run --silent=true",
134
135
  "test:coverage": "vitest run --coverage --silent=true",
135
136
  "test:e2e": "vitest run --config vitest.e2e.config.ts --silent=false",