rulesync 9.8.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,6 +99,9 @@ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH,
99
99
  const RULESYNC_MCP_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
100
100
  const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
101
101
  const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
102
+ const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
103
+ const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
104
+ const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
102
105
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
103
106
  const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
104
107
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
@@ -109,8 +112,12 @@ const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
109
112
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
110
113
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
111
114
  const RULESYNC_PERMISSIONS_FILE_NAME = "permissions.json";
115
+ const RULESYNC_MCP_JSONC_FILE_NAME = "mcp.jsonc";
116
+ const RULESYNC_HOOKS_JSONC_FILE_NAME = "hooks.jsonc";
117
+ const RULESYNC_PERMISSIONS_JSONC_FILE_NAME = "permissions.jsonc";
112
118
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
113
119
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
120
+ const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
114
121
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
115
122
  //#endregion
116
123
  //#region src/types/tool-target-tuples.ts
@@ -7317,12 +7324,55 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
7317
7324
  return canonical;
7318
7325
  }
7319
7326
  //#endregion
7327
+ //#region src/utils/jsonc.ts
7328
+ /**
7329
+ * Rebuild the parsed value from its own enumerable entries, dropping
7330
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
7331
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
7332
+ * literal `__proto__` key would replace the containing object's prototype
7333
+ * instead of becoming an own property; rebuilding gives every object a clean
7334
+ * `Object.prototype` again and severs that path.
7335
+ */
7336
+ function deepSanitize(value) {
7337
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
7338
+ if (value !== null && typeof value === "object") {
7339
+ const sanitized = {};
7340
+ for (const [key, entry] of Object.entries(value)) {
7341
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
7342
+ sanitized[key] = deepSanitize(entry);
7343
+ }
7344
+ return sanitized;
7345
+ }
7346
+ return value;
7347
+ }
7348
+ /**
7349
+ * Parse a JSONC (JSON with Comments) document strictly.
7350
+ *
7351
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
7352
+ * errors and returns a best-effort value), this throws on any parse error so
7353
+ * a malformed source file fails loudly instead of generating half-empty tool
7354
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
7355
+ * `JSON.parse` on files that may contain comments or trailing commas.
7356
+ */
7357
+ function parseJsonc(content) {
7358
+ const errors = [];
7359
+ const result = parse(content, errors, {
7360
+ allowTrailingComma: true,
7361
+ disallowComments: false
7362
+ });
7363
+ if (errors.length > 0) {
7364
+ const details = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
7365
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
7366
+ }
7367
+ return deepSanitize(result);
7368
+ }
7369
+ //#endregion
7320
7370
  //#region src/features/hooks/rulesync-hooks.ts
7321
7371
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7322
7372
  json;
7323
7373
  constructor(params) {
7324
7374
  super({ ...params });
7325
- this.json = JSON.parse(this.fileContent);
7375
+ this.json = parseJsonc(this.fileContent);
7326
7376
  if (params.validate) {
7327
7377
  const result = this.validate();
7328
7378
  if (!result.success) throw result.error;
@@ -7331,7 +7381,11 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7331
7381
  static getSettablePaths() {
7332
7382
  return {
7333
7383
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7334
- relativeFilePath: "hooks.json"
7384
+ relativeFilePath: RULESYNC_HOOKS_FILE_NAME,
7385
+ jsonc: {
7386
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7387
+ relativeFilePath: RULESYNC_HOOKS_JSONC_FILE_NAME
7388
+ }
7335
7389
  };
7336
7390
  }
7337
7391
  validate() {
@@ -7347,16 +7401,23 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7347
7401
  }
7348
7402
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
7349
7403
  const paths = RulesyncHooks.getSettablePaths();
7350
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7351
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} found.`);
7352
- const fileContent = await readFileContent(filePath);
7353
- return new RulesyncHooks({
7354
- outputRoot,
7404
+ const candidates = [paths.jsonc, {
7355
7405
  relativeDirPath: paths.relativeDirPath,
7356
- relativeFilePath: paths.relativeFilePath,
7357
- fileContent,
7358
- validate
7359
- });
7406
+ relativeFilePath: paths.relativeFilePath
7407
+ }];
7408
+ for (const candidate of candidates) {
7409
+ const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
7410
+ if (!await fileExists(filePath)) continue;
7411
+ const fileContent = await readFileContent(filePath);
7412
+ return new RulesyncHooks({
7413
+ outputRoot,
7414
+ relativeDirPath: candidate.relativeDirPath,
7415
+ relativeFilePath: candidate.relativeFilePath,
7416
+ fileContent,
7417
+ validate
7418
+ });
7419
+ }
7420
+ throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} or ${RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH} found.`);
7360
7421
  }
