rulesync 9.6.3 → 9.7.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.
@@ -8,6 +8,7 @@ import { intersection, kebabCase, uniq } from "es-toolkit";
8
8
  import { globbySync } from "globby";
9
9
  import { isDeepStrictEqual } from "node:util";
10
10
  import * as smolToml from "smol-toml";
11
+ import { parse as parse$1, stringify } from "smol-toml";
11
12
  import matter from "gray-matter";
12
13
  import { YAMLException, dump, load } from "js-yaml";
13
14
  import { omit } from "es-toolkit/object";
@@ -2677,6 +2678,13 @@ const CODEXCLI_MCP_FILE_NAME = "config.toml";
2677
2678
  const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
2678
2679
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
2679
2680
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = join("agents", "openai.yaml");
2681
+ const CODEXCLI_OVERRIDE_KEYS = [
2682
+ "approval_policy",
2683
+ "sandbox_mode",
2684
+ "sandbox_workspace_write",
2685
+ "apps",
2686
+ "approvals_reviewer"
2687
+ ];
2680
2688
  //#endregion
2681
2689
  //#region src/features/commands/codexcli-command.ts
2682
2690
  const CodexcliCommandFrontmatterSchema = z.looseObject({
@@ -4055,14 +4063,22 @@ function sanitizeSharedConfigValue(value) {
4055
4063
  * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
4056
4064
  * path when one is given.
4057
4065
  */
4058
- function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
4066
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty", jsoncParseErrors = "tolerate" }) {
4059
4067
  if (fileContent.trim() === "") return {};
4060
4068
  const at = filePath === void 0 ? "" : ` at ${filePath}`;
4061
4069
  let parsed;
4062
4070
  try {
4063
4071
  if (format === "yaml") parsed = loadYaml(fileContent);
4072
+ else if (format === "toml") parsed = parse$1(fileContent);
4064
4073
  else if (format === "json") parsed = JSON.parse(fileContent);
4065
- else parsed = parse(fileContent);
4074
+ else if (jsoncParseErrors === "error") {
4075
+ const errors = [];
4076
+ parsed = parse(fileContent, errors, { allowTrailingComma: true });
4077
+ if (errors.length > 0) {
4078
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
4079
+ throw new Error(details);
4080
+ }
4081
+ } else parsed = parse(fileContent);
4066
4082
  } catch (error) {
4067
4083
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4068
4084
  }
@@ -4076,13 +4092,15 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
4076
4092
  /**
4077
4093
  * Serialize a shared config document. YAML output always ends with exactly one
4078
4094
  * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4079
- * writers have always emitted (no trailing newline).
4095
+ * writers have always emitted (no trailing newline); TOML output matches the
4096
+ * `smol-toml` `stringify` shape the TOML writers have always emitted.
4080
4097
  */
4081
4098
  function stringifySharedConfig({ format, document }) {
4082
4099
  if (format === "yaml") return dump(document, {
4083
4100
  noRefs: true,
4084
4101
  sortKeys: false
4085
4102
  }).trimEnd() + "\n";
4103
+ if (format === "toml") return stringify(document);
4086
4104
  return JSON.stringify(document, null, 2);
4087
4105
  }
4088
4106
  /**
@@ -4121,6 +4139,25 @@ function mergeSharedConfigDeep({ base, patch }) {
4121
4139
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4122
4140
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4123
4141
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4142
+ const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
4143
+ const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
4144
+ const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
4145
+ const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
4146
+ const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
4147
+ /**
4148
+ * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
4149
+ * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
4150
+ * feature classes don't pull the processor registry through this module and
4151
+ * create an import cycle); the ownership lock-step test keeps the two aligned.
4152
+ * Lets a tool whose file lives at a scope-dependent path (`.zed/settings.json`
4153
+ * vs `.config/zed/settings.json`) resolve its declaration from the settable
4154
+ * paths it already holds.
4155
+ */
4156
+ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
4157
+ const dir = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
4158
+ const file = relativeFilePath.replace(/\\/g, "/");
4159
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
4160
+ };
4124
4161
  /**
4125
4162
  * Who owns what in each gateway-managed shared config file, and which policy
4126
4163
  * resolves conflicts. Keys are `dir/file` tokens matching
@@ -4177,6 +4214,298 @@ const SHARED_CONFIG_OWNERSHIP = {
4177
4214
  },
4178
4215
  permissions: { kind: "deep-merge" }
4179
4216
  }
4217
+ },
4218
+ ".zed/settings.json": {
4219
+ format: "json",
4220
+ features: {
4221
+ ignore: {
4222
+ kind: "replace-owned-keys",
4223
+ ownedKeys: ["private_files"]
4224
+ },
4225
+ mcp: {
4226
+ kind: "replace-owned-keys",
4227
+ ownedKeys: ["context_servers"]
4228
+ },
4229
+ permissions: {
4230
+ kind: "replace-owned-keys",
4231
+ ownedKeys: ["agent"]
4232
+ }
4233
+ }
4234
+ },
4235
+ ".config/zed/settings.json": {
4236
+ format: "json",
4237
+ features: {
4238
+ mcp: {
4239
+ kind: "replace-owned-keys",
4240
+ ownedKeys: ["context_servers"]
4241
+ },
4242
+ permissions: {
4243
+ kind: "replace-owned-keys",
4244
+ ownedKeys: ["agent"]
4245
+ }
4246
+ }
4247
+ },
4248
+ ".qwen/settings.json": {
4249
+ format: "json",
4250
+ features: {
4251
+ mcp: {
4252
+ kind: "replace-owned-keys",
4253
+ ownedKeys: ["mcpServers"]
4254
+ },
4255
+ hooks: {
4256
+ kind: "replace-owned-keys",
4257
+ ownedKeys: ["hooks", "disableAllHooks"]
4258
+ },
4259
+ permissions: {
4260
+ kind: "replace-owned-keys",
4261
+ ownedKeys: [
4262
+ "permissions",
4263
+ "tools",
4264
+ "security"
4265
+ ]
4266
+ }
4267
+ }
4268
+ },
4269
+ ".augment/settings.json": {
4270
+ format: "json",
4271
+ features: {
4272
+ mcp: {
4273
+ kind: "replace-owned-keys",
4274
+ ownedKeys: ["mcpServers"]
4275
+ },
4276
+ hooks: {
4277
+ kind: "replace-owned-keys",
4278
+ ownedKeys: ["hooks"]
4279
+ },
4280
+ permissions: {
4281
+ kind: "replace-owned-keys",
4282
+ ownedKeys: ["toolPermissions"]
4283
+ }
4284
+ }
4285
+ },
4286
+ ".devin/config.json": {
4287
+ format: "json",
4288
+ features: {
4289
+ mcp: {
4290
+ kind: "replace-owned-keys",
4291
+ ownedKeys: ["mcpServers"]
4292
+ },
4293
+ permissions: {
4294
+ kind: "replace-owned-keys",
4295
+ ownedKeys: ["permissions"]
4296
+ }
4297
+ }
4298
+ },
4299
+ ".config/devin/config.json": {
4300
+ format: "json",
4301
+ features: {
4302
+ mcp: {
4303
+ kind: "replace-owned-keys",
4304
+ ownedKeys: ["mcpServers"]
4305
+ },
4306
+ hooks: {
4307
+ kind: "replace-owned-keys",
4308
+ ownedKeys: ["hooks"]
4309
+ },
4310
+ permissions: {
4311
+ kind: "replace-owned-keys",
4312
+ ownedKeys: ["permissions"]
4313
+ }
4314
+ }
4315
+ },
4316
+ ".kiro/agents/default.json": {
4317
+ format: "json",
4318
+ features: {
4319
+ hooks: {
4320
+ kind: "replace-owned-keys",
4321
+ ownedKeys: ["hooks"]
4322
+ },
4323
+ permissions: {
4324
+ kind: "replace-owned-keys",
4325
+ ownedKeys: ["allowedTools", "toolsSettings"]
4326
+ }
4327
+ }
4328
+ },
4329
+ ".amp/settings.json": {
4330
+ format: "jsonc",
4331
+ invalidRootPolicy: "error",
4332
+ jsoncParseErrors: "error",
4333
+ features: {
4334
+ mcp: {
4335
+ kind: "replace-owned-keys",
4336
+ ownedKeys: ["amp.mcpServers"]
4337
+ },
4338
+ permissions: {
4339
+ kind: "replace-owned-keys",
4340
+ ownedKeys: [
4341
+ "amp.tools.disable",
4342
+ "amp.permissions",
4343
+ "amp.guardedFiles.allowlist",
4344
+ "amp.dangerouslyAllowAll",
4345
+ "amp.mcpPermissions"
4346
+ ]
4347
+ }
4348
+ }
4349
+ },
4350
+ ".config/amp/settings.json": {
4351
+ format: "jsonc",
4352
+ invalidRootPolicy: "error",
4353
+ jsoncParseErrors: "error",
4354
+ features: {
4355
+ mcp: {
4356
+ kind: "replace-owned-keys",
4357
+ ownedKeys: ["amp.mcpServers"]
4358
+ },
4359
+ permissions: {
4360
+ kind: "replace-owned-keys",
4361
+ ownedKeys: [
4362
+ "amp.tools.disable",
4363
+ "amp.permissions",
4364
+ "amp.guardedFiles.allowlist",
4365
+ "amp.dangerouslyAllowAll",
4366
+ "amp.mcpPermissions"
4367
+ ]
4368
+ }
4369
+ }
4370
+ },
4371
+ "opencode.json": {
4372
+ format: "jsonc",
4373
+ features: {
4374
+ mcp: {
4375
+ kind: "replace-owned-keys",
4376
+ ownedKeys: ["mcp", "tools"]
4377
+ },
4378
+ permissions: {
4379
+ kind: "replace-owned-keys",
4380
+ ownedKeys: ["permission"]
4381
+ },
4382
+ rules: {
4383
+ kind: "replace-owned-keys",
4384
+ ownedKeys: ["instructions"]
4385
+ }
4386
+ }
4387
+ },
4388
+ ".config/opencode/opencode.json": {
4389
+ format: "jsonc",
4390
+ features: {
4391
+ mcp: {
4392
+ kind: "replace-owned-keys",
4393
+ ownedKeys: ["mcp", "tools"]
4394
+ },
4395
+ permissions: {
4396
+ kind: "replace-owned-keys",
4397
+ ownedKeys: ["permission"]
4398
+ }
4399
+ }
4400
+ },
4401
+ "kilo.json": {
4402
+ format: "jsonc",
4403
+ features: {
4404
+ mcp: {
4405
+ kind: "replace-owned-keys",
4406
+ ownedKeys: ["mcp", "tools"]
4407
+ },
4408
+ rules: {
4409
+ kind: "replace-owned-keys",
4410
+ ownedKeys: ["instructions"]
4411
+ }
4412
+ }
4413
+ },
4414
+ ".config/kilo/kilo.json": {
4415
+ format: "jsonc",
4416
+ features: { mcp: {
4417
+ kind: "replace-owned-keys",
4418
+ ownedKeys: ["mcp", "tools"]
4419
+ } }
4420
+ },
4421
+ [CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
4422
+ format: "toml",
4423
+ features: {
4424
+ hooks: {
4425
+ kind: "replace-owned-keys",
4426
+ ownedKeys: ["features"]
4427
+ },
4428
+ mcp: {
4429
+ kind: "replace-owned-keys",
4430
+ ownedKeys: ["mcp_servers"]
4431
+ },
4432
+ permissions: {
4433
+ kind: "replace-owned-keys",
4434
+ ownedKeys: [
4435
+ "permissions",
4436
+ "default_permissions",
4437
+ ...CODEXCLI_OVERRIDE_KEYS
4438
+ ]
4439
+ }
4440
+ }
4441
+ },
4442
+ [GROKCLI_CONFIG_SHARED_FILE_KEY]: {
4443
+ format: "toml",
4444
+ features: {
4445
+ mcp: {
4446
+ kind: "replace-owned-keys",
4447
+ ownedKeys: ["mcp_servers"]
4448
+ },
4449
+ permissions: {
4450
+ kind: "replace-owned-keys",
4451
+ ownedKeys: ["permission", "ui"]
4452
+ }
4453
+ }
4454
+ },
4455
+ [VIBE_CONFIG_SHARED_FILE_KEY]: {
4456
+ format: "toml",
4457
+ features: {
4458
+ hooks: {
4459
+ kind: "replace-owned-keys",
4460
+ ownedKeys: ["enable_experimental_hooks"]
4461
+ },
4462
+ mcp: {
4463
+ kind: "replace-owned-keys",
4464
+ ownedKeys: ["mcp_servers"]
4465
+ },
4466
+ permissions: {
4467
+ kind: "replace-owned-keys",
4468
+ ownedKeys: [
4469
+ "tools",
4470
+ "enabled_tools",
4471
+ "disabled_tools"
4472
+ ]
4473
+ }
4474
+ }
4475
+ },
4476
+ [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
4477
+ format: "toml",
4478
+ features: {
4479
+ mcp: {
4480
+ kind: "replace-owned-keys",
4481
+ ownedKeys: ["plugins"]
4482
+ },
4483
+ permissions: {
4484
+ kind: "replace-owned-keys",
4485
+ ownedKeys: [
4486
+ "permissions",
4487
+ "sandbox",
4488
+ "agent"
4489
+ ]
4490
+ }
4491
+ }
4492
+ },
4493
+ [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
4494
+ format: "toml",
4495
+ features: {
4496
+ mcp: {
4497
+ kind: "replace-owned-keys",
4498
+ ownedKeys: ["plugins"]
4499
+ },
4500
+ permissions: {
4501
+ kind: "replace-owned-keys",
4502
+ ownedKeys: [
4503
+ "permissions",
4504
+ "sandbox",
4505
+ "agent"
4506
+ ]
4507
+ }
4508
+ }
4180
4509
  }
4181
4510
  };
4182
4511
  /**
@@ -4197,17 +4526,20 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
4197
4526
  format: declaration.format,
4198
4527
  fileContent: existingContent,
4199
4528
  filePath,
4200
- ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4529
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy },
4530
+ ...declaration.jsoncParseErrors !== void 0 && { jsoncParseErrors: declaration.jsoncParseErrors }
4201
4531
  });
4202
4532
  if (policy.kind === "replace-owned-keys") {
4203
4533
  const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4204
4534
  if (unowned.length > 0) throw new Error(`Feature '${feature}' tried to write undeclared keys [${unowned.join(", ")}] to '${fileKey}'; extend its ownedKeys declaration if that ownership is intended.`);
4535
+ const document = mergeSharedConfigShallow({
4536
+ base,
4537
+ patch
4538
+ });
4539
+ for (const [key, value] of Object.entries(patch)) if (value === void 0) delete document[key];
4205
4540
  return stringifySharedConfig({
4206
4541
  format: declaration.format,
4207
- document: mergeSharedConfigShallow({
4208
- base,
4209
- patch
4210
- })
4542
+ document
4211
4543
  });
4212
4544
  }
4213
4545
  const merged = mergeSharedConfigDeep({
@@ -7280,12 +7612,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7280
7612
  const paths = AugmentcodeHooks.getSettablePaths({ global });
7281
7613
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7282
7614
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7283
- let settings;
7284
- try {
7285
- settings = JSON.parse(existingContent);
7286
- } catch (error) {
7287
- throw new Error(`Failed to parse existing AugmentCode settings at ${filePath}: ${formatError(error)}`, { cause: error });
7288
- }
7289
7615
  const config = rulesyncHooks.getJson();
7290
7616
  const augmentHooks = canonicalToToolHooks({
7291
7617
  config,
@@ -7293,11 +7619,13 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7293
7619
  converterConfig: AUGMENTCODE_CONVERTER_CONFIG,
7294
7620
  logger
7295
7621
  });
7296
- const merged = {
7297
- ...settings,
7298
- hooks: augmentHooks
7299
- };
7300
- const fileContent = JSON.stringify(merged, null, 2);
7622
+ const fileContent = applySharedConfigPatch({
7623
+ fileKey: sharedConfigFileKey(paths),
7624
+ feature: "hooks",
7625
+ existingContent,
7626
+ patch: { hooks: augmentHooks },
7627
+ filePath
7628
+ });
7301
7629
  return new AugmentcodeHooks({
7302
7630
  outputRoot,
7303
7631
  relativeDirPath: paths.relativeDirPath,
@@ -7464,15 +7792,28 @@ const CODEXCLI_CONVERTER_CONFIG = {
7464
7792
  */
7465
7793
  async function buildCodexConfigTomlContent({ outputRoot }) {
7466
7794
  const configPath = join(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
7467
- const existingContent = await readFileContentOrNull(configPath) ?? smolToml.stringify({});
7795
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
7468
7796
  let configToml;
7469
7797
  try {
7470
- configToml = smolToml.parse(existingContent);
7798
+ configToml = smolToml.parse(existingContent || smolToml.stringify({}));
7471
7799
  } catch (error) {
7472
7800
  throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
7473
7801
  }
7474
- if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
7475
- return smolToml.stringify(configToml);
7802
+ let features;
7803
+ if (typeof configToml.features === "object" && configToml.features !== null) {
7804
+ features = { ...configToml.features };
7805
+ delete features.codex_hooks;
7806
+ }
7807
+ return applySharedConfigPatch({
7808
+ fileKey: sharedConfigFileKey({
7809
+ relativeDirPath: CODEXCLI_DIR,
7810
+ relativeFilePath: CODEXCLI_MCP_FILE_NAME
7811
+ }),
7812
+ feature: "hooks",
7813
+ existingContent,
7814
+ patch: { features },
7815
+ filePath: configPath
7816
+ });
7476
7817
  }
7477
7818
  /**
7478
7819
  * Represents the `.codex/config.toml` file as a generated ToolFile,
@@ -8388,17 +8729,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
8388
8729
  let fileContent;
8389
8730
  if (global) {
8390
8731
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8391
- let parsedSettings;
8392
- try {
8393
- parsedSettings = JSON.parse(existingContent);
8394
- } catch (error) {
8395
- throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
8396
- }
8397
- const merged = {
8398
- ...isRecord(parsedSettings) ? parsedSettings : {},
8399
- hooks: devinHooks
8400
- };
8401
- fileContent = JSON.stringify(merged, null, 2);
8732
+ fileContent = applySharedConfigPatch({
8733
+ fileKey: sharedConfigFileKey(paths),
8734
+ feature: "hooks",
8735
+ existingContent,
8736
+ patch: { hooks: devinHooks },
8737
+ filePath
8738
+ });
8402
8739
  } else {
8403
8740
  await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8404
8741
  fileContent = JSON.stringify(devinHooks, null, 2);
@@ -9210,18 +9547,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
9210
9547
  const paths = KiroHooks.getSettablePaths({ global });
9211
9548
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9212
9549
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9213
- let agentConfig;
9214
- try {
9215
- agentConfig = JSON.parse(existingContent);
9216
- } catch (error) {
9217
- throw new Error(`Failed to parse existing Kiro agent config at ${filePath}: ${formatError(error)}`, { cause: error });
9218
- }
9219
9550
  const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson(), this.getOverrideKey());
9220
- const merged = {
9221
- ...agentConfig,
9222
- hooks: kiroHooks
9223
- };
9224
- const fileContent = JSON.stringify(merged, null, 2);
9551
+ const fileContent = applySharedConfigPatch({
9552
+ fileKey: sharedConfigFileKey(paths),
9553
+ feature: "hooks",
9554
+ existingContent,
9555
+ patch: { hooks: kiroHooks },
9556
+ filePath
9557
+ });
9225
9558
  return new KiroHooks({
9226
9559
  outputRoot,
9227
9560
  relativeDirPath: paths.relativeDirPath,
@@ -9716,21 +10049,17 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
9716
10049
  const paths = QwencodeHooks.getSettablePaths({ global });
9717
10050
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9718
10051
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9719
- let settings;
9720
- try {
9721
- settings = JSON.parse(existingContent);
9722
- } catch (error) {
9723
- throw new Error(`Failed to parse existing Qwen Code settings at ${filePath}: ${formatError(error)}`, { cause: error });
9724
- }
9725
10052
  const config = rulesyncHooks.getJson();
9726
- const qwencodeHooks = canonicalToQwencodeHooks(config);
9727
- const merged = {
9728
- ...settings,
9729
- hooks: qwencodeHooks
9730
- };
10053
+ const patch = { hooks: canonicalToQwencodeHooks(config) };
9731
10054
  const disableAllHooks = config.qwencode?.disableAllHooks;
9732
- if (typeof disableAllHooks === "boolean") merged.disableAllHooks = disableAllHooks;
9733
- const fileContent = JSON.stringify(merged, null, 2);
10055
+ if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
10056
+ const fileContent = applySharedConfigPatch({
10057
+ fileKey: sharedConfigFileKey(paths),
10058
+ feature: "hooks",
10059
+ existingContent,
10060
+ patch,
10061
+ filePath
10062
+ });
9734
10063
  return new QwencodeHooks({
9735
10064
  outputRoot,
9736
10065
  relativeDirPath: paths.relativeDirPath,
@@ -9989,10 +10318,6 @@ function canonicalToVibeHooks(config, toolOverride) {
9989
10318
  }
9990
10319
  return { hooks };
9991
10320
  }
9992
- /**
9993
- * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
9994
- * into a canonical event → definition[] record.
9995
- */
9996
10321
  /** Convert one raw `[[hooks]]` entry to a canonical definition, or null to skip. */
9997
10322
  function vibeEntryToCanonicalDef(raw) {
9998
10323
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -10012,6 +10337,10 @@ function vibeEntryToCanonicalDef(raw) {
10012
10337
  def
10013
10338
  };
10014
10339
  }
10340
+ /**
10341
+ * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
10342
+ * into a canonical event → definition[] record.
10343
+ */
10015
10344
  function vibeHooksToCanonical(parsed) {
10016
10345
  const canonical = {};
10017
10346
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return canonical;
@@ -10040,15 +10369,22 @@ function parseVibeToml(fileContent) {
10040
10369
  */
10041
10370
  async function buildVibeConfigTomlContent({ outputRoot }) {
10042
10371
  const configPath = join(outputRoot, VIBE_DIR, VIBE_CONFIG_FILE_NAME);
10043
- const existingContent = await readFileContentOrNull(configPath) ?? smolToml.stringify({});
10044
- let config;
10372
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
10045
10373
  try {
10046
- config = parseVibeToml(existingContent);
10374
+ parseVibeToml(existingContent);
10047
10375
  } catch (error) {
10048
10376
  throw new Error(`Failed to parse existing Vibe config at ${configPath}: ${formatError(error)}`, { cause: error });
10049
10377
  }
10050
- config.enable_experimental_hooks = true;
10051
- return smolToml.stringify(config);
10378
+ return applySharedConfigPatch({
10379
+ fileKey: sharedConfigFileKey({
10380
+ relativeDirPath: VIBE_DIR,
10381
+ relativeFilePath: VIBE_CONFIG_FILE_NAME
10382
+ }),
10383
+ feature: "hooks",
10384
+ existingContent,
10385
+ patch: { enable_experimental_hooks: true },
10386
+ filePath: configPath
10387
+ });
10052
10388
  }
10053
10389
  /**
10054
10390
  * Represents the `.vibe/config.toml` file as a generated ToolFile so it goes
@@ -11548,17 +11884,18 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
11548
11884
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
11549
11885
  const filePath = join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
11550
11886
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
11551
- const existingJsonValue = JSON.parse(existingFileContent);
11552
- const mergedPatterns = uniq([...existingJsonValue.private_files ?? [], ...patterns].toSorted());
11553
- const jsonValue = {
11554
- ...existingJsonValue,
11555
- private_files: mergedPatterns
11556
- };
11887
+ const mergedPatterns = uniq([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
11557
11888
  return new ZedIgnore({
11558
11889
  outputRoot,
11559
11890
  relativeDirPath: this.getSettablePaths().relativeDirPath,
11560
11891
  relativeFilePath: this.getSettablePaths().relativeFilePath,
11561
- fileContent: JSON.stringify(jsonValue, null, 2),
11892
+ fileContent: applySharedConfigPatch({
11893
+ fileKey: sharedConfigFileKey(this.getSettablePaths()),
11894
+ feature: "ignore",
11895
+ existingContent: existingFileContent,
11896
+ patch: { private_files: mergedPatterns },
11897
+ filePath
11898
+ }),
11562
11899
  validate: true
11563
11900
  });
11564
11901
  }
@@ -12015,17 +12352,17 @@ var AmpMcp = class AmpMcp extends ToolMcp {
12015
12352
  const basePaths = this.getSettablePaths({ global });
12016
12353
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
12017
12354
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
12018
- const json = fileContent ? parseAmpSettingsJsonc(fileContent) : { [AMP_MCP_SERVERS_KEY]: {} };
12019
- const filteredMcpServers = filterMcpServers(rulesyncMcp.getMcpServers());
12020
- const newJson = {
12021
- ...json,
12022
- [AMP_MCP_SERVERS_KEY]: filteredMcpServers
12023
- };
12024
12355
  return new AmpMcp({
12025
12356
  outputRoot,
12026
12357
  relativeDirPath: basePaths.relativeDirPath,
12027
12358
  relativeFilePath,
12028
- fileContent: JSON.stringify(newJson, null, 2),
12359
+ fileContent: applySharedConfigPatch({
12360
+ fileKey: sharedConfigFileKey(basePaths),
12361
+ feature: "mcp",
12362
+ existingContent: fileContent ?? "",
12363
+ patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
12364
+ filePath: join(jsonDir, relativeFilePath)
12365
+ }),
12029
12366
  validate,
12030
12367
  global
12031
12368
  });
@@ -12312,15 +12649,19 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12312
12649
  }
12313
12650
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12314
12651
  const paths = this.getSettablePaths({ global });
12315
- const merged = {
12316
- ...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
12317
- mcpServers: rulesyncMcp.getMcpServers()
12318
- };
12652
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
12653
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
12319
12654
  return new AugmentcodeMcp({
12320
12655
  outputRoot,
12321
12656
  relativeDirPath: paths.relativeDirPath,
12322
12657
  relativeFilePath: paths.relativeFilePath,
12323
- fileContent: JSON.stringify(merged, null, 2),
12658
+ fileContent: applySharedConfigPatch({
12659
+ fileKey: sharedConfigFileKey(paths),
12660
+ feature: "mcp",
12661
+ existingContent,
12662
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
12663
+ filePath
12664
+ }),
12324
12665
  validate,
12325
12666
  global
12326
12667
  });
@@ -12714,8 +13055,9 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12714
13055
  }
12715
13056
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12716
13057
  const paths = this.getSettablePaths({ global });
12717
- const configTomlFileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smolToml.stringify({}));
12718
- const configToml = smolToml.parse(configTomlFileContent);
13058
+ const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13059
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13060
+ const configToml = smolToml.parse(configTomlFileContent || smolToml.stringify({}));
12719
13061
  const strippedMcpServers = rulesyncMcp.getMcpServers();
12720
13062
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
12721
13063
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
@@ -12728,7 +13070,7 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12728
13070
  const filteredMcpServers = this.removeEmptyEntries(converted);
12729
13071
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
12730
13072
  const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
12731
- configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
13073
+ const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
12732
13074
  const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12733
13075
  const serverRecord = serverConfig;
12734
13076
  if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
@@ -12741,7 +13083,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12741
13083
  outputRoot,
12742
13084
  relativeDirPath: paths.relativeDirPath,
12743
13085
  relativeFilePath: paths.relativeFilePath,
12744
- fileContent: smolToml.stringify(configToml),
13086
+ fileContent: applySharedConfigPatch({
13087
+ fileKey: sharedConfigFileKey(paths),
13088
+ feature: "mcp",
13089
+ existingContent: configTomlFileContent,
13090
+ patch: { mcp_servers: mergedMcpServers },
13091
+ filePath: configTomlFilePath
13092
+ }),
12745
13093
  validate
12746
13094
  });
12747
13095
  }
@@ -13308,16 +13656,19 @@ var DevinMcp = class DevinMcp extends ToolMcp {
13308
13656
  }
13309
13657
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13310
13658
  const paths = this.getSettablePaths({ global });
13311
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
13312
- const devinConfig = {
13313
- ...this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath),
13314
- mcpServers: rulesyncMcp.getMcpServers()
13315
- };
13659
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13660
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
13316
13661
  return new DevinMcp({
13317
13662
  outputRoot,
13318
13663
  relativeDirPath: paths.relativeDirPath,
13319
13664
  relativeFilePath: paths.relativeFilePath,
13320
- fileContent: JSON.stringify(devinConfig, null, 2),
13665
+ fileContent: applySharedConfigPatch({
13666
+ fileKey: sharedConfigFileKey(paths),
13667
+ feature: "mcp",
13668
+ existingContent,
13669
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
13670
+ filePath
13671
+ }),
13321
13672
  validate,
13322
13673
  global
13323
13674
  });
@@ -13804,17 +14155,22 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
13804
14155
  }
13805
14156
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13806
14157
  const paths = this.getSettablePaths({ global });
13807
- const configTomlFileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smolToml.stringify({}));
13808
- const configToml = smolToml.parse(configTomlFileContent);
14158
+ const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
14159
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13809
14160
  const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
13810
14161
  const filteredMcpServers = this.removeEmptyEntries(converted);
13811
14162
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
13812
- configToml["mcp_servers"] = filteredMcpServers;
13813
14163
  return new GrokcliMcp({
13814
14164
  outputRoot,
13815
14165
  relativeDirPath: paths.relativeDirPath,
13816
14166
  relativeFilePath: paths.relativeFilePath,
13817
- fileContent: smolToml.stringify(configToml),
14167
+ fileContent: applySharedConfigPatch({
14168
+ fileKey: sharedConfigFileKey(paths),
14169
+ feature: "mcp",
14170
+ existingContent: configTomlFileContent,
14171
+ patch: { mcp_servers: filteredMcpServers },
14172
+ filePath: configTomlFilePath
14173
+ }),
13818
14174
  validate
13819
14175
  });
13820
14176
  }
@@ -14347,20 +14703,21 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14347
14703
  fileContent = await readFileContentOrNull(jsonPath);
14348
14704
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
14349
14705
  }
14350
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14351
- const json = parse(fileContent);
14352
14706
  const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(rulesyncMcp.getMcpServers());
14353
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14354
- const newJson = {
14355
- ...jsonWithoutTools,
14356
- mcp: convertedMcp,
14357
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14358
- };
14359
14707
  return new KiloMcp({
14360
14708
  outputRoot,
14361
14709
  relativeDirPath: basePaths.relativeDirPath,
14362
14710
  relativeFilePath,
14363
- fileContent: JSON.stringify(newJson, null, 2),
14711
+ fileContent: applySharedConfigPatch({
14712
+ fileKey: sharedConfigFileKey(basePaths),
14713
+ feature: "mcp",
14714
+ existingContent: fileContent ?? "",
14715
+ patch: {
14716
+ mcp: convertedMcp,
14717
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
14718
+ },
14719
+ filePath: join(jsonDir, relativeFilePath)
14720
+ }),
14364
14721
  validate
14365
14722
  });
14366
14723
  }
@@ -14389,15 +14746,17 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14389
14746
  const json = fileContent ? parse(fileContent) : {};
14390
14747
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14391
14748
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14392
- const newJson = {
14393
- ...json,
14394
- instructions: mergedInstructions
14395
- };
14396
14749
  return new KiloMcp({
14397
14750
  outputRoot,
14398
14751
  relativeDirPath: basePaths.relativeDirPath,
14399
14752
  relativeFilePath,
14400
- fileContent: JSON.stringify(newJson, null, 2),
14753
+ fileContent: applySharedConfigPatch({
14754
+ fileKey: sharedConfigFileKey(basePaths),
14755
+ feature: "rules",
14756
+ existingContent: fileContent ?? "",
14757
+ patch: { instructions: mergedInstructions },
14758
+ filePath: join(jsonDir, relativeFilePath)
14759
+ }),
14401
14760
  validate
14402
14761
  });
14403
14762
  }
@@ -14669,23 +15028,24 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14669
15028
  fileContent = await readFileContentOrNull(jsonPath);
14670
15029
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
14671
15030
  }
14672
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14673
- const json = parse(fileContent);
14674
15031
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
14675
15032
  mcpServers: rulesyncMcp.getMcpServers(),
14676
15033
  replacement: "{env:$1}"
14677
15034
  }));
14678
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14679
- const newJson = {
14680
- ...jsonWithoutTools,
14681
- mcp: convertedMcp,
14682
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14683
- };
14684
15035
  return new OpencodeMcp({
14685
15036
  outputRoot,
14686
15037
  relativeDirPath: basePaths.relativeDirPath,
14687
15038
  relativeFilePath,
14688
- fileContent: JSON.stringify(newJson, null, 2),
15039
+ fileContent: applySharedConfigPatch({
15040
+ fileKey: sharedConfigFileKey(basePaths),
15041
+ feature: "mcp",
15042
+ existingContent: fileContent ?? "",
15043
+ patch: {
15044
+ mcp: convertedMcp,
15045
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
15046
+ },
15047
+ filePath: join(jsonDir, relativeFilePath)
15048
+ }),
14689
15049
  validate
14690
15050
  });
14691
15051
  }
@@ -14720,15 +15080,17 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14720
15080
  const json = fileContent ? parse(fileContent) : {};
14721
15081
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14722
15082
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14723
- const newJson = {
14724
- ...json,
14725
- instructions: mergedInstructions
14726
- };
14727
15083
  return new OpencodeMcp({
14728
15084
  outputRoot,
14729
15085
  relativeDirPath: basePaths.relativeDirPath,
14730
15086
  relativeFilePath,
14731
- fileContent: JSON.stringify(newJson, null, 2),
15087
+ fileContent: applySharedConfigPatch({
15088
+ fileKey: sharedConfigFileKey(basePaths),
15089
+ feature: "rules",
15090
+ existingContent: fileContent ?? "",
15091
+ patch: { instructions: mergedInstructions },
15092
+ filePath: join(jsonDir, relativeFilePath)
15093
+ }),
14732
15094
  validate
14733
15095
  });
14734
15096
  }
@@ -14838,16 +15200,19 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
14838
15200
  }
14839
15201
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14840
15202
  const paths = this.getSettablePaths({ global });
14841
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
14842
- const newJson = {
14843
- ...JSON.parse(fileContent),
14844
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers())
14845
- };
15203
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15204
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
14846
15205
  return new QwencodeMcp({
14847
15206
  outputRoot,
14848
15207
  relativeDirPath: paths.relativeDirPath,
14849
15208
  relativeFilePath: paths.relativeFilePath,
14850
- fileContent: JSON.stringify(newJson, null, 2),
15209
+ fileContent: applySharedConfigPatch({
15210
+ fileKey: sharedConfigFileKey(paths),
15211
+ feature: "mcp",
15212
+ existingContent,
15213
+ patch: { mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()) },
15214
+ filePath
15215
+ }),
14851
15216
  validate
14852
15217
  });
14853
15218
  }
@@ -14932,13 +15297,20 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
14932
15297
  }
14933
15298
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14934
15299
  const paths = this.getSettablePaths({ global });
14935
- const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14936
- config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
15300
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15301
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15302
+ const plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
14937
15303
  return new ReasonixMcp({
14938
15304
  outputRoot,
14939
15305
  relativeDirPath: paths.relativeDirPath,
14940
15306
  relativeFilePath: paths.relativeFilePath,
14941
- fileContent: smolToml.stringify(config),
15307
+ fileContent: applySharedConfigPatch({
15308
+ fileKey: sharedConfigFileKey(paths),
15309
+ feature: "mcp",
15310
+ existingContent,
15311
+ patch: { plugins },
15312
+ filePath
15313
+ }),
14942
15314
  validate,
14943
15315
  global
14944
15316
  });
@@ -15413,13 +15785,20 @@ var VibeMcp = class VibeMcp extends ToolMcp {
15413
15785
  }
15414
15786
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15415
15787
  const paths = this.getSettablePaths({ global });
15416
- const config = parseVibeConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
15417
- config.mcp_servers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
15788
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15789
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15790
+ const mcpServers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
15418
15791
  return new VibeMcp({
15419
15792
  outputRoot,
15420
15793
  relativeDirPath: paths.relativeDirPath,
15421
15794
  relativeFilePath: paths.relativeFilePath,
15422
- fileContent: smolToml.stringify(config),
15795
+ fileContent: applySharedConfigPatch({
15796
+ fileKey: sharedConfigFileKey(paths),
15797
+ feature: "mcp",
15798
+ existingContent,
15799
+ patch: { mcp_servers: mcpServers },
15800
+ filePath
15801
+ }),
15423
15802
  validate,
15424
15803
  global
15425
15804
  });
@@ -15650,16 +16029,19 @@ var ZedMcp = class ZedMcp extends ToolMcp {
15650
16029
  }
15651
16030
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15652
16031
  const paths = this.getSettablePaths({ global });
15653
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "{}");
15654
- const newJson = {
15655
- ...JSON.parse(fileContent),
15656
- context_servers: rulesyncMcp.getMcpServers()
15657
- };
16032
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16033
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
15658
16034
  return new ZedMcp({
15659
16035
  outputRoot,
15660
16036
  relativeDirPath: paths.relativeDirPath,
15661
16037
  relativeFilePath: paths.relativeFilePath,
15662
- fileContent: JSON.stringify(newJson, null, 2),
16038
+ fileContent: applySharedConfigPatch({
16039
+ fileKey: sharedConfigFileKey(paths),
16040
+ feature: "mcp",
16041
+ existingContent,
16042
+ patch: { context_servers: rulesyncMcp.getMcpServers() },
16043
+ filePath
16044
+ }),
15663
16045
  validate
15664
16046
  });
15665
16047
  }
@@ -16828,25 +17210,29 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16828
17210
  const override = config.amp;
16829
17211
  const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16830
17212
  const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16831
- const newJson = {
16832
- ...json,
16833
- [AMP_TOOLS_DISABLE_KEY]: disable
16834
- };
16835
17213
  const mergedPermissions = mergeAmpPermissions([
16836
17214
  ...permissions,
16837
17215
  ...authoredExtras,
16838
17216
  ...preservedDelegates
16839
17217
  ]);
16840
- if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16841
- else delete newJson[AMP_PERMISSIONS_KEY];
16842
- if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16843
- if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16844
- if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
17218
+ const patch = {
17219
+ [AMP_TOOLS_DISABLE_KEY]: disable,
17220
+ [AMP_PERMISSIONS_KEY]: mergedPermissions.length > 0 ? mergedPermissions : void 0
17221
+ };
17222
+ if (override?.guardedFiles?.allowlist !== void 0) patch[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
17223
+ if (override?.dangerouslyAllowAll !== void 0) patch[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
17224
+ if (override?.mcpPermissions !== void 0) patch[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16845
17225
  return new AmpPermissions({
16846
17226
  outputRoot,
16847
17227
  relativeDirPath: basePaths.relativeDirPath,
16848
17228
  relativeFilePath,
16849
- fileContent: JSON.stringify(newJson, null, 2),
17229
+ fileContent: applySharedConfigPatch({
17230
+ fileKey: sharedConfigFileKey(basePaths),
17231
+ feature: "permissions",
17232
+ existingContent: fileContent ?? "",
17233
+ patch,
17234
+ filePath: join(jsonDir, relativeFilePath)
17235
+ }),
16850
17236
  validate: true
16851
17237
  });
16852
17238
  }
@@ -17788,11 +18174,13 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
17788
18174
  ...preservedBasicEntries,
17789
18175
  ...authoredBasics
17790
18176
  ]);
17791
- const merged = {
17792
- ...settings,
17793
- toolPermissions: [...specialEntries, ...sortedBasic]
17794
- };
17795
- const fileContent = JSON.stringify(merged, null, 2);
18177
+ const fileContent = applySharedConfigPatch({
18178
+ fileKey: sharedConfigFileKey(paths),
18179
+ feature: "permissions",
18180
+ existingContent,
18181
+ patch: { toolPermissions: [...specialEntries, ...sortedBasic] },
18182
+ filePath
18183
+ });
17796
18184
  return new AugmentcodePermissions({
17797
18185
  outputRoot,
17798
18186
  relativeDirPath: paths.relativeDirPath,
@@ -18384,13 +18772,6 @@ const WORKSPACE_WIDE_WRITE_PATTERNS = /* @__PURE__ */ new Set([
18384
18772
  ]);
18385
18773
  const CODEX_MINIMAL_KEY = ":minimal";
18386
18774
  const GLOBAL_WILDCARD_DOMAIN = "*";
18387
- const CODEXCLI_OVERRIDE_KEYS = [
18388
- "approval_policy",
18389
- "sandbox_mode",
18390
- "sandbox_workspace_write",
18391
- "apps",
18392
- "approvals_reviewer"
18393
- ];
18394
18775
  var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18395
18776
  static getSettablePaths(_options = {}) {
18396
18777
  return {
@@ -18414,26 +18795,27 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18414
18795
  }
18415
18796
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
18416
18797
  const paths = this.getSettablePaths({ global });
18417
- const existingContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
18418
- const parsed = toMutableTable(smolToml.parse(existingContent));
18798
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18799
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
18800
+ const existing = toMutableTable(smolToml.parse(existingContent || smolToml.stringify({})));
18419
18801
  const newProfile = convertRulesyncToCodexProfile({
18420
18802
  config: rulesyncPermissions.getJson(),
18421
18803
  logger
18422
18804
  });
18423
- const permissionsTable = toMutableTable(parsed.permissions);
18805
+ const permissionsTable = toMutableTable(existing.permissions);
18424
18806
  const { profile: existingProfile, domainsHadUnknown: existingDomainsHadUnknown } = toCodexProfile(permissionsTable[RULESYNC_PROFILE_NAME]);
18425
- if (existingProfile?.extends !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
18426
- if (existingProfile?.network?.unix_sockets !== void 0) logger?.warn(`Preserving existing "network.unix_sockets" from config. Review these entries manually as they may grant broad system access.`);
18427
- 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.`);
18428
- if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
18807
+ warnAboutPreservedProfileState({
18808
+ existingProfile,
18809
+ newProfile,
18810
+ existingDomainsHadUnknown,
18811
+ logger
18812
+ });
18429
18813
  permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
18430
18814
  newProfile,
18431
18815
  existingProfile
18432
18816
  });
18433
- parsed.permissions = permissionsTable;
18434
- parsed.default_permissions = RULESYNC_PROFILE_NAME;
18435
- applyCodexcliOverride({
18436
- parsed,
18817
+ const overridePatch = computeCodexcliOverridePatch({
18818
+ existing,
18437
18819
  override: rulesyncPermissions.getJson().codexcli,
18438
18820
  logger
18439
18821
  });
@@ -18441,7 +18823,17 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18441
18823
  outputRoot,
18442
18824
  relativeDirPath: paths.relativeDirPath,
18443
18825
  relativeFilePath: paths.relativeFilePath,
18444
- fileContent: smolToml.stringify(parsed),
18826
+ fileContent: applySharedConfigPatch({
18827
+ fileKey: sharedConfigFileKey(paths),
18828
+ feature: "permissions",
18829
+ existingContent,
18830
+ patch: {
18831
+ permissions: permissionsTable,
18832
+ default_permissions: RULESYNC_PROFILE_NAME,
18833
+ ...overridePatch
18834
+ },
18835
+ filePath
18836
+ }),
18445
18837
  validate
18446
18838
  });
18447
18839
  }
@@ -18611,6 +19003,12 @@ function toCodexProfile(value) {
18611
19003
  domainsHadUnknown
18612
19004
  };
18613
19005
  }
19006
+ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingDomainsHadUnknown, logger }) {
19007
+ if (existingProfile?.extends !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
19008
+ if (existingProfile?.network?.unix_sockets !== void 0) logger?.warn(`Preserving existing "network.unix_sockets" from config. Review these entries manually as they may grant broad system access.`);
19009
+ 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.`);
19010
+ if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19011
+ }
18614
19012
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
18615
19013
  if (!existingProfile) return newProfile;
18616
19014
  const mergedNetwork = { ...newProfile.network };
@@ -18661,8 +19059,9 @@ function toMutableTable(value) {
18661
19059
  function isPlainObject(value) {
18662
19060
  return value !== null && typeof value === "object" && !Array.isArray(value);
18663
19061
  }
18664
- function applyCodexcliOverride({ parsed, override, logger }) {
18665
- if (!override) return;
19062
+ function computeCodexcliOverridePatch({ existing, override, logger }) {
19063
+ const patch = {};
19064
+ if (!override) return patch;
18666
19065
  const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
18667
19066
  for (const [key, value] of Object.entries(override)) {
18668
19067
  if (!allowed.has(key)) {
@@ -18670,12 +19069,13 @@ function applyCodexcliOverride({ parsed, override, logger }) {
18670
19069
  continue;
18671
19070
  }
18672
19071
  if (value === void 0) continue;
18673
- const existing = parsed[key];
18674
- parsed[key] = isPlainObject(existing) && isPlainObject(value) ? {
18675
- ...existing,
19072
+ const existingValue = existing[key];
19073
+ patch[key] = isPlainObject(existingValue) && isPlainObject(value) ? {
19074
+ ...existingValue,
18676
19075
  ...value
18677
19076
  } : value;
18678
19077
  }
19078
+ return patch;
18679
19079
  }
18680
19080
  function extractCodexcliOverride(table) {
18681
19081
  const override = {};
@@ -19236,15 +19636,17 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
19236
19636
  else delete mergedPermissions.ask;
19237
19637
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19238
19638
  else delete mergedPermissions.deny;
19239
- const merged = {
19240
- ...settings,
19241
- permissions: mergedPermissions
19242
- };
19243
19639
  return new DevinPermissions({
19244
19640
  outputRoot,
19245
19641
  relativeDirPath: paths.relativeDirPath,
19246
19642
  relativeFilePath: paths.relativeFilePath,
19247
- fileContent: JSON.stringify(merged, null, 2),
19643
+ fileContent: applySharedConfigPatch({
19644
+ fileKey: sharedConfigFileKey(paths),
19645
+ feature: "permissions",
19646
+ existingContent,
19647
+ patch: { permissions: mergedPermissions },
19648
+ filePath
19649
+ }),
19248
19650
  validate
19249
19651
  });
19250
19652
  }
@@ -19853,23 +20255,31 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19853
20255
  const config = rulesyncPermissions.getJson();
19854
20256
  const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19855
20257
  const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19856
- parsed[GROKCLI_PERMISSION_KEY] = {
20258
+ const permission = {
19857
20259
  ...existingPermission,
19858
20260
  allow: buckets.allow,
19859
20261
  deny: buckets.deny,
19860
20262
  ask: buckets.ask
19861
20263
  };
19862
20264
  const mode = deriveGrokPermissionMode(config);
19863
- const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
19864
- parsed[GROKCLI_UI_KEY] = {
19865
- ...existingUi,
20265
+ const ui = {
20266
+ ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
19866
20267
  [GROKCLI_PERMISSION_MODE_KEY]: mode
19867
20268
  };
19868
20269
  return new GrokcliPermissions({
19869
20270
  outputRoot,
19870
20271
  relativeDirPath: paths.relativeDirPath,
19871
20272
  relativeFilePath: paths.relativeFilePath,
19872
- fileContent: smolToml.stringify(parsed),
20273
+ fileContent: applySharedConfigPatch({
20274
+ fileKey: sharedConfigFileKey(paths),
20275
+ feature: "permissions",
20276
+ existingContent,
20277
+ patch: {
20278
+ [GROKCLI_PERMISSION_KEY]: permission,
20279
+ [GROKCLI_UI_KEY]: ui
20280
+ },
20281
+ filePath
20282
+ }),
19873
20283
  validate: true,
19874
20284
  global: true
19875
20285
  });
@@ -20572,7 +20982,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
20572
20982
  outputRoot,
20573
20983
  relativeDirPath: paths.relativeDirPath,
20574
20984
  relativeFilePath: paths.relativeFilePath,
20575
- fileContent: JSON.stringify(next, null, 2),
20985
+ fileContent: applySharedConfigPatch({
20986
+ fileKey: sharedConfigFileKey(paths),
20987
+ feature: "permissions",
20988
+ existingContent,
20989
+ patch: {
20990
+ allowedTools: next.allowedTools,
20991
+ toolsSettings: next.toolsSettings
20992
+ },
20993
+ filePath
20994
+ }),
20576
20995
  validate
20577
20996
  });
20578
20997
  }
@@ -20869,21 +21288,22 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
20869
21288
  fileContent = await readFileContentOrNull(jsonPath);
20870
21289
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
20871
21290
  }
20872
- const parsed = parse(fileContent ?? "{}");
20873
21291
  const rulesyncJson = rulesyncPermissions.getJson();
20874
21292
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
20875
- const nextJson = {
20876
- ...parsed,
20877
- permission: {
20878
- ...rulesyncJson.permission,
20879
- ...overridePermission
20880
- }
20881
- };
20882
21293
  return new OpencodePermissions({
20883
21294
  outputRoot,
20884
21295
  relativeDirPath: basePaths.relativeDirPath,
20885
21296
  relativeFilePath,
20886
- fileContent: JSON.stringify(nextJson, null, 2),
21297
+ fileContent: applySharedConfigPatch({
21298
+ fileKey: sharedConfigFileKey(basePaths),
21299
+ feature: "permissions",
21300
+ existingContent: fileContent ?? "",
21301
+ patch: { permission: {
21302
+ ...rulesyncJson.permission,
21303
+ ...overridePermission
21304
+ } },
21305
+ filePath: join(jsonDir, relativeFilePath)
21306
+ }),
20887
21307
  validate: true
20888
21308
  });
20889
21309
  }
@@ -21092,20 +21512,23 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21092
21512
  else delete mergedPermissions.ask;
21093
21513
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21094
21514
  else delete mergedPermissions.deny;
21095
- const merged = {
21096
- ...settings,
21097
- permissions: mergedPermissions
21098
- };
21515
+ const patch = { permissions: mergedPermissions };
21099
21516
  const override = config.qwencode;
21100
- if (override?.tools !== void 0) merged.tools = {
21517
+ if (override?.tools !== void 0) patch.tools = {
21101
21518
  ...asPlainRecord(settings.tools),
21102
21519
  ...asPlainRecord(override.tools)
21103
21520
  };
21104
- if (override?.security !== void 0) merged.security = {
21521
+ if (override?.security !== void 0) patch.security = {
21105
21522
  ...asPlainRecord(settings.security),
21106
21523
  ...asPlainRecord(override.security)
21107
21524
  };
21108
- const fileContent = JSON.stringify(merged, null, 2);
21525
+ const fileContent = applySharedConfigPatch({
21526
+ fileKey: sharedConfigFileKey(paths),
21527
+ feature: "permissions",
21528
+ existingContent,
21529
+ patch,
21530
+ filePath
21531
+ });
21109
21532
  return new QwencodePermissions({
21110
21533
  outputRoot,
21111
21534
  relativeDirPath: paths.relativeDirPath,
@@ -21340,7 +21763,9 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21340
21763
  }
21341
21764
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21342
21765
  const paths = this.getSettablePaths({ global });
21343
- const parsed = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
21766
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21767
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21768
+ const parsed = parseReasonixConfig(existingContent);
21344
21769
  const config = rulesyncPermissions.getJson();
21345
21770
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
21346
21771
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
@@ -21365,25 +21790,27 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21365
21790
  else delete mergedPermissions.ask;
21366
21791
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21367
21792
  else delete mergedPermissions.deny;
21368
- const merged = {
21369
- ...parsed,
21370
- permissions: mergedPermissions
21371
- };
21793
+ const patch = { permissions: mergedPermissions };
21372
21794
  const override = config.reasonix;
21373
- if (override?.sandbox !== void 0) merged.sandbox = {
21795
+ if (override?.sandbox !== void 0) patch.sandbox = {
21374
21796
  ...asReasonixRecord(parsed.sandbox),
21375
21797
  ...asReasonixRecord(override.sandbox)
21376
21798
  };
21377
- if (override?.agent !== void 0) merged.agent = {
21799
+ if (override?.agent !== void 0) patch.agent = {
21378
21800
  ...asReasonixRecord(parsed.agent),
21379
21801
  ...asReasonixRecord(override.agent)
21380
21802
  };
21381
- const fileContent = smolToml.stringify(merged);
21382
21803
  return new ReasonixPermissions({
21383
21804
  outputRoot,
21384
21805
  relativeDirPath: paths.relativeDirPath,
21385
21806
  relativeFilePath: paths.relativeFilePath,
21386
- fileContent,
21807
+ fileContent: applySharedConfigPatch({
21808
+ fileKey: sharedConfigFileKey(paths),
21809
+ feature: "permissions",
21810
+ existingContent,
21811
+ patch,
21812
+ filePath
21813
+ }),
21387
21814
  validate
21388
21815
  });
21389
21816
  }
@@ -21954,7 +22381,9 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
21954
22381
  }
21955
22382
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21956
22383
  const paths = this.getSettablePaths({ global });
21957
- const config = parseVibeConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
22384
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
22385
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
22386
+ const config = parseVibeConfig(existingContent);
21958
22387
  const permission = rulesyncPermissions.getJson().permission;
21959
22388
  const tools = toVibeToolsRecord(config.tools);
21960
22389
  const enabledTools = new Set(toStringArray(config.enabled_tools));
@@ -21999,16 +22428,21 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
21999
22428
  tools[vibeToolName] = nextTool;
22000
22429
  }
22001
22430
  applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
22002
- config.tools = tools;
22003
- if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
22004
- else delete config.enabled_tools;
22005
- if (disabledTools.size > 0) config.disabled_tools = [...disabledTools].toSorted();
22006
- else delete config.disabled_tools;
22007
22431
  return new VibePermissions({
22008
22432
  outputRoot,
22009
22433
  relativeDirPath: paths.relativeDirPath,
22010
22434
  relativeFilePath: paths.relativeFilePath,
22011
- fileContent: smolToml.stringify(config),
22435
+ fileContent: applySharedConfigPatch({
22436
+ fileKey: sharedConfigFileKey(paths),
22437
+ feature: "permissions",
22438
+ existingContent,
22439
+ patch: {
22440
+ tools,
22441
+ enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
22442
+ disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
22443
+ },
22444
+ filePath
22445
+ }),
22012
22446
  validate,
22013
22447
  global
22014
22448
  });
@@ -22471,24 +22905,26 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
22471
22905
  }
22472
22906
  const managedToolNames = new Set(Object.keys(managedTools));
22473
22907
  const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
22474
- const mergedSettings = {
22475
- ...settings,
22476
- agent: {
22477
- ...agent,
22478
- tool_permissions: {
22479
- ...toolPermissions,
22480
- tools: {
22481
- ...preservedTools,
22482
- ...managedTools
22483
- }
22484
- }
22485
- }
22486
- };
22487
22908
  return new ZedPermissions({
22488
22909
  outputRoot,
22489
22910
  relativeDirPath: paths.relativeDirPath,
22490
22911
  relativeFilePath: paths.relativeFilePath,
22491
- fileContent: JSON.stringify(mergedSettings, null, 2),
22912
+ fileContent: applySharedConfigPatch({
22913
+ fileKey: sharedConfigFileKey(paths),
22914
+ feature: "permissions",
22915
+ existingContent,
22916
+ patch: { agent: {
22917
+ ...agent,
22918
+ tool_permissions: {
22919
+ ...toolPermissions,
22920
+ tools: {
22921
+ ...preservedTools,
22922
+ ...managedTools
22923
+ }
22924
+ }
22925
+ } },
22926
+ filePath
22927
+ }),
22492
22928
  validate: true
22493
22929
  });
22494
22930
  }
@@ -38178,8 +38614,13 @@ const collectFactoryPaths = (factory) => [...settablePathsForScope({
38178
38614
  * Derive, from the processor registry, every on-disk file that two or more
38179
38615
  * features read-modify-write. Source of truth the generation step graph's
38180
38616
  * `writesSharedFile` declarations must match.
38617
+ *
38618
+ * `minWriters: 1` widens the result to single-feature files as well — used to
38619
+ * validate `SHARED_CONFIG_OWNERSHIP` declarations for files that route through
38620
+ * the gateway without being cross-feature shared (e.g. a global-scope twin
38621
+ * only one feature writes).
38181
38622
  */
38182
- const deriveSharedFileWriters = () => {
38623
+ const deriveSharedFileWriters = ({ minWriters = 2 } = {}) => {
38183
38624
  const byKey = /* @__PURE__ */ new Map();
38184
38625
  const pathByKey = /* @__PURE__ */ new Map();
38185
38626
  for (const entry of PROCESSOR_REGISTRY) {
@@ -38205,7 +38646,7 @@ const deriveSharedFileWriters = () => {
38205
38646
  }
38206
38647
  const writers = [];
38207
38648
  for (const [key, features] of byKey) {
38208
- if (features.size < 2) continue;
38649
+ if (features.size < minWriters) continue;
38209
38650
  const path = pathByKey.get(key);
38210
38651
  const toolsByFeature = /* @__PURE__ */ new Map();
38211
38652
  for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
@@ -39150,4 +39591,4 @@ async function importPermissionsCore(params) {
39150
39591
  //#endregion
39151
39592
  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 };
39152
39593
 
39153
- //# sourceMappingURL=import-A1yGuzwv.js.map
39594
+ //# sourceMappingURL=import-DC9T1JlQ.js.map