7361
7422
  getJson() {
7362
7423
  return this.json;
@@ -12364,15 +12425,51 @@ const RulesyncMcpServerSchema = z.extend(McpServerSchema, {
12364
12425
  exposed: z.optional(z.boolean())
12365
12426
  });
12366
12427
  const RulesyncMcpConfigSchema = z.object({ mcpServers: z.record(z.string(), RulesyncMcpServerSchema) });
12428
+ /**
12429
+ * Tool-scoped MCP block: servers that apply only to one tool. A named entry
12430
+ * replaces/adds the same-named shared server wholesale for that tool; `null`
12431
+ * removes the shared server for that tool. Mirrors `{toolname}.hooks` in
12432
+ * `.rulesync/hooks.json` and `{toolname}.permission` in
12433
+ * `.rulesync/permissions.json`.
12434
+ */
12435
+ const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.string(), z.nullable(RulesyncMcpServerSchema))) });
12367
12436
  const RulesyncMcpFileSchema = z.looseObject({
12368
12437
  $schema: z.optional(z.string()),
12369
- ...RulesyncMcpConfigSchema.shape
12438
+ ...RulesyncMcpConfigSchema.shape,
12439
+ amp: z.optional(toolScopedMcpSchema),
12440
+ "antigravity-cli": z.optional(toolScopedMcpSchema),
12441
+ "antigravity-ide": z.optional(toolScopedMcpSchema),
12442
+ augmentcode: z.optional(toolScopedMcpSchema),
12443
+ claudecode: z.optional(toolScopedMcpSchema),
12444
+ cline: z.optional(toolScopedMcpSchema),
12445
+ codexcli: z.optional(toolScopedMcpSchema),
12446
+ copilot: z.optional(toolScopedMcpSchema),
12447
+ copilotcli: z.optional(toolScopedMcpSchema),
12448
+ cursor: z.optional(toolScopedMcpSchema),
12449
+ deepagents: z.optional(toolScopedMcpSchema),
12450
+ devin: z.optional(toolScopedMcpSchema),
12451
+ factorydroid: z.optional(toolScopedMcpSchema),
12452
+ goose: z.optional(toolScopedMcpSchema),
12453
+ grokcli: z.optional(toolScopedMcpSchema),
12454
+ hermesagent: z.optional(toolScopedMcpSchema),
12455
+ junie: z.optional(toolScopedMcpSchema),
12456
+ kilo: z.optional(toolScopedMcpSchema),
12457
+ kiro: z.optional(toolScopedMcpSchema),
12458
+ opencode: z.optional(toolScopedMcpSchema),
12459
+ qwencode: z.optional(toolScopedMcpSchema),
12460
+ reasonix: z.optional(toolScopedMcpSchema),
12461
+ roo: z.optional(toolScopedMcpSchema),
12462
+ rovodev: z.optional(toolScopedMcpSchema),
12463
+ takt: z.optional(toolScopedMcpSchema),
12464
+ vibe: z.optional(toolScopedMcpSchema),
12465
+ warp: z.optional(toolScopedMcpSchema),
12466
+ zed: z.optional(toolScopedMcpSchema)
12370
12467
  });
12371
12468
  var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12372
12469
  json;
12373
12470
  constructor(params) {
12374
12471
  super(params);
12375
- this.json = JSON.parse(this.fileContent);
12472
+ this.json = parseJsonc(this.fileContent);
12376
12473
  if (params.validate) {
12377
12474
  const result = this.validate();
12378
12475
  if (!result.success) throw result.error;
@@ -12382,7 +12479,11 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12382
12479
  return {
12383
12480
  recommended: {
12384
12481
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12385
- relativeFilePath: "mcp.json"
12482
+ relativeFilePath: RULESYNC_MCP_FILE_NAME
12483
+ },
12484
+ jsonc: {
12485
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12486
+ relativeFilePath: RULESYNC_MCP_JSONC_FILE_NAME
12386
12487
  },
12387
12488
  legacy: {
12388
12489
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
@@ -12404,7 +12505,18 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12404
12505
  static async fromFile({ outputRoot = process.cwd(), validate = true, logger }) {
12405
12506
  const paths = this.getSettablePaths();
12406
12507
  const recommendedPath = join(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
12508
+ const jsoncPath = join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
12407
12509
  const legacyPath = join(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
12510
+ if (await fileExists(jsoncPath)) {
12511
+ const fileContent = await readFileContent(jsoncPath);
12512
+ return new RulesyncMcp({
12513
+ outputRoot,
12514
+ relativeDirPath: paths.jsonc.relativeDirPath,
12515
+ relativeFilePath: paths.jsonc.relativeFilePath,
12516
+ fileContent,
12517
+ validate
12518
+ });
12519
+ }
12408
12520
  if (await fileExists(recommendedPath)) {
12409
12521
  const fileContent = await readFileContent(recommendedPath);
12410
12522
  return new RulesyncMcp({
@@ -12464,7 +12576,141 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12464
12576
  getJson() {
12465
12577
  return this.json;
12466
12578
  }
12467
- };
12579
+ /**
12580
+ * Build the effective MCP config for one tool target:
12581
+ *
12582
+ * 1. Filter shared servers by the DEPRECATED per-server `targets` field
12583
+ * (missing/`["*"]` means every tool). A deprecation warning points at
12584
+ * the tool-scoped blocks that replace it.
12585
+ * 2. Overlay the tool-scoped `{toolname}.mcpServers` block(s): a named
12586
+ * entry replaces/adds the shared server wholesale for this tool; `null`
12587
+ * removes it.
12588
+ *
12589
+ * Targets that share one output file resolve identically so the shared
12590
+ * file's content never depends on which of them generates last — see
12591
+ * `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
12592
+ * and the Antigravity pair).
12593
+ *
12594
+ * Returns the same instance when neither mechanism is used.
12595
+ */
12596
+ forTarget({ toolTarget, logger }) {
12597
+ const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
12598
+ const json = this.json;
12599
+ const sharedServers = this.json.mcpServers ?? {};
12600
+ const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
12601
+ if (serverNamesWithTargets.length > 0) this.warnTargetsDeprecationOnce({
12602
+ serverNamesWithTargets,
12603
+ logger
12604
+ });
12605
+ for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
12606
+ if (!isRecord(json[ignoredKey])) continue;
12607
+ this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
12608
+ }
12609
+ const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
12610
+ if (serverNamesWithTargets.length === 0 && toolBlockKeys.length === 0) return this;
12611
+ const effectiveServers = Object.fromEntries(Object.entries(sharedServers).filter(([, serverConfig]) => {
12612
+ const targets = serverConfig.targets;
12613
+ if (targets === void 0) return true;
12614
+ return targets.some((target) => target === "*" || acceptedTargetNames.has(target));
12615
+ }));
12616
+ for (const blockKey of blockKeys) {
12617
+ const toolBlock = json[blockKey];
12618
+ const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
12619
+ for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
12620
+ if (isPrototypePollutionKey(serverName)) continue;
12621
+ if (serverConfig === null) delete effectiveServers[serverName];
12622
+ else effectiveServers[serverName] = serverConfig;
12623
+ }
12624
+ }
12625
+ const rest = Object.fromEntries(Object.entries(json).filter(([key]) => !MCP_TOOL_BLOCK_KEYS.has(key)));
12626
+ return new RulesyncMcp({
12627
+ outputRoot: this.outputRoot,
12628
+ relativeDirPath: this.relativeDirPath,
12629
+ relativeFilePath: this.relativeFilePath,
12630
+ fileContent: JSON.stringify({
12631
+ ...rest,
12632
+ mcpServers: effectiveServers
12633
+ }, null, 2)
12634
+ });
12635
+ }
12636
+ /**
12637
+ * The deprecation warning would otherwise repeat once per generated tool
12638
+ * target (a full `--targets "*"` run creates one RulesyncMcp per target),
12639
+ * so it is deduplicated per source file path.
12640
+ */
12641
+ warnTargetsDeprecationOnce({ serverNamesWithTargets, logger }) {
12642
+ this.warnOncePerFile("targets-deprecation", `The per-server "targets" field in ${join(this.relativeDirPath, this.relativeFilePath)} is deprecated (servers: ${serverNamesWithTargets.join(", ")}). Author tool-scoped "{toolname}.mcpServers" blocks instead.`, logger);
12643
+ }
12644
+ warnOncePerFile(kind, message, logger) {
12645
+ const dedupeKey = `${this.getFilePath()}#${kind}`;
12646
+ if (warnedOnceKeys.has(dedupeKey)) return;
12647
+ warnedOnceKeys.add(dedupeKey);
12648
+ logger?.warn(message);
12649
+ }
12650
+ };
12651
+ /**
12652
+ * All keys that may hold a tool-scoped `{toolname}.mcpServers` block. Derived
12653
+ * from the MCP processor's target tuple so a newly added MCP-capable tool is
12654
+ * covered automatically.
12655
+ */
12656
+ const MCP_TOOL_BLOCK_KEYS = new Set(mcpProcessorToolTargetTuple);
12657
+ /**
12658
+ * Targets whose `{toolname}.mcpServers` block key is ALWAYS another target's
12659
+ * key (at every scope): `claudecode-legacy` is a deprecated alias of
12660
+ * `claudecode`, and the Kiro IDE/CLI targets share the `kiro` block because
12661
+ * all three write the same `.kiro/settings/mcp.json` at both scopes. Blocks
12662
+ * authored under these source names are never read (a warning is emitted).
12663
+ */
12664
+ const MCP_BLOCK_KEY_ALIASES = {
12665
+ "claudecode-legacy": "claudecode",
12666
+ "kiro-cli": "kiro",
12667
+ "kiro-ide": "kiro"
12668
+ };
12669
+ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
12670
+ /**
12671
+ * Resolve which tool-scoped blocks a target reads and which deprecated
12672
+ * `targets` names match it. Targets that share one output file must resolve
12673
+ * identically, otherwise the shared file's content would depend on which
12674
+ * target happened to generate last:
12675
+ *
12676
+ * - `claudecode-legacy` aliases `claudecode`; the Kiro trio shares `kiro`
12677
+ * (same output file at both scopes).
12678
+ * - `antigravity-ide` / `antigravity-cli` share their output file at BOTH
12679
+ * scopes — `.agents/mcp_config.json` (project) and
12680
+ * `~/.gemini/config/mcp_config.json` (global; both global subdirs are
12681
+ * `config`) — so both targets always apply both blocks in a fixed order
12682
+ * (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
12683
+ * per server on conflict).
12684
+ */
12685
+ function resolveMcpTarget({ toolTarget }) {
12686
+ if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
12687
+ blockKeys: ["claudecode"],
12688
+ acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
12689
+ };
12690
+ if (toolTarget === "kiro" || toolTarget === "kiro-cli" || toolTarget === "kiro-ide") return {
12691
+ blockKeys: ["kiro"],
12692
+ acceptedTargetNames: /* @__PURE__ */ new Set([
12693
+ "kiro",
12694
+ "kiro-cli",
12695
+ "kiro-ide"
12696
+ ])
12697
+ };
12698
+ if (toolTarget === "antigravity-ide" || toolTarget === "antigravity-cli") return {
12699
+ blockKeys: ["antigravity-ide", "antigravity-cli"],
12700
+ acceptedTargetNames: /* @__PURE__ */ new Set(["antigravity-ide", "antigravity-cli"])
12701
+ };
12702
+ return {
12703
+ blockKeys: [toolTarget],
12704
+ acceptedTargetNames: /* @__PURE__ */ new Set([toolTarget])
12705
+ };
12706
+ }
12707
+ /**
12708
+ * Deduplication set for once-per-source-file warnings, keyed by
12709
+ * `<absolute file path>#<warning kind>`. Never cleared: rulesync CLI runs
12710
+ * are one-shot processes, and in a long-lived embedding (the rulesync MCP
12711
+ * server) repeating the same warning per generate would only add noise.
12712
+ */
12713
+ const warnedOnceKeys = /* @__PURE__ */ new Set();
12468
12714
  //#endregion
12469
12715
  //#region src/features/mcp/tool-mcp.ts
12470
12716
  var ToolMcp = class extends ToolFile {
@@ -16714,10 +16960,14 @@ var McpProcessor = class extends FeatureProcessor {
16714
16960
  if (!rulesyncMcp) throw new Error(`No ${RULESYNC_MCP_RELATIVE_FILE_PATH} found.`);
16715
16961
  const factory = this.getFactory(this.toolTarget);
16716
16962
  return await Promise.all([rulesyncMcp].map(async (mcp) => {
16963
+ const targetedRulesyncMcp = mcp.forTarget({
16964
+ toolTarget: this.toolTarget,
16965
+ logger: this.logger
16966
+ });
16717
16967
  const fieldsToStrip = [];
16718
16968
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
16719
16969
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
16720
- const filteredRulesyncMcp = mcp.stripMcpServerFields(fieldsToStrip);
16970
+ const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
16721
16971
  return await factory.class.fromRulesyncMcp({
16722
16972
  outputRoot: this.outputRoot,
16723
16973
  rulesyncMcp: filteredRulesyncMcp,
@@ -16767,6 +17017,25 @@ const PermissionActionSchema = z.enum([
16767
17017
  */
16768
17018
  const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
16769
17019
  /**
17020
+ * Tool-scoped canonical `permission` block: the same shape as the shared
17021
+ * top-level `permission` record, but applied only to the tool whose override
17022
+ * key it appears under. During generation the categories placed here are
17023
+ * merged over the shared `permission` block per category (the tool-scoped
17024
+ * category replaces the shared one wholesale), mirroring how
17025
+ * `.rulesync/hooks.json` merges `{toolname}.hooks` per event.
17026
+ *
17027
+ * @example
17028
+ * { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
17029
+ */
17030
+ const ToolScopedPermissionSchema = z.record(z.string(), PermissionRulesSchema);
17031
+ /**
17032
+ * Generic tool-scoped override block for tools that have no tool-specific
17033
+ * override keys of their own; it carries only the canonical tool-scoped
17034
+ * `permission` block. Kept `looseObject` so future tool-specific keys can be
17035
+ * added without a schema break.
17036
+ */
17037
+ const CanonicalPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
17038
+ /**
16770
17039
  * OpenCode-specific permission value. Unlike the shared canonical block, which
16771
17040
  * only accepts a pattern-to-action map, OpenCode also allows a bare action
16772
17041
  * string that applies to the whole category (e.g. `"external_directory": "deny"`).
@@ -16796,7 +17065,7 @@ const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional
16796
17065
  * @example
16797
17066
  * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
16798
17067
  */
16799
- const HermesPermissionsOverrideSchema = z.looseObject({});
17068
+ const HermesPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
16800
17069
  /**
16801
17070
  * Tool-scoped override block for Cline. Cline's `command-permissions.json`
16802
17071
  * carries a single global `allowRedirects` boolean (gates shell redirection
@@ -16808,7 +17077,10 @@ const HermesPermissionsOverrideSchema = z.looseObject({});
16808
17077
  * @example
16809
17078
  * { "allowRedirects": true }
16810
17079
  */
16811
- const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
17080
+ const ClinePermissionsOverrideSchema = z.looseObject({
17081
+ permission: z.optional(ToolScopedPermissionSchema),
17082
+ allowRedirects: z.optional(z.boolean())
17083
+ });
16812
17084
  /**
16813
17085
  * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
16814
17086
  * object is a free-form record with tool-specific keys that have no canonical
@@ -16838,7 +17110,10 @@ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
16838
17110
  * @example
16839
17111
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16840
17112
  */
16841
- const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
17113
+ const ClaudecodePermissionsOverrideSchema = z.looseObject({
17114
+ permission: z.optional(ToolScopedPermissionSchema),
17115
+ permissions: z.optional(z.looseObject({}))
17116
+ });
16842
17117
  /**
16843
17118
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16844
17119
  * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
@@ -16870,6 +17145,7 @@ const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
16870
17145
  * { "approvalMode": "auto-review" }
16871
17146
  */
16872
17147
  const CursorPermissionsOverrideSchema = z.looseObject({
17148
+ permission: z.optional(ToolScopedPermissionSchema),
16873
17149
  approvalMode: z.optional(z.string()),
16874
17150
  sandbox: z.optional(z.looseObject({}))
16875
17151
  });
@@ -16895,6 +17171,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
16895
17171
  * }
16896
17172
  */
16897
17173
  const QwencodePermissionsOverrideSchema = z.looseObject({
17174
+ permission: z.optional(ToolScopedPermissionSchema),
16898
17175
  tools: z.optional(z.looseObject({})),
16899
17176
  security: z.optional(z.looseObject({})),
16900
17177
  autoMode: z.optional(z.looseObject({}))
@@ -16916,6 +17193,7 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
16916
17193
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16917
17194
  */
16918
17195
  const ReasonixPermissionsOverrideSchema = z.looseObject({
17196
+ permission: z.optional(ToolScopedPermissionSchema),
16919
17197
  sandbox: z.optional(z.looseObject({})),
16920
17198
  agent: z.optional(z.looseObject({}))
16921
17199
  });
@@ -16935,7 +17213,10 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
16935
17213
  * @example
16936
17214
  * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16937
17215
  */
16938
- const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
17216
+ const FactorydroidPermissionsOverrideSchema = z.looseObject({
17217
+ permission: z.optional(ToolScopedPermissionSchema),
17218
+ commandBlocklist: z.optional(z.array(z.string()))
17219
+ });
16939
17220
  /**
16940
17221
  * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16941
17222
  * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
@@ -16952,6 +17233,7 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist:
16952
17233
  * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16953
17234
  */
16954
17235
  const WarpPermissionsOverrideSchema = z.looseObject({
17236
+ permission: z.optional(ToolScopedPermissionSchema),
16955
17237
  agent_mode_coding_permissions: z.optional(z.string()),
16956
17238
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
16957
17239
  agent_mode_execute_readonly_commands: z.optional(z.boolean())
@@ -16969,6 +17251,7 @@ const WarpPermissionsOverrideSchema = z.looseObject({
16969
17251
  * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16970
17252
  */
16971
17253
  const JuniePermissionsOverrideSchema = z.looseObject({
17254
+ permission: z.optional(ToolScopedPermissionSchema),
16972
17255
  allowReadonlyCommands: z.optional(z.boolean()),
16973
17256
  defaultBehavior: z.optional(z.string())
16974
17257
  });
@@ -16996,6 +17279,7 @@ const JuniePermissionsOverrideSchema = z.looseObject({
16996
17279
  * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16997
17280
  */
16998
17281
  const TaktPermissionsOverrideSchema = z.looseObject({
17282
+ permission: z.optional(ToolScopedPermissionSchema),
16999
17283
  step_permission_overrides: z.optional(z.record(z.string(), z.string())),
17000
17284
  provider_options: z.optional(z.looseObject({}))
17001
17285
  });
@@ -17023,6 +17307,7 @@ const TaktPermissionsOverrideSchema = z.looseObject({
17023
17307
  * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
17024
17308
  */
17025
17309
  const AmpPermissionsOverrideSchema = z.looseObject({
17310
+ permission: z.optional(ToolScopedPermissionSchema),
17026
17311
  permissions: z.optional(z.array(z.looseObject({
17027
17312
  tool: z.string(),
17028
17313
  action: z.string()
@@ -17051,6 +17336,7 @@ const AmpPermissionsOverrideSchema = z.looseObject({
17051
17336
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
17052
17337
  */
17053
17338
  const AntigravityCliPermissionsOverrideSchema = z.looseObject({
17339
+ permission: z.optional(ToolScopedPermissionSchema),
17054
17340
  toolPermission: z.optional(z.string()),
17055
17341
  enableTerminalSandbox: z.optional(z.boolean())
17056
17342
  });
@@ -17076,10 +17362,13 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
17076
17362
  * "permission": { "type": "webhook-policy", "webhookUrl": "https://api.example.com/validate" } },
17077
17363
  * { "toolName": "view", "eventType": "tool-response", "permission": { "type": "allow" } } ] }
17078
17364
  */
17079
- const AugmentcodePermissionsOverrideSchema = z.looseObject({ toolPermissions: z.optional(z.array(z.looseObject({
17080
- toolName: z.string(),
17081
- permission: z.looseObject({ type: z.string() })
17082
- }))) });
17365
+ const AugmentcodePermissionsOverrideSchema = z.looseObject({
17366
+ permission: z.optional(ToolScopedPermissionSchema),
17367
+ toolPermissions: z.optional(z.array(z.looseObject({
17368
+ toolName: z.string(),
17369
+ permission: z.looseObject({ type: z.string() })
17370
+ })))
17371
+ });
17083
17372
  /**
17084
17373
  * Tool-scoped override block for Kiro. Kiro's agent config (`.kiro/agents/<name>.json`)
17085
17374
  * exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny
@@ -17105,21 +17394,24 @@ const AugmentcodePermissionsOverrideSchema = z.looseObject({ toolPermissions: z.
17105
17394
  * "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] },
17106
17395
  * "web_fetch": { "trusted": [".*github\\.com.*"] } } }
17107
17396
  */
17108
- const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(z.looseObject({
17109
- shell: z.optional(z.looseObject({
17110
- autoAllowReadonly: z.optional(z.boolean()),
17111
- denyByDefault: z.optional(z.boolean())
17112
- })),
17113
- aws: z.optional(z.looseObject({
17114
- allowedServices: z.optional(z.array(z.string())),
17115
- deniedServices: z.optional(z.array(z.string())),
17116
- autoAllowReadonly: z.optional(z.boolean())
17117
- })),
17118
- web_fetch: z.optional(z.looseObject({
17119
- trusted: z.optional(z.array(z.string())),
17120
- blocked: z.optional(z.array(z.string()))
17397
+ const KiroPermissionsOverrideSchema = z.looseObject({
17398
+ permission: z.optional(ToolScopedPermissionSchema),
17399
+ toolsSettings: z.optional(z.looseObject({
17400
+ shell: z.optional(z.looseObject({
17401
+ autoAllowReadonly: z.optional(z.boolean()),
17402
+ denyByDefault: z.optional(z.boolean())
17403
+ })),
17404
+ aws: z.optional(z.looseObject({
17405
+ allowedServices: z.optional(z.array(z.string())),
17406
+ deniedServices: z.optional(z.array(z.string())),
17407
+ autoAllowReadonly: z.optional(z.boolean())
17408
+ })),
17409
+ web_fetch: z.optional(z.looseObject({
17410
+ trusted: z.optional(z.array(z.string())),
17411
+ blocked: z.optional(z.array(z.string()))
17412
+ }))
17121
17413
  }))
17122
- })) });
17414
+ });
17123
17415
  /**
17124
17416
  * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17125
17417
  * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
@@ -17191,6 +17483,7 @@ const CodexApprovalsReviewerSchema = z.enum([
17191
17483
  * "sandbox_workspace_write": { "network_access": true } }
17192
17484
  */
17193
17485
  const CodexcliPermissionsOverrideSchema = z.looseObject({
17486
+ permission: z.optional(ToolScopedPermissionSchema),
17194
17487
  approval_policy: z.optional(z.union([CodexApprovalPolicySchema, z.looseObject({})])),
17195
17488
  sandbox_mode: z.optional(CodexSandboxModeSchema),
17196
17489
  sandbox_workspace_write: z.optional(z.looseObject({})),
@@ -17209,6 +17502,15 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
17209
17502
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
17210
17503
  * block and ignores them.
17211
17504
  *
17505
+ * Additionally, every permissions-capable tool accepts a canonical tool-scoped
17506
+ * `permission` block under its override key (`{toolname}.permission`, same
17507
+ * shape as the shared `permission` record). Its categories apply only to that
17508
+ * tool, merged over the shared block per category — see
17509
+ * `RulesyncPermissions.forTarget`. `kiro-cli`/`kiro-ide` alias to the `kiro`
17510
+ * key and `hermesagent` to `hermes` (matching the shared output file each
17511
+ * writes). OpenCode/Kilo/Vibe keep their existing tool-native `permission`
17512
+ * override semantics (handled by their translators, not the central merge).
17513
+ *
17212
17514
  * @example
17213
17515
  * {
17214
17516
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
@@ -17234,7 +17536,13 @@ const PermissionsConfigSchema = z.looseObject({
17234
17536
  "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
17235
17537
  augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
17236
17538
  kiro: z.optional(KiroPermissionsOverrideSchema),
17237
- codexcli: z.optional(CodexcliPermissionsOverrideSchema)
17539
+ codexcli: z.optional(CodexcliPermissionsOverrideSchema),
17540
+ "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
17541
+ devin: z.optional(CanonicalPermissionsOverrideSchema),
17542
+ goose: z.optional(CanonicalPermissionsOverrideSchema),
17543
+ grokcli: z.optional(CanonicalPermissionsOverrideSchema),
17544
+ rovodev: z.optional(CanonicalPermissionsOverrideSchema),
17545
+ zed: z.optional(CanonicalPermissionsOverrideSchema)
17238
17546
  });
17239
17547
  /**
17240
17548
  * Full permissions file schema including optional $schema field.
@@ -17249,7 +17557,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17249
17557
  json;
17250
17558
  constructor(params) {
17251
17559
  super({ ...params });
17252
- this.json = JSON.parse(this.fileContent);
17560
+ this.json = parseJsonc(this.fileContent);
17253
17561
  if (params.validate) {
17254
17562
  const result = this.validate();
17255
17563
  if (!result.success) throw result.error;
@@ -17258,7 +17566,11 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17258
17566
  static getSettablePaths() {
17259
17567
  return {
17260
17568
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17261
- relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME
17569
+ relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
17570
+ jsonc: {
17571
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17572
+ relativeFilePath: RULESYNC_PERMISSIONS_JSONC_FILE_NAME
17573
+ }
17262
17574
  };
17263
17575
  }
17264
17576
  validate() {
@@ -17274,20 +17586,85 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
17274
17586
  }
17275
17587
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
17276
17588
  const paths = RulesyncPermissions.getSettablePaths();
17277
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17278
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
17279
- const fileContent = await readFileContent(filePath);
17280
- return new RulesyncPermissions({
17281
- outputRoot,
17589
+ const candidates = [paths.jsonc, {
17282
17590
  relativeDirPath: paths.relativeDirPath,
17283
- relativeFilePath: paths.relativeFilePath,
17284
- fileContent,
17285
- validate
17286
- });
17591
+ relativeFilePath: paths.relativeFilePath
17592
+ }];
17593
+ for (const candidate of candidates) {
17594
+ const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
17595
+ if (!await fileExists(filePath)) continue;
17596
+ const fileContent = await readFileContent(filePath);
17597
+ return new RulesyncPermissions({
17598
+ outputRoot,
17599
+ relativeDirPath: candidate.relativeDirPath,
17600
+ relativeFilePath: candidate.relativeFilePath,
17601
+ fileContent,
17602
+ validate
17603
+ });
17604
+ }
17605
+ throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} or ${RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH} found.`);
17287
17606
  }
17288
17607
  getJson() {
17289
17608
  return this.json;
17290
17609
  }
17610
+ /**
17611
+ * Build the effective permissions config for one tool target by merging the
17612
+ * tool-scoped `{toolname}.permission` block over the shared `permission`
17613
+ * block, per category (the tool-scoped category replaces the shared one
17614
+ * wholesale — mirroring how `{toolname}.hooks` merges per event in
17615
+ * `.rulesync/hooks.json`). The consumed `permission` key is stripped from
17616
+ * the override block so verbatim-passthrough translators (e.g. the Hermes
17617
+ * deep merge or the Codex CLI top-level key whitelist) never see it.
17618
+ *
17619
+ * Returns the same instance when the target has no tool-scoped canonical
17620
+ * `permission` block, or when the target's translator natively consumes its
17621
+ * own `permission` override shape (OpenCode / Kilo / Vibe).
17622
+ */
17623
+ forTarget({ toolTarget, logger }) {
17624
+ if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
17625
+ const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
17626
+ const json = this.json;
17627
+ if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
17628
+ const overrideBlock = json[overrideKey];
17629
+ if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
17630
+ const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
17631
+ const merged = {
17632
+ ...json,
17633
+ permission: {
17634
+ ...this.json.permission,
17635
+ ...toolScopedPermission
17636
+ }
17637
+ };
17638
+ if (Object.keys(restOverride).length > 0) merged[overrideKey] = restOverride;
17639
+ else delete merged[overrideKey];
17640
+ return new RulesyncPermissions({
17641
+ outputRoot: this.outputRoot,
17642
+ relativeDirPath: this.relativeDirPath,
17643
+ relativeFilePath: this.relativeFilePath,
17644
+ fileContent: JSON.stringify(merged, null, 2)
17645
+ });
17646
+ }
17647
+ };
17648
+ /**
17649
+ * Targets whose tool-scoped `permission` override uses tool-native semantics
17650
+ * consumed directly by their translator (bare action strings / tool-only
17651
+ * categories for OpenCode and Kilo, `sensitive_patterns` objects for Vibe).
17652
+ * The central canonical merge must not touch those blocks.
17653
+ */
17654
+ const NATIVE_PERMISSION_OVERRIDE_TARGETS = /* @__PURE__ */ new Set([
17655
+ "opencode",
17656
+ "kilo",
17657
+ "vibe"
17658
+ ]);
17659
+ /**
17660
+ * Targets that read their tool-scoped override from a differently named key:
17661
+ * Kiro IDE/CLI share the `kiro` block (they write the same agent config) and
17662
+ * Hermes Agent's established override key is `hermes`.
17663
+ */
17664
+ const PERMISSION_OVERRIDE_KEY_ALIASES = {
17665
+ "kiro-cli": "kiro",
17666
+ "kiro-ide": "kiro",
17667
+ hermesagent: "hermes"
17291
17668
  };
17292
17669
  //#endregion
17293
17670
  //#region src/features/permissions/tool-permissions.ts
@@ -19127,10 +19504,15 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
19127
19504
  existingDomainsHadUnknown,
19128
19505
  logger
19129
19506
  });
19130
- permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
19507
+ const profile = mergeWithExistingProfile({
19131
19508
  newProfile,
19132
19509
  existingProfile
19133
19510
  });
19511
+ permissionsTable[RULESYNC_PROFILE_NAME] = preserveUnmanagedProfileKeys({
19512
+ rawExistingProfile: permissionsTable[RULESYNC_PROFILE_NAME],
19513
+ profile,
19514
+ logger
19515
+ });
19134
19516
  const overridePatch = computeCodexcliOverridePatch({
19135
19517
  existing,
19136
19518
  override: rulesyncPermissions.getJson().codexcli,
@@ -19326,6 +19708,43 @@ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingD
19326
19708
  if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
19327
19709
  if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19328
19710
  }
19711
+ const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19712
+ "description",
19713
+ "extends",
19714
+ "filesystem",
19715
+ "network"
19716
+ ]);
19717
+ const MANAGED_NETWORK_KEYS = /* @__PURE__ */ new Set([
19718
+ "enabled",
19719
+ "mode",
19720
+ "domains",
19721
+ "unix_sockets"
19722
+ ]);
19723
+ /**
19724
+ * Re-attach the keys of the existing rulesync profile that rulesync does not
19725
+ * manage. The managed keys always come from `profile` (the freshly computed
19726
+ * merge result); unmanaged siblings — at the profile level and inside the
19727
+ * `network` table — are preserved verbatim from the existing config so a
19728
+ * regeneration never deletes user-authored Codex settings.
19729
+ */
19730
+ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19731
+ const rawProfileTable = toMutableTable(rawExistingProfile);
19732
+ const profileExtras = Object.fromEntries(Object.entries(rawProfileTable).filter(([key]) => !MANAGED_PROFILE_KEYS.has(key)));
19733
+ const rawNetworkTable = toMutableTable(rawProfileTable.network);
19734
+ const networkExtras = Object.fromEntries(Object.entries(rawNetworkTable).filter(([key]) => !MANAGED_NETWORK_KEYS.has(key)));
19735
+ const preservedKeyNames = [...Object.keys(profileExtras), ...Object.keys(networkExtras).map((key) => `network.${key}`)];
19736
+ if (preservedKeyNames.length > 0) logger?.warn(`Preserving unmanaged keys in the "${RULESYNC_PROFILE_NAME}" permissions profile: ${preservedKeyNames.join(", ")}. Review them manually; rulesync carries them forward verbatim.`);
19737
+ const result = {
19738
+ ...profileExtras,
19739
+ ...profile
19740
+ };
19741
+ const mergedNetwork = {
19742
+ ...networkExtras,
19743
+ ...profile.network
19744
+ };
19745
+ if (Object.keys(mergedNetwork).length > 0) result.network = mergedNetwork;
19746
+ return result;
19747
+ }
19329
19748
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19330
19749
  if (!existingProfile) return newProfile;
19331
19750
  const mergedNetwork = { ...newProfile.network };
@@ -23587,16 +24006,20 @@ var PermissionsProcessor = class extends FeatureProcessor {
23587
24006
  if (!rulesyncPermissions) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
23588
24007
  const factory = toolPermissionsFactories.get(this.toolTarget);
23589
24008
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
24009
+ const effectivePermissions = rulesyncPermissions.forTarget({
24010
+ toolTarget: this.toolTarget,
24011
+ logger: this.logger
24012
+ });
23590
24013
  const toolPermissions = await factory.class.fromRulesyncPermissions({
23591
24014
  outputRoot: this.outputRoot,
23592
- rulesyncPermissions,
24015
+ rulesyncPermissions: effectivePermissions,
23593
24016
  logger: this.logger,
23594
24017
  global: this.global
23595
24018
  });
23596
24019
  if (this.toolTarget !== "codexcli") return [toolPermissions];
23597
24020
  return [toolPermissions, createCodexcliBashRulesFile({
23598
24021
  outputRoot: this.outputRoot,
23599
- config: rulesyncPermissions.getJson()
24022
+ config: effectivePermissions.getJson()
23600
24023
  })];
23601
24024
  }
23602
24025
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -39692,6 +40115,16 @@ async function generatePermissionsCore(params) {
39692
40115
  //#endregion
39693
40116
  //#region src/lib/import.ts
39694
40117
  /**
40118
+ * Import always writes the `.json` variant of a fixed-path source file, but a
40119
+ * sibling `.jsonc` variant takes precedence at read time — so an import into a
40120
+ * project that authors the `.jsonc` variant would be silently shadowed. Warn
40121
+ * so the user merges (or removes) one of the two by hand.
40122
+ */
40123
+ async function warnIfShadowedByJsonc(params) {
40124
+ const { outputRoot, jsonRelativePath, jsoncRelativePath, logger } = params;
40125
+ if (await fileExists(join(outputRoot, jsoncRelativePath))) logger.warn(`${jsoncRelativePath} exists and takes precedence over the imported ${jsonRelativePath}. Merge the imported content into ${jsoncRelativePath} (or remove it) so the import takes effect.`);
40126
+ }
40127
+ /**
39695
40128
  * Import configuration files from AI tools.
39696
40129
  */
39697
40130
  async function importFromTool(params) {
@@ -39803,6 +40236,12 @@ async function importMcpCore(params) {
39803
40236
  }
39804
40237
  const rulesyncFiles = await mcpProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39805
40238
  const { count: writtenCount } = await mcpProcessor.writeAiFiles(rulesyncFiles);
40239
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40240
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40241
+ jsonRelativePath: RULESYNC_MCP_RELATIVE_FILE_PATH,
40242
+ jsoncRelativePath: RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH,
40243
+ logger
40244
+ });
39806
40245
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} MCP files`);
39807
40246
  return writtenCount;
39808
40247
  }
@@ -39902,6 +40341,12 @@ async function importHooksCore(params) {
39902
40341
  }
39903
40342
  const rulesyncFiles = await hooksProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39904
40343
  const { count: writtenCount } = await hooksProcessor.writeAiFiles(rulesyncFiles);
40344
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40345
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40346
+ jsonRelativePath: RULESYNC_HOOKS_RELATIVE_FILE_PATH,
40347
+ jsoncRelativePath: RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH,
40348
+ logger
40349
+ });
39905
40350
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} hooks file(s)`);
39906
40351
  return writtenCount;
39907
40352
  }
@@ -39931,10 +40376,16 @@ async function importPermissionsCore(params) {
39931
40376
  }
39932
40377
  const rulesyncFiles = await permissionsProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39933
40378
  const { count: writtenCount } = await permissionsProcessor.writeAiFiles(rulesyncFiles);
40379
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40380
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40381
+ jsonRelativePath: RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
40382
+ jsoncRelativePath: RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH,
40383
+ logger
40384
+ });
39934
40385
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} permissions file(s)`);
39935
40386
  return writtenCount;
39936
40387
  }
39937
40388
  //#endregion
39938
- export { removeTempDirectory as $, RulesyncCommand as A, checkPathTraversal as B, RulesyncHooks as C, RULESYNC_RULES_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, formatError as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Et, findControlCharacter as F, findFilesByGlobs as G, directoryExists as H, ConsoleLogger as I, isSymlink as J, getFileSize as K, JsonLogger as L, stringifyFrontmatter as M, loadYaml as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES as Ot, ConfigResolver as P, removeFile as Q, CLIError as R, HooksProcessor as S, RULESYNC_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Tt, ensureDir as U, createTempDirectory as V, fileExists as W, readFileContent as X, listDirectoryFiles as Y, removeDirectory as Z, RulesyncPermissions as _, RULESYNC_MCP_RELATIVE_FILE_PATH as _t, convertFromTool as a, MAX_FILE_SIZE as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_FILE_NAME as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dt, toPosixPath as et, SkillsProcessor as f, RULESYNC_HOOKS_FILE_NAME as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_FILE_NAME as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ToolTargetSchema as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, ALL_FEATURES_WITH_WILDCARD as kt, SubagentsProcessor as l, RULESYNC_CONFIG_RELATIVE_FILE_PATH as lt, RulesyncSkill as m, RULESYNC_IGNORE_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS as nt, RulesProcessor as o, RULESYNC_AIIGNORE_FILE_NAME as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_RELATIVE_FILE_PATH as pt, getHomeDirectory as q, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rt, RulesyncRule as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as st, importFromTool as t, writeFileContent as tt, RulesyncSubagent as u, RULESYNC_CONFIG_SCHEMA_URL as ut, McpProcessor as v, RULESYNC_MCP_SCHEMA_URL as vt, CommandsProcessor as w, RULESYNC_SKILLS_RELATIVE_DIR_PATH as wt, RulesyncIgnore as x, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as xt, RulesyncMcp as y, RULESYNC_OVERVIEW_FILE_NAME as yt, ErrorCodes as z };
40389
+ export { removeFile as $, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as At, ErrorCodes as B, RulesyncHooks as C, RULESYNC_PERMISSIONS_FILE_NAME as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_RELATIVE_DIR_PATH as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_SCHEMA_URL as Et, ConfigResolver as F, fileExists as G, createTempDirectory as H, findControlCharacter as I, getHomeDirectory as J, findFilesByGlobs as K, ConsoleLogger as L, RulesyncCommandFrontmatterSchema as M, formatError as Mt, stringifyFrontmatter as N, ALL_FEATURES as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RULES_RELATIVE_DIR_PATH as Ot, loadYaml as P, ALL_FEATURES_WITH_WILDCARD as Pt, removeDirectory as Q, JsonLogger as R, HooksProcessor as S, RULESYNC_OVERVIEW_FILE_NAME as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Tt, directoryExists as U, checkPathTraversal as V, ensureDir as W, listDirectoryFiles as X, isSymlink as Y, readFileContent as Z, RulesyncPermissions as _, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as _t, convertFromTool as a, ToolTargetSchema as at, IgnoreProcessor as b, RULESYNC_MCP_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CONFIG_SCHEMA_URL as dt, removeTempDirectory as et, SkillsProcessor as f, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_IGNORE_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ALL_TOOL_TARGETS_WITH_WILDCARD as it, RulesyncCommand as j, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_SKILLS_RELATIVE_DIR_PATH as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as lt, RulesyncSkill as m, RULESYNC_HOOKS_JSONC_FILE_NAME as mt, checkRulesyncDirExists as n, writeFileContent as nt, RulesProcessor as o, MAX_FILE_SIZE as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_FILE_NAME as pt, getFileSize as q, generate as r, ALL_TOOL_TARGETS as rt, RulesyncRule as s, RULESYNC_AIIGNORE_FILE_NAME as st, importFromTool as t, toPosixPath as tt, RulesyncSubagent as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ut, McpProcessor as v, RULESYNC_MCP_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_PERMISSIONS_JSONC_FILE_NAME as wt, RulesyncIgnore as x, RULESYNC_MCP_SCHEMA_URL as xt, RulesyncMcp as y, RULESYNC_MCP_JSONC_FILE_NAME as yt, CLIError as z };
39939
40390
 
39940
- //# sourceMappingURL=import-Dioh9ca8.js.map
40391
+ //# sourceMappingURL=import-B02AyWkQ.js.map