rulesync 16.14.0 → 16.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +206 -70
- package/dist/cli/index.js +204 -68
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-Ds0QeVp6.js → import-DimKwtQ6.js} +2115 -344
- package/dist/import-DimKwtQ6.js.map +1 -0
- package/dist/{import-DsiSzsJH.cjs → import-NiPCjH6E.cjs} +2145 -356
- package/dist/index.cjs +4 -3
- package/dist/index.d.cts +26 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +26 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
- package/dist/import-Ds0QeVp6.js.map +0 -1
|
@@ -113,11 +113,18 @@ const { join: join$1 } = posix;
|
|
|
113
113
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
114
114
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
115
115
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
const
|
|
116
|
+
const RULES_FEATURE_SUBDIR = "rules";
|
|
117
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$1(RULES_FEATURE_SUBDIR, ".curated");
|
|
118
|
+
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
119
|
+
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
120
|
+
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
121
|
+
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
122
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$1(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
123
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
124
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
125
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
126
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
127
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
121
128
|
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
122
129
|
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
123
130
|
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
@@ -128,8 +135,8 @@ const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
|
128
135
|
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
129
136
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
130
137
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
131
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH,
|
|
132
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$1(
|
|
138
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
139
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
133
140
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
134
141
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
135
142
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -1117,6 +1124,7 @@ const ConfigParamsSchema = z.object({
|
|
|
1117
1124
|
dryRun: optional(z.boolean()),
|
|
1118
1125
|
check: optional(z.boolean()),
|
|
1119
1126
|
inputRoot: optional(z.string()),
|
|
1127
|
+
inputRoots: optional(z.array(z.string()).check(minLength(1, "inputRoots must be non-empty"))),
|
|
1120
1128
|
sources: optional(z.array(SourceEntrySchema))
|
|
1121
1129
|
});
|
|
1122
1130
|
z.partial(ConfigParamsSchema);
|
|
@@ -1129,14 +1137,39 @@ z.required(ConfigParamsSchema);
|
|
|
1129
1137
|
* Normalizes the configuration file location to an absolute path.
|
|
1130
1138
|
*
|
|
1131
1139
|
* `ConfigResolver` always supplies the path it actually loaded; the fallback
|
|
1132
|
-
* only covers direct programmatic construction
|
|
1133
|
-
*
|
|
1140
|
+
* only covers direct programmatic construction. `anchorDir` is the directory
|
|
1141
|
+
* the config file lives next to — for the default `.rulesync/` layout this
|
|
1142
|
+
* is the parent of the primary source tree.
|
|
1134
1143
|
*/
|
|
1135
|
-
function normalizeConfigFilePath({ configFilePath,
|
|
1136
|
-
if (configFilePath === void 0) return join(
|
|
1144
|
+
function normalizeConfigFilePath({ configFilePath, anchorDir }) {
|
|
1145
|
+
if (configFilePath === void 0) return join(anchorDir, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
|
|
1137
1146
|
return isAbsolute(configFilePath) ? configFilePath : resolve(configFilePath);
|
|
1138
1147
|
}
|
|
1139
1148
|
/**
|
|
1149
|
+
* Resolves any accepted input-root shape (`inputRoot`, `inputRoots`, or
|
|
1150
|
+
* neither) to the canonical non-empty tuple of absolute paths that
|
|
1151
|
+
* `Config` stores. Relative entries are resolved against the current
|
|
1152
|
+
* working directory at call time.
|
|
1153
|
+
*
|
|
1154
|
+
* Semantics (post-refactor):
|
|
1155
|
+
* - Each entry in `inputRoots` is a rulesync **source tree** (the directory
|
|
1156
|
+
* that directly holds `rules/`, `skills/`, `mcp.jsonc`, etc.). No implicit
|
|
1157
|
+
* `.rulesync/` join is applied.
|
|
1158
|
+
* - The legacy singular `inputRoot` is a shorthand for "parent of the
|
|
1159
|
+
* default `.rulesync/` source tree", and is expanded to
|
|
1160
|
+
* `[join(inputRoot, ".rulesync")]` before hitting any consumer. This is
|
|
1161
|
+
* the ONLY place `.rulesync` is appended by convention.
|
|
1162
|
+
* - The "nothing configured" default expands to `[join(cwd, ".rulesync")]`
|
|
1163
|
+
* so existing projects with a single `.rulesync/` tree keep working
|
|
1164
|
+
* unchanged.
|
|
1165
|
+
*
|
|
1166
|
+
* Callers must have already run `assertInputRootFieldsExclusive`.
|
|
1167
|
+
*/
|
|
1168
|
+
function normalizeInputRoots({ inputRoot, inputRoots }) {
|
|
1169
|
+
const resolved = (inputRoots !== void 0 && inputRoots.length > 0 ? inputRoots : inputRoot !== void 0 ? [join(inputRoot, RULESYNC_RELATIVE_DIR_PATH)] : [join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)]).map((entry) => isAbsolute(entry) ? entry : resolve(entry));
|
|
1170
|
+
return [resolved[0], ...resolved.slice(1)];
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1140
1173
|
* Conflicting target pairs that cannot be used together.
|
|
1141
1174
|
* Exported so `rulesync doctor` can report the same conflicts as diagnostics
|
|
1142
1175
|
* without duplicating the list.
|
|
@@ -1173,6 +1206,28 @@ const assertTargetsFeaturesExclusive = ({ targets, features }) => {
|
|
|
1173
1206
|
if (targets !== void 0 && !Array.isArray(targets) && features !== void 0) throw new Error("Invalid config: when 'targets' is in object form, 'features' must be omitted. Declare per-target features inside the 'targets' object instead.");
|
|
1174
1207
|
};
|
|
1175
1208
|
/**
|
|
1209
|
+
* Rejects a single user-authored config file (or a single programmatic
|
|
1210
|
+
* construction) that defines both `inputRoot` and `inputRoots` — the two
|
|
1211
|
+
* fields express the same setting at singular vs. list level and cannot
|
|
1212
|
+
* be combined within one file without ambiguity.
|
|
1213
|
+
*
|
|
1214
|
+
* The check is intentionally per-file: base and local config files can each
|
|
1215
|
+
* be valid in isolation and merge into a state where both survive, and the
|
|
1216
|
+
* resolver picks `inputRoots` in that case (see `resolveEffectiveInputRoots`).
|
|
1217
|
+
* Only a single file declaring both is a genuine authoring error.
|
|
1218
|
+
*/
|
|
1219
|
+
const assertInputRootFieldsExclusive = ({ inputRoot, inputRoots }) => {
|
|
1220
|
+
if (inputRoot !== void 0 && inputRoots !== void 0) throw new Error("Invalid config: 'inputRoot' and 'inputRoots' cannot be combined. Remove 'inputRoot' and keep 'inputRoots', or reduce 'inputRoots' to a single-element 'inputRoot' string.");
|
|
1221
|
+
};
|
|
1222
|
+
/**
|
|
1223
|
+
* Rejects an explicitly supplied empty `inputRoots` list. Omitting the field
|
|
1224
|
+
* selects the conventional default, while an empty list has no meaningful
|
|
1225
|
+
* source-tree semantics and must not be treated as an absent override.
|
|
1226
|
+
*/
|
|
1227
|
+
const assertInputRootsNonEmpty = ({ inputRoots }) => {
|
|
1228
|
+
if (inputRoots !== void 0 && inputRoots.length === 0) throw new Error("Invalid config: 'inputRoots' must be non-empty.");
|
|
1229
|
+
};
|
|
1230
|
+
/**
|
|
1176
1231
|
* Normalizes a post-resolution `ConfigParams` input by rejecting the case
|
|
1177
1232
|
* where both `targets` and `features` are undefined — a degenerate state
|
|
1178
1233
|
* that would silently produce a no-op config (no targets, no features).
|
|
@@ -1208,14 +1263,35 @@ var Config = class Config {
|
|
|
1208
1263
|
gitignoreDestination;
|
|
1209
1264
|
dryRun;
|
|
1210
1265
|
check;
|
|
1211
|
-
|
|
1266
|
+
/**
|
|
1267
|
+
* Ordered, absolute-path list of rulesync source trees. Each entry is a
|
|
1268
|
+
* source tree itself — the directory that directly contains `rules/`,
|
|
1269
|
+
* `skills/`, `mcp.jsonc`, etc. No implicit `.rulesync/` join is applied.
|
|
1270
|
+
*
|
|
1271
|
+
* Always non-empty by construction — the constructor either normalizes
|
|
1272
|
+
* an `inputRoot`/`inputRoots` input or falls back to a single-element
|
|
1273
|
+
* list containing `join(<cwd>, ".rulesync")`.
|
|
1274
|
+
*
|
|
1275
|
+
* `inputRoot` (singular) is a deprecated backward-compatibility alias
|
|
1276
|
+
* that expands to `[join(inputRoot, ".rulesync")]`.
|
|
1277
|
+
*
|
|
1278
|
+
* Typed as a non-empty tuple so the one internal caller that legitimately
|
|
1279
|
+
* needs "the primary root" (`normalizeConfigFilePath` fallback) can index
|
|
1280
|
+
* `[0]` without a runtime null-check.
|
|
1281
|
+
*/
|
|
1282
|
+
inputRoots;
|
|
1212
1283
|
configFilePath;
|
|
1213
1284
|
sources;
|
|
1214
|
-
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
|
|
1285
|
+
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
1215
1286
|
assertTargetsFeaturesExclusive({
|
|
1216
1287
|
targets,
|
|
1217
1288
|
features
|
|
1218
1289
|
});
|
|
1290
|
+
assertInputRootFieldsExclusive({
|
|
1291
|
+
inputRoot,
|
|
1292
|
+
inputRoots
|
|
1293
|
+
});
|
|
1294
|
+
assertInputRootsNonEmpty({ inputRoots });
|
|
1219
1295
|
assertTargetsOrFeaturesProvided({
|
|
1220
1296
|
targets,
|
|
1221
1297
|
features
|
|
@@ -1243,10 +1319,13 @@ var Config = class Config {
|
|
|
1243
1319
|
this.gitignoreDestination = gitignoreDestination ?? "gitignore";
|
|
1244
1320
|
this.dryRun = dryRun ?? false;
|
|
1245
1321
|
this.check = check ?? false;
|
|
1246
|
-
this.
|
|
1322
|
+
this.inputRoots = normalizeInputRoots({
|
|
1323
|
+
inputRoot,
|
|
1324
|
+
inputRoots
|
|
1325
|
+
});
|
|
1247
1326
|
this.configFilePath = normalizeConfigFilePath({
|
|
1248
1327
|
configFilePath,
|
|
1249
|
-
|
|
1328
|
+
anchorDir: dirname(this.inputRoots[0])
|
|
1250
1329
|
});
|
|
1251
1330
|
this.sources = sources ?? [];
|
|
1252
1331
|
}
|
|
@@ -1430,16 +1509,22 @@ var Config = class Config {
|
|
|
1430
1509
|
return this.check;
|
|
1431
1510
|
}
|
|
1432
1511
|
/**
|
|
1433
|
-
* Returns the
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1512
|
+
* Returns the ordered list of rulesync source trees. Each entry is the
|
|
1513
|
+
* source tree itself — the directory that directly contains `rules/`,
|
|
1514
|
+
* `skills/`, `mcp.jsonc`, etc. Values are absolute paths captured at
|
|
1515
|
+
* config-construction time, so this accessor is pure and never depends on
|
|
1516
|
+
* a live `process.cwd()` read.
|
|
1436
1517
|
*
|
|
1437
|
-
*
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1518
|
+
* The returned tuple is always non-empty: when no `inputRoot`/`inputRoots`
|
|
1519
|
+
* was supplied, `[join(process.cwd(), ".rulesync")]` is snapshotted once
|
|
1520
|
+
* during construction. The first entry is the required base source tree.
|
|
1521
|
+
* Later entries are optional overlays and may be absent; when present, they
|
|
1522
|
+
* take precedence when the same relative source path exists in more than
|
|
1523
|
+
* one root (see per-feature merge policies in the processor
|
|
1524
|
+
* `loadRulesync*` methods).
|
|
1440
1525
|
*/
|
|
1441
|
-
|
|
1442
|
-
return this.
|
|
1526
|
+
getInputRoots() {
|
|
1527
|
+
return this.inputRoots;
|
|
1443
1528
|
}
|
|
1444
1529
|
/**
|
|
1445
1530
|
* Returns the absolute path of the configuration file this config was
|
|
@@ -1480,6 +1565,7 @@ const getDefaults = () => ({
|
|
|
1480
1565
|
dryRun: false,
|
|
1481
1566
|
check: false,
|
|
1482
1567
|
inputRoot: void 0,
|
|
1568
|
+
inputRoots: void 0,
|
|
1483
1569
|
sources: []
|
|
1484
1570
|
});
|
|
1485
1571
|
const loadConfigFromFile = async (filePath) => {
|
|
@@ -1491,8 +1577,23 @@ const loadConfigFromFile = async (filePath) => {
|
|
|
1491
1577
|
targets: configParams.targets,
|
|
1492
1578
|
features: configParams.features
|
|
1493
1579
|
});
|
|
1580
|
+
try {
|
|
1581
|
+
assertInputRootFieldsExclusive({
|
|
1582
|
+
inputRoot: configParams.inputRoot,
|
|
1583
|
+
inputRoots: configParams.inputRoots
|
|
1584
|
+
});
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1587
|
+
throw new Error(`${detail} (in ${JSON.stringify(filePath)})`, { cause: error });
|
|
1588
|
+
}
|
|
1494
1589
|
return configParams;
|
|
1495
1590
|
};
|
|
1591
|
+
function mergeInputRootConfigs({ baseConfig, localConfig }) {
|
|
1592
|
+
return {
|
|
1593
|
+
inputRoot: localConfig.inputRoot ?? baseConfig.inputRoot,
|
|
1594
|
+
inputRoots: localConfig.inputRoots ?? baseConfig.inputRoots
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1496
1597
|
const mergeConfigs = (baseConfig, localConfig) => {
|
|
1497
1598
|
return {
|
|
1498
1599
|
targets: localConfig.targets ?? baseConfig.targets,
|
|
@@ -1510,7 +1611,10 @@ const mergeConfigs = (baseConfig, localConfig) => {
|
|
|
1510
1611
|
gitignoreDestination: localConfig.gitignoreDestination ?? baseConfig.gitignoreDestination,
|
|
1511
1612
|
dryRun: localConfig.dryRun ?? baseConfig.dryRun,
|
|
1512
1613
|
check: localConfig.check ?? baseConfig.check,
|
|
1513
|
-
|
|
1614
|
+
...mergeInputRootConfigs({
|
|
1615
|
+
baseConfig,
|
|
1616
|
+
localConfig
|
|
1617
|
+
}),
|
|
1514
1618
|
sources: localConfig.sources ?? baseConfig.sources
|
|
1515
1619
|
};
|
|
1516
1620
|
};
|
|
@@ -1539,13 +1643,14 @@ function assertMergedTargetsFeaturesExclusive({ configByFile, validatedConfigPat
|
|
|
1539
1643
|
}
|
|
1540
1644
|
}
|
|
1541
1645
|
/**
|
|
1542
|
-
* Resolve the effective `global` flag. When an
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1646
|
+
* Resolve the effective `global` flag. When an input root (singular
|
|
1647
|
+
* `inputRoot` or plural `inputRoots`) is in play the user is decoupling
|
|
1648
|
+
* source from output, so a config-file `global: true` is dropped (unless
|
|
1649
|
+
* the caller also explicitly passes `global`); a warning is emitted in
|
|
1545
1650
|
* that case. Returns the resolved boolean `global`.
|
|
1546
1651
|
*/
|
|
1547
1652
|
function resolveGlobal({ logger, resolvedInputRoot, global, configByFile, validatedConfigPath }) {
|
|
1548
|
-
if (resolvedInputRoot !== void 0 && global === void 0 && configByFile.global === true) warnWithFallback(logger, `Ignoring "global: true" from ${JSON.stringify(validatedConfigPath)} because an
|
|
1653
|
+
if (resolvedInputRoot !== void 0 && global === void 0 && configByFile.global === true) warnWithFallback(logger, `Ignoring "global: true" from ${JSON.stringify(validatedConfigPath)} because an input root was configured; pass global=true (CLI: --global) to keep user-scope output. Output will be project-scope (global=false).`);
|
|
1549
1654
|
return pick({
|
|
1550
1655
|
cli: global,
|
|
1551
1656
|
file: resolvedInputRoot !== void 0 ? false : configByFile.global,
|
|
@@ -1569,17 +1674,81 @@ function resolveFeaturesAndTargets({ features, targets, configByFile }) {
|
|
|
1569
1674
|
resolvedTargets: userProvidedTargets ?? getDefaults().targets
|
|
1570
1675
|
};
|
|
1571
1676
|
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Resolve the effective, non-empty, absolute-path list of source-tree roots
|
|
1679
|
+
* by applying CLI > file > default precedence and preferring `inputRoots`
|
|
1680
|
+
* over `inputRoot` when both survive the base+local merge. Duplicates (after
|
|
1681
|
+
* normalization to absolute paths) are removed silently so overlapping
|
|
1682
|
+
* base/local declarations do not double-count the same tree.
|
|
1683
|
+
*
|
|
1684
|
+
* Semantics (post-refactor):
|
|
1685
|
+
* - `inputRoots` entries are the source trees themselves (each holds
|
|
1686
|
+
* `rules/`, `skills/`, `mcp.jsonc`, etc.); they are passed through
|
|
1687
|
+
* unchanged.
|
|
1688
|
+
* - `inputRoot` (legacy singular) is a shorthand for "parent of the
|
|
1689
|
+
* `.rulesync/` source tree" and is expanded to `join(inputRoot,
|
|
1690
|
+
* ".rulesync")` before it hits any consumer.
|
|
1691
|
+
* - The "nothing configured" default expands to `[join(cwd, ".rulesync")]`
|
|
1692
|
+
* so existing projects keep working unchanged.
|
|
1693
|
+
*
|
|
1694
|
+
* When both the merged file config has `inputRoots` and the CLI supplied
|
|
1695
|
+
* `inputRoot` (or vice versa), CLI wins outright — matching how every
|
|
1696
|
+
* other field is resolved. When only the file config supplies both, the
|
|
1697
|
+
* plural wins over the singular and the drop is logged at debug level.
|
|
1698
|
+
*/
|
|
1699
|
+
function resolveEffectiveInputRoots({ cliInputRoot, cliInputRoots, configByFile, cwd, logger }) {
|
|
1700
|
+
let source;
|
|
1701
|
+
let field;
|
|
1702
|
+
if (cliInputRoots !== void 0 && cliInputRoots.length > 0) {
|
|
1703
|
+
source = cliInputRoots;
|
|
1704
|
+
field = "inputRoots";
|
|
1705
|
+
} else if (cliInputRoot !== void 0) {
|
|
1706
|
+
source = [join(cliInputRoot, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1707
|
+
field = "inputRoot";
|
|
1708
|
+
} else if (configByFile.inputRoots !== void 0 && configByFile.inputRoots.length > 0) {
|
|
1709
|
+
source = configByFile.inputRoots;
|
|
1710
|
+
field = "inputRoots";
|
|
1711
|
+
if (configByFile.inputRoot !== void 0) logger?.debug(`Both 'inputRoot' and 'inputRoots' were set after merging base and local configs; 'inputRoots' wins and 'inputRoot' was dropped.`);
|
|
1712
|
+
} else if (configByFile.inputRoot !== void 0) {
|
|
1713
|
+
source = [join(configByFile.inputRoot, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1714
|
+
field = "inputRoot";
|
|
1715
|
+
} else source = [join(cwd, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1716
|
+
const candidates = source.map((entry) => resolve(cwd, entry));
|
|
1717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1718
|
+
const resolved = [];
|
|
1719
|
+
for (const absolute of candidates) {
|
|
1720
|
+
if (seen.has(absolute)) continue;
|
|
1721
|
+
seen.add(absolute);
|
|
1722
|
+
resolved.push(absolute);
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
inputRoots: [resolved[0], ...resolved.slice(1)],
|
|
1726
|
+
candidates,
|
|
1727
|
+
field
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1572
1730
|
var ConfigResolver = class {
|
|
1573
|
-
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot }, { logger } = {}) {
|
|
1731
|
+
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot, inputRoots }, { logger } = {}) {
|
|
1574
1732
|
const cwd = resolve(process.cwd());
|
|
1733
|
+
assertInputRootFieldsExclusive({
|
|
1734
|
+
inputRoot,
|
|
1735
|
+
inputRoots
|
|
1736
|
+
});
|
|
1737
|
+
assertInputRootsNonEmpty({ inputRoots });
|
|
1575
1738
|
if (inputRoot !== void 0) validateOutputRoot(inputRoot);
|
|
1576
|
-
|
|
1739
|
+
if (inputRoots !== void 0) for (const entry of inputRoots) validateOutputRoot(entry);
|
|
1740
|
+
const cliConfigAnchor = inputRoot;
|
|
1741
|
+
const hasCliInputRootOverride = inputRoot !== void 0 || inputRoots !== void 0;
|
|
1742
|
+
const validatedConfigPath = resolvePath(configPath, resolve(cliConfigAnchor ?? cwd));
|
|
1577
1743
|
const baseConfig = await loadConfigFromFile(validatedConfigPath);
|
|
1578
1744
|
const configDir = dirname(validatedConfigPath);
|
|
1579
1745
|
const localConfigPath = join(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
|
|
1580
1746
|
const localConfig = await loadConfigFromFile(localConfigPath);
|
|
1581
1747
|
const configByFile = mergeConfigs(baseConfig, localConfig);
|
|
1582
|
-
if (
|
|
1748
|
+
if (!hasCliInputRootOverride) {
|
|
1749
|
+
if (configByFile.inputRoot !== void 0) validateOutputRoot(configByFile.inputRoot);
|
|
1750
|
+
if (configByFile.inputRoots !== void 0) for (const entry of configByFile.inputRoots) validateOutputRoot(entry);
|
|
1751
|
+
}
|
|
1583
1752
|
assertMergedTargetsFeaturesExclusive({
|
|
1584
1753
|
configByFile,
|
|
1585
1754
|
validatedConfigPath,
|
|
@@ -1605,10 +1774,16 @@ var ConfigResolver = class {
|
|
|
1605
1774
|
silent: resolvedSilent
|
|
1606
1775
|
});
|
|
1607
1776
|
}
|
|
1608
|
-
const
|
|
1777
|
+
const resolvedInputRoots = resolveEffectiveInputRoots({
|
|
1778
|
+
cliInputRoot: inputRoot,
|
|
1779
|
+
cliInputRoots: inputRoots,
|
|
1780
|
+
configByFile,
|
|
1781
|
+
cwd,
|
|
1782
|
+
logger
|
|
1783
|
+
}).inputRoots;
|
|
1609
1784
|
const resolvedGlobal = resolveGlobal({
|
|
1610
1785
|
logger,
|
|
1611
|
-
resolvedInputRoot,
|
|
1786
|
+
resolvedInputRoot: inputRoot !== void 0 || inputRoots !== void 0 || configByFile.inputRoot !== void 0 || configByFile.inputRoots !== void 0 ? resolvedInputRoots[0] : void 0,
|
|
1612
1787
|
global,
|
|
1613
1788
|
configByFile,
|
|
1614
1789
|
validatedConfigPath
|
|
@@ -1672,7 +1847,7 @@ var ConfigResolver = class {
|
|
|
1672
1847
|
file: configByFile.check,
|
|
1673
1848
|
fallback: getDefaults().check
|
|
1674
1849
|
}),
|
|
1675
|
-
|
|
1850
|
+
inputRoots: resolvedInputRoots,
|
|
1676
1851
|
configFilePath: validatedConfigPath,
|
|
1677
1852
|
sources: configByFile.sources ?? getDefaults().sources,
|
|
1678
1853
|
flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
|
|
@@ -2072,8 +2247,9 @@ var RulesyncCheck = class RulesyncCheck extends RulesyncFile {
|
|
|
2072
2247
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
2073
2248
|
};
|
|
2074
2249
|
}
|
|
2075
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
2076
|
-
const
|
|
2250
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
2251
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().relativeDirPath;
|
|
2252
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
2077
2253
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
2078
2254
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
2079
2255
|
const result = RulesyncCheckFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -2081,7 +2257,7 @@ var RulesyncCheck = class RulesyncCheck extends RulesyncFile {
|
|
|
2081
2257
|
const filename = basename(relativeFilePath);
|
|
2082
2258
|
return new RulesyncCheck({
|
|
2083
2259
|
outputRoot,
|
|
2084
|
-
relativeDirPath:
|
|
2260
|
+
relativeDirPath: dirPath,
|
|
2085
2261
|
relativeFilePath: filename,
|
|
2086
2262
|
frontmatter: result.data,
|
|
2087
2263
|
body: content.trim()
|
|
@@ -2152,8 +2328,9 @@ var RulesyncCommand = class RulesyncCommand extends RulesyncFile {
|
|
|
2152
2328
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
2153
2329
|
};
|
|
2154
2330
|
}
|
|
2155
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
2156
|
-
const
|
|
2331
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
2332
|
+
const dirPath = relativeDirPath ?? RulesyncCommand.getSettablePaths().relativeDirPath;
|
|
2333
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
2157
2334
|
const fileContent = await readFileContent(filePath);
|
|
2158
2335
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
|
|
2159
2336
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
@@ -2161,7 +2338,7 @@ var RulesyncCommand = class RulesyncCommand extends RulesyncFile {
|
|
|
2161
2338
|
if (!result.success) throw new Error(`Invalid frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
|
|
2162
2339
|
return new RulesyncCommand({
|
|
2163
2340
|
outputRoot,
|
|
2164
|
-
relativeDirPath:
|
|
2341
|
+
relativeDirPath: dirPath,
|
|
2165
2342
|
relativeFilePath,
|
|
2166
2343
|
frontmatter: result.data,
|
|
2167
2344
|
body: content.trim(),
|
|
@@ -3550,8 +3727,19 @@ const CANONICAL_TO_VIBE_EVENT_NAMES = {
|
|
|
3550
3727
|
};
|
|
3551
3728
|
/**
|
|
3552
3729
|
* Map Mistral Vibe snake_case event names to canonical camelCase.
|
|
3730
|
+
*
|
|
3731
|
+
* The pre-2.21.0 spellings are accepted alongside the current ones. Vibe's
|
|
3732
|
+
* strict `HookType` enum rejects a file that still uses them, so such a file is
|
|
3733
|
+
* already dead on disk; reading it here and emitting the renamed spelling is
|
|
3734
|
+
* what repairs it, whereas leaving the old name unmapped would route the hook
|
|
3735
|
+
* into a tool override block and lose the event.
|
|
3553
3736
|
*/
|
|
3554
|
-
const VIBE_TO_CANONICAL_EVENT_NAMES =
|
|
3737
|
+
const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
3738
|
+
...Object.fromEntries(Object.entries(CANONICAL_TO_VIBE_EVENT_NAMES).map(([k, v]) => [v, k])),
|
|
3739
|
+
before_tool: "preToolUse",
|
|
3740
|
+
after_tool: "postToolUse",
|
|
3741
|
+
post_agent_turn: "stop"
|
|
3742
|
+
};
|
|
3555
3743
|
/**
|
|
3556
3744
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
3557
3745
|
*
|
|
@@ -3772,15 +3960,17 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
3772
3960
|
error: null
|
|
3773
3961
|
};
|
|
3774
3962
|
}
|
|
3775
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
3963
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true }) {
|
|
3776
3964
|
const paths = RulesyncHooks.getSettablePaths();
|
|
3965
|
+
const overrideDirPath = relativeDirPath;
|
|
3777
3966
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
3778
|
-
const
|
|
3967
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
3968
|
+
const filePath = join(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
3779
3969
|
if (!await fileExists(filePath)) continue;
|
|
3780
3970
|
const fileContent = await readFileContent(filePath);
|
|
3781
3971
|
return new RulesyncHooks({
|
|
3782
3972
|
outputRoot,
|
|
3783
|
-
relativeDirPath:
|
|
3973
|
+
relativeDirPath: candidateDirPath,
|
|
3784
3974
|
relativeFilePath: candidate.relativeFilePath,
|
|
3785
3975
|
fileContent,
|
|
3786
3976
|
validate
|
|
@@ -3807,21 +3997,23 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
|
|
|
3807
3997
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
3808
3998
|
relativeFilePath: RULESYNC_AIIGNORE_FILE_NAME
|
|
3809
3999
|
},
|
|
3810
|
-
legacy: {
|
|
4000
|
+
legacy: [{
|
|
3811
4001
|
relativeDirPath: ".",
|
|
3812
4002
|
relativeFilePath: RULESYNC_IGNORE_RELATIVE_FILE_PATH
|
|
3813
|
-
}
|
|
4003
|
+
}]
|
|
3814
4004
|
};
|
|
3815
4005
|
}
|
|
3816
|
-
static async fromFile({ outputRoot = process.cwd() } = {}) {
|
|
4006
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath } = {}) {
|
|
3817
4007
|
const paths = this.getSettablePaths();
|
|
3818
|
-
const
|
|
3819
|
-
const
|
|
4008
|
+
const recommendedDirPath = relativeDirPath ?? paths.recommended.relativeDirPath;
|
|
4009
|
+
const recommendedPath = join(outputRoot, recommendedDirPath, paths.recommended.relativeFilePath);
|
|
4010
|
+
const [legacy] = paths.legacy;
|
|
4011
|
+
const legacyPath = join(outputRoot, legacy.relativeDirPath, legacy.relativeFilePath);
|
|
3820
4012
|
if (await fileExists(recommendedPath)) {
|
|
3821
4013
|
const fileContent = await readFileContent(recommendedPath);
|
|
3822
4014
|
return new RulesyncIgnore({
|
|
3823
4015
|
outputRoot,
|
|
3824
|
-
relativeDirPath:
|
|
4016
|
+
relativeDirPath: recommendedDirPath,
|
|
3825
4017
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
3826
4018
|
fileContent
|
|
3827
4019
|
});
|
|
@@ -3830,15 +4022,15 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
|
|
|
3830
4022
|
const fileContent = await readFileContent(legacyPath);
|
|
3831
4023
|
return new RulesyncIgnore({
|
|
3832
4024
|
outputRoot,
|
|
3833
|
-
relativeDirPath:
|
|
3834
|
-
relativeFilePath:
|
|
4025
|
+
relativeDirPath: legacy.relativeDirPath,
|
|
4026
|
+
relativeFilePath: legacy.relativeFilePath,
|
|
3835
4027
|
fileContent
|
|
3836
4028
|
});
|
|
3837
4029
|
}
|
|
3838
4030
|
const fileContent = await readFileContent(recommendedPath);
|
|
3839
4031
|
return new RulesyncIgnore({
|
|
3840
4032
|
outputRoot,
|
|
3841
|
-
relativeDirPath:
|
|
4033
|
+
relativeDirPath: recommendedDirPath,
|
|
3842
4034
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
3843
4035
|
fileContent
|
|
3844
4036
|
});
|
|
@@ -3994,6 +4186,118 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
3994
4186
|
warp: z.optional(toolScopedMcpSchema),
|
|
3995
4187
|
zed: z.optional(toolScopedMcpSchema)
|
|
3996
4188
|
});
|
|
4189
|
+
/**
|
|
4190
|
+
* The tool-scoped block keys that carry a `{toolname}.mcpServers` sub-map.
|
|
4191
|
+
* Derived from `RulesyncMcpFileSchema`'s own shape so this set can never drift
|
|
4192
|
+
* from the schema — every tool-scoped block declared above is treated as a
|
|
4193
|
+
* "merge servers by name" site by `mergeMcpJsonOverlays`, and everything else
|
|
4194
|
+
* (including `$schema` and top-level Kimi Code timeout fields) is replaced
|
|
4195
|
+
* atomically.
|
|
4196
|
+
*/
|
|
4197
|
+
const TOOL_SCOPED_MCP_KEYS = new Set(Object.keys(RulesyncMcpFileSchema.def.shape).filter((key) => key !== "$schema" && key !== "mcpServers"));
|
|
4198
|
+
/**
|
|
4199
|
+
* Return the first candidate path (recommended, then legacy variants) that
|
|
4200
|
+
* exists under `outputRoot`, or `undefined` when none is present. Shared
|
|
4201
|
+
* between `fromFile` (single-root) and `fromRoots` (multi-root) so both
|
|
4202
|
+
* paths honour the same intra-root resolution order.
|
|
4203
|
+
*
|
|
4204
|
+
* When `overrideDirPath` is provided it replaces the candidates'
|
|
4205
|
+
* class-level `relativeDirPath` (which defaults to `.rulesync/`) so the
|
|
4206
|
+
* caller can point at a non-default source tree (e.g. `.rulesync.local/`).
|
|
4207
|
+
* Also returned is the effective `relativeDirPath` for the winning
|
|
4208
|
+
* candidate so the caller can reconstruct a `RulesyncMcp` with matching
|
|
4209
|
+
* anchor fields.
|
|
4210
|
+
*/
|
|
4211
|
+
async function findFirstExistingCandidate({ paths, outputRoot, overrideDirPath }) {
|
|
4212
|
+
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
4213
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
4214
|
+
const filePath = join(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
4215
|
+
if (await fileExists(filePath)) return {
|
|
4216
|
+
filePath,
|
|
4217
|
+
candidate: {
|
|
4218
|
+
relativeDirPath: candidateDirPath,
|
|
4219
|
+
relativeFilePath: candidate.relativeFilePath
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Merge two parsed MCP JSON objects with the one-level policy from the
|
|
4226
|
+
* inputRoots plan: the top-level `mcpServers` map and each
|
|
4227
|
+
* `<toolname>.mcpServers` sub-map are merged by server name (later wins per
|
|
4228
|
+
* key). Every other value — individual server configs, other top-level keys
|
|
4229
|
+
* — is replaced atomically. This keeps the merge predictable: an overlay can
|
|
4230
|
+
* add or replace whole shared servers, but a partial patch of one server's
|
|
4231
|
+
* `args`/`env` is deliberately not supported.
|
|
4232
|
+
*/
|
|
4233
|
+
function getRecordField({ value, path }) {
|
|
4234
|
+
if (value === void 0) return {};
|
|
4235
|
+
if (!isRecord$1(value)) throw new Error(`Invalid MCP overlay: '${path}' must be an object.`);
|
|
4236
|
+
return value;
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Overlay one record onto another, dropping any overlay key that could reach
|
|
4240
|
+
* `Object.prototype`. Every overlay merge goes through this so a `__proto__`
|
|
4241
|
+
* entry cannot enter the merged config from any depth — top-level
|
|
4242
|
+
* `mcpServers`, a tool-scoped block, or that block's own `mcpServers`.
|
|
4243
|
+
*/
|
|
4244
|
+
function mergeRecordsSkippingPollutionKeys({ base, overlay }) {
|
|
4245
|
+
const merged = { ...base };
|
|
4246
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
4247
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
4248
|
+
merged[key] = value;
|
|
4249
|
+
}
|
|
4250
|
+
return merged;
|
|
4251
|
+
}
|
|
4252
|
+
function mergeMcpJsonOverlays({ base, overlay }) {
|
|
4253
|
+
const merged = { ...base };
|
|
4254
|
+
for (const [key, overlayValue] of Object.entries(overlay)) {
|
|
4255
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
4256
|
+
if (key === "mcpServers") {
|
|
4257
|
+
merged.mcpServers = mergeRecordsSkippingPollutionKeys({
|
|
4258
|
+
base: getRecordField({
|
|
4259
|
+
value: base.mcpServers,
|
|
4260
|
+
path: "mcpServers"
|
|
4261
|
+
}),
|
|
4262
|
+
overlay: getRecordField({
|
|
4263
|
+
value: overlayValue,
|
|
4264
|
+
path: "mcpServers"
|
|
4265
|
+
})
|
|
4266
|
+
});
|
|
4267
|
+
continue;
|
|
4268
|
+
}
|
|
4269
|
+
if (TOOL_SCOPED_MCP_KEYS.has(key)) {
|
|
4270
|
+
const baseBlock = getRecordField({
|
|
4271
|
+
value: base[key],
|
|
4272
|
+
path: key
|
|
4273
|
+
});
|
|
4274
|
+
const overlayBlock = getRecordField({
|
|
4275
|
+
value: overlayValue,
|
|
4276
|
+
path: key
|
|
4277
|
+
});
|
|
4278
|
+
const mergedBlock = mergeRecordsSkippingPollutionKeys({
|
|
4279
|
+
base: baseBlock,
|
|
4280
|
+
overlay: overlayBlock
|
|
4281
|
+
});
|
|
4282
|
+
const baseServers = getRecordField({
|
|
4283
|
+
value: baseBlock.mcpServers,
|
|
4284
|
+
path: `${key}.mcpServers`
|
|
4285
|
+
});
|
|
4286
|
+
const overlayServers = getRecordField({
|
|
4287
|
+
value: overlayBlock.mcpServers,
|
|
4288
|
+
path: `${key}.mcpServers`
|
|
4289
|
+
});
|
|
4290
|
+
if (Object.keys(baseServers).length > 0 || Object.keys(overlayServers).length > 0) mergedBlock.mcpServers = mergeRecordsSkippingPollutionKeys({
|
|
4291
|
+
base: baseServers,
|
|
4292
|
+
overlay: overlayServers
|
|
4293
|
+
});
|
|
4294
|
+
merged[key] = mergedBlock;
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
merged[key] = overlayValue;
|
|
4298
|
+
}
|
|
4299
|
+
return merged;
|
|
4300
|
+
}
|
|
3997
4301
|
var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
3998
4302
|
json;
|
|
3999
4303
|
constructor(params) {
|
|
@@ -4030,28 +4334,136 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
4030
4334
|
error: null
|
|
4031
4335
|
};
|
|
4032
4336
|
}
|
|
4033
|
-
|
|
4337
|
+
/**
|
|
4338
|
+
* Load and merge MCP source files across the configured input roots.
|
|
4339
|
+
*
|
|
4340
|
+
* `inputRoots` entries are the source trees themselves (e.g.
|
|
4341
|
+
* `/repo/.rulesync`, `/repo/.rulesync.local`). Per-root behavior mirrors
|
|
4342
|
+
* `fromFile`: each root's own candidate paths (recommended `mcp.jsonc`,
|
|
4343
|
+
* legacy `mcp.json`, deprecated `.mcp.json`) are checked INSIDE that
|
|
4344
|
+
* source tree and the first hit is loaded. Roots that have no candidate
|
|
4345
|
+
* contribute nothing.
|
|
4346
|
+
*
|
|
4347
|
+
* Cross-root behavior: the parsed JSON objects are folded left-to-right
|
|
4348
|
+
* with `mergeMcpJsonOverlays`, so later roots overlay earlier ones by
|
|
4349
|
+
* server name (one level deep) and replace every other value atomically.
|
|
4350
|
+
* With one root, this delegates to `fromFile` so JSONC formatting and the
|
|
4351
|
+
* actual candidate path are preserved. With multiple roots, each source is
|
|
4352
|
+
* parsed and schema-validated before merging so failures name the originating
|
|
4353
|
+
* file. The merged object is necessarily synthetic, serialized JSON anchored
|
|
4354
|
+
* to the first root's recommended path.
|
|
4355
|
+
*
|
|
4356
|
+
* A multi-root configuration where only one root actually supplies a file is
|
|
4357
|
+
* treated as the single-root case: nothing is merged, so the original file
|
|
4358
|
+
* content is kept verbatim (preserving JSONC comments) and the instance is
|
|
4359
|
+
* anchored at the root that supplied it rather than at the primary root's
|
|
4360
|
+
* recommended path.
|
|
4361
|
+
*
|
|
4362
|
+
* When no root supplies any candidate, this falls back to reading the
|
|
4363
|
+
* primary root's recommended path so the underlying file-not-found error
|
|
4364
|
+
* matches the single-root behavior of `fromFile`.
|
|
4365
|
+
*/
|
|
4366
|
+
static async fromRoots({ inputRoots, validate = true, logger }) {
|
|
4367
|
+
if (inputRoots.length === 1) {
|
|
4368
|
+
const [primary] = inputRoots;
|
|
4369
|
+
return this.fromFile({
|
|
4370
|
+
outputRoot: dirname(primary),
|
|
4371
|
+
relativeDirPath: basename(primary),
|
|
4372
|
+
validate,
|
|
4373
|
+
logger
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4034
4376
|
const paths = this.getSettablePaths();
|
|
4377
|
+
const rootSources = [];
|
|
4378
|
+
for (const root of inputRoots) {
|
|
4379
|
+
const parent = dirname(root);
|
|
4380
|
+
const treeName = basename(root);
|
|
4381
|
+
const found = await findFirstExistingCandidate({
|
|
4382
|
+
paths,
|
|
4383
|
+
outputRoot: parent,
|
|
4384
|
+
overrideDirPath: treeName
|
|
4385
|
+
});
|
|
4386
|
+
if (found === void 0) continue;
|
|
4387
|
+
const { filePath, candidate } = found;
|
|
4388
|
+
if (filePath.endsWith(".mcp.json")) {
|
|
4389
|
+
const recommendedPath = join(parent, treeName, paths.recommended.relativeFilePath);
|
|
4390
|
+
logger?.warn(`⚠️ Using deprecated path "${filePath}". Please migrate to "${recommendedPath}"`);
|
|
4391
|
+
}
|
|
4392
|
+
const fileContent = await readFileContent(filePath);
|
|
4393
|
+
let parsed;
|
|
4394
|
+
try {
|
|
4395
|
+
parsed = parseJsonc(fileContent);
|
|
4396
|
+
if (!isRecord$1(parsed)) throw new Error("Expected a JSON object.");
|
|
4397
|
+
if (validate) {
|
|
4398
|
+
const result = RulesyncMcpFileSchema.safeParse(parsed);
|
|
4399
|
+
if (!result.success) throw result.error;
|
|
4400
|
+
}
|
|
4401
|
+
} catch (error) {
|
|
4402
|
+
throw new Error(`Invalid MCP source file '${filePath}': ${formatError(error)}`, { cause: error });
|
|
4403
|
+
}
|
|
4404
|
+
rootSources.push({
|
|
4405
|
+
record: parsed,
|
|
4406
|
+
outputRoot: parent,
|
|
4407
|
+
relativeDirPath: candidate.relativeDirPath,
|
|
4408
|
+
relativeFilePath: candidate.relativeFilePath,
|
|
4409
|
+
fileContent
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
if (rootSources.length === 0) {
|
|
4413
|
+
const primary = inputRoots[0];
|
|
4414
|
+
return this.fromFile({
|
|
4415
|
+
outputRoot: dirname(primary),
|
|
4416
|
+
relativeDirPath: basename(primary),
|
|
4417
|
+
validate,
|
|
4418
|
+
logger
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
const onlySource = rootSources.length === 1 ? rootSources[0] : void 0;
|
|
4422
|
+
if (onlySource !== void 0) return new RulesyncMcp({
|
|
4423
|
+
outputRoot: onlySource.outputRoot,
|
|
4424
|
+
relativeDirPath: onlySource.relativeDirPath,
|
|
4425
|
+
relativeFilePath: onlySource.relativeFilePath,
|
|
4426
|
+
fileContent: onlySource.fileContent,
|
|
4427
|
+
validate
|
|
4428
|
+
});
|
|
4429
|
+
const merged = rootSources.reduce((acc, next) => mergeMcpJsonOverlays({
|
|
4430
|
+
base: acc,
|
|
4431
|
+
overlay: next.record
|
|
4432
|
+
}), {});
|
|
4433
|
+
const primary = inputRoots[0];
|
|
4434
|
+
return new RulesyncMcp({
|
|
4435
|
+
outputRoot: dirname(primary),
|
|
4436
|
+
relativeDirPath: basename(primary),
|
|
4437
|
+
relativeFilePath: paths.recommended.relativeFilePath,
|
|
4438
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
4439
|
+
validate
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true, logger }) {
|
|
4443
|
+
const paths = this.getSettablePaths();
|
|
4444
|
+
const overrideDirPath = relativeDirPath;
|
|
4035
4445
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
4036
|
-
const
|
|
4446
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
4447
|
+
const filePath = join(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
4037
4448
|
if (!await fileExists(filePath)) continue;
|
|
4038
4449
|
if (candidate.relativeFilePath === ".mcp.json") {
|
|
4039
|
-
const recommendedPath = join(outputRoot,
|
|
4450
|
+
const recommendedPath = join(outputRoot, candidateDirPath, paths.recommended.relativeFilePath);
|
|
4040
4451
|
logger?.warn(`⚠️ Using deprecated path "${filePath}". Please migrate to "${recommendedPath}"`);
|
|
4041
4452
|
}
|
|
4042
4453
|
const fileContent = await readFileContent(filePath);
|
|
4043
4454
|
return new RulesyncMcp({
|
|
4044
4455
|
outputRoot,
|
|
4045
|
-
relativeDirPath:
|
|
4456
|
+
relativeDirPath: candidateDirPath,
|
|
4046
4457
|
relativeFilePath: candidate.relativeFilePath,
|
|
4047
4458
|
fileContent,
|
|
4048
4459
|
validate
|
|
4049
4460
|
});
|
|
4050
4461
|
}
|
|
4051
|
-
const
|
|
4462
|
+
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
4463
|
+
const fileContent = await readFileContent(join(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath));
|
|
4052
4464
|
return new RulesyncMcp({
|
|
4053
4465
|
outputRoot,
|
|
4054
|
-
relativeDirPath:
|
|
4466
|
+
relativeDirPath: fallbackDirPath,
|
|
4055
4467
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
4056
4468
|
fileContent,
|
|
4057
4469
|
validate
|
|
@@ -4397,11 +4809,26 @@ const KiloPermissionsOverrideSchema = z.looseObject({
|
|
|
4397
4809
|
* rather than which commands are permitted — so it is a loose passthrough on
|
|
4398
4810
|
* the same terms, merged into the top level of `.claude/settings.json`.
|
|
4399
4811
|
*
|
|
4812
|
+
* Any other key is a plain top-level `settings.json` key (`editorMode`, `env`,
|
|
4813
|
+
* `model`, ...), deep-merged into the generated file verbatim so settings
|
|
4814
|
+
* Claude Code adds faster than an allowlist can track stay authorable.
|
|
4815
|
+
* The exceptions are the keys another feature owns (`hooks`) and `$schema`.
|
|
4816
|
+
* Keys the target file cannot honor — `Managed`-only and `~/.claude.json`-only
|
|
4817
|
+
* keys in either scope, plus user-scope keys at project scope — are dropped
|
|
4818
|
+
* with a warning rather than written where they would never apply. So are the
|
|
4819
|
+
* keys whose value is a command Claude Code executes (`apiKeyHelper`,
|
|
4820
|
+
* `statusLine`, ...): this file is shareable via `rulesync fetch`, and a file
|
|
4821
|
+
* named for restricting things is not where a command belongs — author those
|
|
4822
|
+
* in `.rulesync/hooks.jsonc` instead.
|
|
4823
|
+
*
|
|
4400
4824
|
* @example
|
|
4401
4825
|
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
4402
4826
|
* @example
|
|
4403
4827
|
* { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
|
|
4828
|
+
* @example
|
|
4829
|
+
* { "editorMode": "vim", "env": { "MY_VAR": "1" } }
|
|
4404
4830
|
* @see https://code.claude.com/docs/en/sandboxing
|
|
4831
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
4405
4832
|
*/
|
|
4406
4833
|
const ClaudecodePermissionsOverrideSchema = z.looseObject({
|
|
4407
4834
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
@@ -5076,6 +5503,15 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
|
|
|
5076
5503
|
sandbox_workspace_write: z.optional(z.looseObject({})),
|
|
5077
5504
|
apps: z.optional(z.looseObject({})),
|
|
5078
5505
|
approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})])),
|
|
5506
|
+
/**
|
|
5507
|
+
* The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
|
|
5508
|
+
* Not a permission surface, but like `apps` it is a top-level table with no
|
|
5509
|
+
* canonical category, and Codex adds keys to it faster than an explicit model
|
|
5510
|
+
* could track — so it is a loose passthrough written verbatim.
|
|
5511
|
+
*
|
|
5512
|
+
* @see https://developers.openai.com/codex/config-reference
|
|
5513
|
+
*/
|
|
5514
|
+
tui: z.optional(z.looseObject({})),
|
|
5079
5515
|
git_write_rules: z.optional(z.boolean())
|
|
5080
5516
|
});
|
|
5081
5517
|
/**
|
|
@@ -5222,15 +5658,17 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
5222
5658
|
error: null
|
|
5223
5659
|
};
|
|
5224
5660
|
}
|
|
5225
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
5661
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true }) {
|
|
5226
5662
|
const paths = RulesyncPermissions.getSettablePaths();
|
|
5663
|
+
const overrideDirPath = relativeDirPath;
|
|
5227
5664
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
5228
|
-
const
|
|
5665
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
5666
|
+
const filePath = join(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
5229
5667
|
if (!await fileExists(filePath)) continue;
|
|
5230
5668
|
const fileContent = await readFileContent(filePath);
|
|
5231
5669
|
return new RulesyncPermissions({
|
|
5232
5670
|
outputRoot,
|
|
5233
|
-
relativeDirPath:
|
|
5671
|
+
relativeDirPath: candidateDirPath,
|
|
5234
5672
|
relativeFilePath: candidate.relativeFilePath,
|
|
5235
5673
|
fileContent,
|
|
5236
5674
|
validate
|
|
@@ -5394,8 +5832,9 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5394
5832
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5395
5833
|
};
|
|
5396
5834
|
}
|
|
5397
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
5398
|
-
const
|
|
5835
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
5836
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().recommended.relativeDirPath;
|
|
5837
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
5399
5838
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5400
5839
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5401
5840
|
const result = RulesyncRuleFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5408,7 +5847,7 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5408
5847
|
};
|
|
5409
5848
|
return new RulesyncRule({
|
|
5410
5849
|
outputRoot,
|
|
5411
|
-
relativeDirPath:
|
|
5850
|
+
relativeDirPath: dirPath,
|
|
5412
5851
|
relativeFilePath,
|
|
5413
5852
|
frontmatter: validatedFrontmatter,
|
|
5414
5853
|
body: content.trim(),
|
|
@@ -5656,7 +6095,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5656
6095
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
5657
6096
|
"user-invocable": z.optional(z.boolean()),
|
|
5658
6097
|
enabled: z.optional(z.boolean()),
|
|
5659
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
6098
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
6099
|
+
license: z.optional(z.unknown()),
|
|
6100
|
+
compatibility: z.optional(z.unknown()),
|
|
6101
|
+
metadata: z.optional(z.unknown()),
|
|
6102
|
+
version: z.optional(z.unknown())
|
|
5660
6103
|
})),
|
|
5661
6104
|
grokcli: z.optional(z.looseObject({
|
|
5662
6105
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
@@ -5832,8 +6275,9 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5832
6275
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5833
6276
|
};
|
|
5834
6277
|
}
|
|
5835
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
5836
|
-
const
|
|
6278
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
6279
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().relativeDirPath;
|
|
6280
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
5837
6281
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5838
6282
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5839
6283
|
const result = RulesyncSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5841,7 +6285,7 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5841
6285
|
const filename = basename(relativeFilePath);
|
|
5842
6286
|
return new RulesyncSubagent({
|
|
5843
6287
|
outputRoot,
|
|
5844
|
-
relativeDirPath:
|
|
6288
|
+
relativeDirPath: dirPath,
|
|
5845
6289
|
relativeFilePath: filename,
|
|
5846
6290
|
frontmatter: result.data,
|
|
5847
6291
|
body: content.trim()
|
|
@@ -5851,16 +6295,18 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5851
6295
|
//#endregion
|
|
5852
6296
|
//#region src/features/skills/skills-utils.ts
|
|
5853
6297
|
/**
|
|
5854
|
-
* Returns the set of local skill directory names (excluding `.curated`)
|
|
6298
|
+
* Returns the set of local skill directory names (excluding `.curated`)
|
|
6299
|
+
* from a rulesync source tree (e.g. `/repo/.rulesync` or
|
|
6300
|
+
* `/repo/.rulesync.local`).
|
|
5855
6301
|
*/
|
|
5856
|
-
async function getLocalSkillDirNames(
|
|
5857
|
-
const skillsDir = join(
|
|
6302
|
+
async function getLocalSkillDirNames(sourceTree) {
|
|
6303
|
+
const skillsDir = join(sourceTree, SKILLS_FEATURE_SUBDIR);
|
|
5858
6304
|
const names = /* @__PURE__ */ new Set();
|
|
5859
6305
|
if (!await directoryExists(skillsDir)) return names;
|
|
5860
6306
|
const dirPaths = await findFilesByGlobs(join(skillsDir, "*"), { type: "dir" });
|
|
5861
6307
|
for (const dirPath of dirPaths) {
|
|
5862
6308
|
const name = basename(dirPath);
|
|
5863
|
-
if (name === basename(
|
|
6309
|
+
if (name === basename(CURATED_SKILLS_FEATURE_SUBDIR)) continue;
|
|
5864
6310
|
names.add(name);
|
|
5865
6311
|
}
|
|
5866
6312
|
return names;
|
|
@@ -6173,15 +6619,49 @@ function companionFileContentsEquivalent({ filePath, expected, existing, compose
|
|
|
6173
6619
|
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
6174
6620
|
}
|
|
6175
6621
|
//#endregion
|
|
6622
|
+
//#region src/utils/control-characters.ts
|
|
6623
|
+
/**
|
|
6624
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
6625
|
+
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
6626
|
+
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
6627
|
+
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
6628
|
+
* settings file must never reach the terminal with these intact: they let the
|
|
6629
|
+
* text forge log lines, reorder what is printed around them, or inject escape
|
|
6630
|
+
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
6631
|
+
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
6632
|
+
* place to preserve the typography of a right-to-left name.
|
|
6633
|
+
*/
|
|
6634
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
6635
|
+
/**
|
|
6636
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
6637
|
+
* log line or other terminal output.
|
|
6638
|
+
*/
|
|
6639
|
+
function stripControlCharacters(text) {
|
|
6640
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
6641
|
+
}
|
|
6642
|
+
//#endregion
|
|
6176
6643
|
//#region src/types/feature-processor.ts
|
|
6177
6644
|
var FeatureProcessor = class {
|
|
6178
6645
|
outputRoot;
|
|
6179
|
-
|
|
6646
|
+
/**
|
|
6647
|
+
* Ordered, non-empty list of rulesync source-tree directories. Each entry
|
|
6648
|
+
* is a source tree itself — the directory that directly contains feature
|
|
6649
|
+
* subdirectories (`rules/`, `commands/`, …) and single-file features
|
|
6650
|
+
* (`mcp.jsonc`, `hooks.jsonc`, …). Later entries take precedence in
|
|
6651
|
+
* per-feature merges. Defaults to `[join(process.cwd(), ".rulesync")]`.
|
|
6652
|
+
*
|
|
6653
|
+
* The singular user-facing alias (`inputRoot` in `rulesync.jsonc` / the
|
|
6654
|
+
* `--input-root` CLI flag / `GenerateOptions.inputRoot`) is deprecated
|
|
6655
|
+
* and collapsed into `[join(inputRoot, ".rulesync")]` before it ever
|
|
6656
|
+
* reaches a processor — every internal consumer only ever sees the
|
|
6657
|
+
* plural form, with the source tree already resolved.
|
|
6658
|
+
*/
|
|
6659
|
+
inputRoots;
|
|
6180
6660
|
dryRun;
|
|
6181
6661
|
logger;
|
|
6182
|
-
constructor({ outputRoot = process.cwd(),
|
|
6662
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, logger }) {
|
|
6183
6663
|
this.outputRoot = outputRoot;
|
|
6184
|
-
this.
|
|
6664
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
6185
6665
|
this.dryRun = dryRun;
|
|
6186
6666
|
this.logger = logger;
|
|
6187
6667
|
}
|
|
@@ -6248,6 +6728,198 @@ var FeatureProcessor = class {
|
|
|
6248
6728
|
return orphanFiles.length;
|
|
6249
6729
|
}
|
|
6250
6730
|
};
|
|
6731
|
+
/**
|
|
6732
|
+
* Messages already reported for a given logger.
|
|
6733
|
+
*
|
|
6734
|
+
* One `generate` run constructs a single-file processor per tool target and
|
|
6735
|
+
* per output root — more than twenty times for `--targets "*"` — and each one
|
|
6736
|
+
* re-resolves the same roots. Keying on the logger — created once per run, and
|
|
6737
|
+
* once per test — keeps the shadowing warning to a single line instead of
|
|
6738
|
+
* repeating it for every target. `--watch` reuses one logger across runs, so
|
|
6739
|
+
* {@link resetRootShadowingWarnings} clears the set at the start of each one.
|
|
6740
|
+
*/
|
|
6741
|
+
const warnedRootShadowingByLogger = /* @__PURE__ */ new WeakMap();
|
|
6742
|
+
/**
|
|
6743
|
+
* Forget which shadowing warnings have already been reported.
|
|
6744
|
+
*
|
|
6745
|
+
* `generate` calls this once per run. Without it, `--watch` reuses one logger
|
|
6746
|
+
* for the whole session, so the warning would be printed on the first
|
|
6747
|
+
* generation and never again — the opposite of why it is a warning, since
|
|
6748
|
+
* `--watch` is exactly when an overlay is most likely to be added or edited.
|
|
6749
|
+
*/
|
|
6750
|
+
function resetRootShadowingWarnings({ logger }) {
|
|
6751
|
+
warnedRootShadowingByLogger.delete(logger);
|
|
6752
|
+
}
|
|
6753
|
+
/**
|
|
6754
|
+
* Return the last input root that contains any of the given `relativePaths`,
|
|
6755
|
+
* or `undefined` when none of the roots has any of them. Used by single-file
|
|
6756
|
+
* features (hooks, permissions, ignore) to implement the "later root wins
|
|
6757
|
+
* the whole file" merge policy without materializing the file contents.
|
|
6758
|
+
*
|
|
6759
|
+
* `relativePaths` accepts a small list so features that historically read
|
|
6760
|
+
* either a recommended path or a legacy alias (e.g. `.rulesync/mcp.jsonc`
|
|
6761
|
+
* plus `.rulesync/mcp.json`) can preserve that resolution order per root.
|
|
6762
|
+
* A root counts as "having" the file as long as at least one candidate path
|
|
6763
|
+
* is present.
|
|
6764
|
+
*/
|
|
6765
|
+
async function pickLastRootWithFile({ inputRoots, relativePaths, logger, artifactName }) {
|
|
6766
|
+
let winner;
|
|
6767
|
+
const rootsWithFile = [];
|
|
6768
|
+
for (const root of inputRoots) for (const relativePath of relativePaths) if (await fileExists(join(root, relativePath))) {
|
|
6769
|
+
winner = root;
|
|
6770
|
+
rootsWithFile.push(root);
|
|
6771
|
+
break;
|
|
6772
|
+
}
|
|
6773
|
+
if (rootsWithFile.length > 1 && winner !== void 0) {
|
|
6774
|
+
const shadowed = rootsWithFile.slice(0, -1);
|
|
6775
|
+
const message = `${artifactName} is provided by more than one input root; '${stripControlCharacters(winner)}' replaces the whole file from ${shadowed.map((root) => `'${stripControlCharacters(root)}'`).join(", ")}.`;
|
|
6776
|
+
let warnedMessages = warnedRootShadowingByLogger.get(logger);
|
|
6777
|
+
if (warnedMessages === void 0) {
|
|
6778
|
+
warnedMessages = /* @__PURE__ */ new Set();
|
|
6779
|
+
warnedRootShadowingByLogger.set(logger, warnedMessages);
|
|
6780
|
+
}
|
|
6781
|
+
if (!warnedMessages.has(message)) {
|
|
6782
|
+
warnedMessages.add(message);
|
|
6783
|
+
logger.warn(message);
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
return winner;
|
|
6787
|
+
}
|
|
6788
|
+
/**
|
|
6789
|
+
* Merge per-root result lists into a single ordered list, keeping the
|
|
6790
|
+
* later root's entry when two roots produced an item with the same
|
|
6791
|
+
* identity. Identity is intentionally provided by the caller so
|
|
6792
|
+
* per-feature nuances (case-insensitive filesystems, directory names for
|
|
6793
|
+
* skills, server names for MCP) live next to the feature that owns them.
|
|
6794
|
+
*
|
|
6795
|
+
* The returned list preserves the FIRST appearance order of each identity —
|
|
6796
|
+
* items in the earliest root keep their position, but their content is
|
|
6797
|
+
* replaced by the last root that provided the same identity. This matches
|
|
6798
|
+
* the "overlay" mental model: an overlay changes content, not order.
|
|
6799
|
+
*/
|
|
6800
|
+
function mergeByIdentity({ perRoot, identity }) {
|
|
6801
|
+
const order = [];
|
|
6802
|
+
const winnerByKey = /* @__PURE__ */ new Map();
|
|
6803
|
+
for (const rootItems of perRoot) for (const item of rootItems) {
|
|
6804
|
+
const key = identity(item);
|
|
6805
|
+
if (!winnerByKey.has(key)) order.push(key);
|
|
6806
|
+
winnerByKey.set(key, item);
|
|
6807
|
+
}
|
|
6808
|
+
return order.map((key) => winnerByKey.get(key));
|
|
6809
|
+
}
|
|
6810
|
+
/**
|
|
6811
|
+
* The key two spellings share when a case-insensitive filesystem would give
|
|
6812
|
+
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
6813
|
+
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
6814
|
+
* locale), and the NFC pass folds the composed and decomposed spellings of an
|
|
6815
|
+
* accented name — which macOS also resolves to a single directory —
|
|
6816
|
+
* onto each other.
|
|
6817
|
+
*
|
|
6818
|
+
* This is simple lowercasing rather than full Unicode case folding, so it is
|
|
6819
|
+
* deliberately narrower than what a filesystem considers one file: a Greek
|
|
6820
|
+
* final sigma, a Turkish `ı` under NTFS's upcasing, and a Win32 name whose
|
|
6821
|
+
* trailing dot is stripped all still produce distinct keys. Those pairs keep
|
|
6822
|
+
* the pre-existing behavior (both are imported, and the later one wins on the
|
|
6823
|
+
* filesystem); folding them here would instead drop names that a
|
|
6824
|
+
* case-sensitive filesystem keeps genuinely apart.
|
|
6825
|
+
*/
|
|
6826
|
+
function caseFoldIdentity(identity) {
|
|
6827
|
+
return identity.normalize("NFC").toLowerCase();
|
|
6828
|
+
}
|
|
6829
|
+
/**
|
|
6830
|
+
* Group spellings by their case-folded identity, keeping every original
|
|
6831
|
+
* spelling. On a case-sensitive filesystem one identity can cover several
|
|
6832
|
+
* spellings at once, and the caller needs them all to describe a collision
|
|
6833
|
+
* accurately.
|
|
6834
|
+
*/
|
|
6835
|
+
function groupSpellingsByCaseFoldedIdentity(spellings) {
|
|
6836
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6837
|
+
for (const spelling of spellings) {
|
|
6838
|
+
const identity = caseFoldIdentity(spelling);
|
|
6839
|
+
const existing = grouped.get(identity);
|
|
6840
|
+
if (existing === void 0) grouped.set(identity, [spelling]);
|
|
6841
|
+
else existing.push(spelling);
|
|
6842
|
+
}
|
|
6843
|
+
return grouped;
|
|
6844
|
+
}
|
|
6845
|
+
/**
|
|
6846
|
+
* Build the warning emitted when a `.curated/` entry and a local entry in the
|
|
6847
|
+
* same tree differ only in case.
|
|
6848
|
+
*
|
|
6849
|
+
* `.curated/` is expanded from a declarative source (an external Git repository
|
|
6850
|
+
* or npm package), so its names are untrusted input; both sides are stripped of
|
|
6851
|
+
* control characters before they reach the terminal.
|
|
6852
|
+
*
|
|
6853
|
+
* The winning local spelling is the LAST one, matching the precedence
|
|
6854
|
+
* {@link mergeByCaseInsensitiveIdentity} applies afterwards; any other spelling
|
|
6855
|
+
* that folds onto the same identity is listed too, so the message never names a
|
|
6856
|
+
* spelling that loses.
|
|
6857
|
+
*/
|
|
6858
|
+
function formatCuratedCaseCollisionWarning({ artifactKind, entryNoun, treeDirPath, curatedSpelling, localSpellings }) {
|
|
6859
|
+
const winner = localSpellings[localSpellings.length - 1] ?? "";
|
|
6860
|
+
const shadowed = localSpellings.slice(0, -1);
|
|
6861
|
+
const shadowedSuffix = shadowed.length === 0 ? "" : ` Other local spellings that fold onto the same identity: ${shadowed.map((spelling) => `'${stripControlCharacters(spelling)}'`).join(", ")}.`;
|
|
6862
|
+
return `Case-insensitive ${artifactKind} collision under ${stripControlCharacters(treeDirPath)}: curated '${stripControlCharacters(curatedSpelling)}' and local '${stripControlCharacters(winner)}' resolve to the same identity. The local ${entryNoun} wins and the curated ${entryNoun} is skipped.` + shadowedSuffix;
|
|
6863
|
+
}
|
|
6864
|
+
/**
|
|
6865
|
+
* Merge artifacts whose filenames are case-insensitive identities, warning
|
|
6866
|
+
* when distinct spellings collapse to the same key. Exact-name overlays are
|
|
6867
|
+
* intentional and remain quiet; only case-only ambiguity is diagnosed.
|
|
6868
|
+
*
|
|
6869
|
+
* Precedence here is the opposite of {@link ClaimedIdentities}: this merges
|
|
6870
|
+
* overlay roots, where the LAST entry wins, while the tool-side loaders keep
|
|
6871
|
+
* the FIRST root to claim a name.
|
|
6872
|
+
*/
|
|
6873
|
+
function mergeByCaseInsensitiveIdentity({ perRoot, identity, artifactName, logger }) {
|
|
6874
|
+
const spellingByKey = /* @__PURE__ */ new Map();
|
|
6875
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
6876
|
+
return mergeByIdentity({
|
|
6877
|
+
perRoot,
|
|
6878
|
+
identity: (item) => {
|
|
6879
|
+
const spelling = identity(item);
|
|
6880
|
+
const key = caseFoldIdentity(spelling);
|
|
6881
|
+
const previousSpelling = spellingByKey.get(key);
|
|
6882
|
+
if (previousSpelling !== void 0 && previousSpelling !== spelling && !warnedKeys.has(key)) {
|
|
6883
|
+
logger.warn(`Case-insensitive ${artifactName} collision: '${stripControlCharacters(previousSpelling)}' and '${stripControlCharacters(spelling)}' resolve to the same identity. The later entry wins.`);
|
|
6884
|
+
warnedKeys.add(key);
|
|
6885
|
+
}
|
|
6886
|
+
if (previousSpelling === void 0) spellingByKey.set(key, spelling);
|
|
6887
|
+
return key;
|
|
6888
|
+
}
|
|
6889
|
+
});
|
|
6890
|
+
}
|
|
6891
|
+
/**
|
|
6892
|
+
* Tracks the import identities already claimed while scanning, folding case
|
|
6893
|
+
* through {@link caseFoldIdentity}.
|
|
6894
|
+
*
|
|
6895
|
+
* The tool-side loaders scan several roots in precedence order and keep the
|
|
6896
|
+
* first spelling of each identity. Comparing those identities exactly lets
|
|
6897
|
+
* `.junie/skills/dup-skill` and `.agents/skills/Dup-Skill` both through, and
|
|
6898
|
+
* since macOS and Windows resolve the two written-back directories to a
|
|
6899
|
+
* single one, the shared Agent Skills copy lands last and overwrites the
|
|
6900
|
+
* tool-specific one — inverting the precedence the roots were ordered by.
|
|
6901
|
+
*
|
|
6902
|
+
* The FIRST claimer wins, which is the opposite of the overlay precedence in
|
|
6903
|
+
* {@link mergeByCaseInsensitiveIdentity}: roots are passed in precedence
|
|
6904
|
+
* order, so the earliest one to name a skill is the one that should keep it.
|
|
6905
|
+
*/
|
|
6906
|
+
var ClaimedIdentities = class {
|
|
6907
|
+
claimByKey = /* @__PURE__ */ new Map();
|
|
6908
|
+
/**
|
|
6909
|
+
* Claim `identity` on behalf of `source`. Returns `null` when nothing held
|
|
6910
|
+
* it yet, or the standing claim when something did.
|
|
6911
|
+
*/
|
|
6912
|
+
claim({ identity, source }) {
|
|
6913
|
+
const key = caseFoldIdentity(identity);
|
|
6914
|
+
const claimed = this.claimByKey.get(key);
|
|
6915
|
+
if (claimed !== void 0) return claimed;
|
|
6916
|
+
this.claimByKey.set(key, {
|
|
6917
|
+
spelling: identity,
|
|
6918
|
+
source
|
|
6919
|
+
});
|
|
6920
|
+
return null;
|
|
6921
|
+
}
|
|
6922
|
+
};
|
|
6251
6923
|
//#endregion
|
|
6252
6924
|
//#region src/constants/amp-paths.ts
|
|
6253
6925
|
const AMP_DIR = ".amp";
|
|
@@ -7463,7 +8135,8 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
7463
8135
|
"sandbox_mode",
|
|
7464
8136
|
"sandbox_workspace_write",
|
|
7465
8137
|
"apps",
|
|
7466
|
-
"approvals_reviewer"
|
|
8138
|
+
"approvals_reviewer",
|
|
8139
|
+
"tui"
|
|
7467
8140
|
];
|
|
7468
8141
|
//#endregion
|
|
7469
8142
|
//#region src/features/shared/shared-config-gateway.ts
|
|
@@ -8569,10 +9242,10 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8569
9242
|
toolTarget;
|
|
8570
9243
|
global;
|
|
8571
9244
|
getFactory;
|
|
8572
|
-
constructor({ outputRoot = process.cwd(),
|
|
9245
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$6, dryRun = false, logger }) {
|
|
8573
9246
|
super({
|
|
8574
9247
|
outputRoot,
|
|
8575
|
-
|
|
9248
|
+
inputRoots,
|
|
8576
9249
|
dryRun,
|
|
8577
9250
|
logger
|
|
8578
9251
|
});
|
|
@@ -8609,11 +9282,15 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8609
9282
|
return toolFiles.filter((file) => file instanceof ToolCheck).flatMap((toolCheck) => toolCheck.toRulesyncChecks());
|
|
8610
9283
|
}
|
|
8611
9284
|
/**
|
|
8612
|
-
*
|
|
8613
|
-
*
|
|
9285
|
+
* Load check files from a single source-tree's `checks/` subtree.
|
|
9286
|
+
* `sourceTree` is the source tree itself (e.g. `/repo/.rulesync` or
|
|
9287
|
+
* `/repo/.rulesync.local`).
|
|
8614
9288
|
*/
|
|
8615
|
-
async
|
|
8616
|
-
const
|
|
9289
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
9290
|
+
const treeParent = dirname(sourceTree);
|
|
9291
|
+
const treeName = basename(sourceTree);
|
|
9292
|
+
const treeChecksDirPath = join(treeName, CHECKS_FEATURE_SUBDIR);
|
|
9293
|
+
const checksDir = join(sourceTree, CHECKS_FEATURE_SUBDIR);
|
|
8617
9294
|
if (!await directoryExists(checksDir)) {
|
|
8618
9295
|
this.logger.debug(`Rulesync checks directory not found: ${checksDir}`);
|
|
8619
9296
|
return [];
|
|
@@ -8629,7 +9306,8 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8629
9306
|
const filepath = join(checksDir, mdFile);
|
|
8630
9307
|
try {
|
|
8631
9308
|
const rulesyncCheck = await RulesyncCheck.fromFile({
|
|
8632
|
-
outputRoot:
|
|
9309
|
+
outputRoot: treeParent,
|
|
9310
|
+
relativeDirPath: treeChecksDirPath,
|
|
8633
9311
|
relativeFilePath: mdFile,
|
|
8634
9312
|
validate: true
|
|
8635
9313
|
});
|
|
@@ -8640,6 +9318,21 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8640
9318
|
continue;
|
|
8641
9319
|
}
|
|
8642
9320
|
}
|
|
9321
|
+
return rulesyncChecks;
|
|
9322
|
+
}
|
|
9323
|
+
/**
|
|
9324
|
+
* Implementation of abstract method from Processor
|
|
9325
|
+
* Load and parse rulesync check files from every configured input root's
|
|
9326
|
+
* `.rulesync/checks/` directory, merging by relative file path so a check
|
|
9327
|
+
* from a later root replaces the earlier root's copy.
|
|
9328
|
+
*/
|
|
9329
|
+
async loadRulesyncFiles() {
|
|
9330
|
+
const rulesyncChecks = mergeByCaseInsensitiveIdentity({
|
|
9331
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
9332
|
+
identity: (check) => check.getRelativeFilePath(),
|
|
9333
|
+
artifactName: "check",
|
|
9334
|
+
logger: this.logger
|
|
9335
|
+
});
|
|
8643
9336
|
this.logger.debug(`Successfully loaded ${rulesyncChecks.length} rulesync checks`);
|
|
8644
9337
|
return rulesyncChecks;
|
|
8645
9338
|
}
|
|
@@ -10118,18 +10811,23 @@ function commandSlug(relativeFilePath) {
|
|
|
10118
10811
|
return basename(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
10119
10812
|
}
|
|
10120
10813
|
/**
|
|
10121
|
-
* Whether a rulesync command exists whose slug matches `dirName
|
|
10814
|
+
* Whether a rulesync command exists whose slug matches `dirName` in any of
|
|
10815
|
+
* the configured input roots.
|
|
10816
|
+
*
|
|
10817
|
+
* `inputRoots[i]` is a source tree itself (e.g. `/repo/.rulesync` or
|
|
10818
|
+
* `/repo/.rulesync.local`), so commands live directly under
|
|
10819
|
+
* `<sourceTree>/commands/`.
|
|
10122
10820
|
*
|
|
10123
10821
|
* Used by the skills-surface `isDirOwned` hooks of tools whose commands are
|
|
10124
10822
|
* emitted as `<slug>/SKILL.md` into the skills tree: a directory matching a
|
|
10125
10823
|
* current command slug is owned by the commands feature, so the skills
|
|
10126
10824
|
* feature must neither import it as a skill nor delete it as an orphan
|
|
10127
|
-
* skill. Once the command is removed from
|
|
10128
|
-
* directory stops matching and the skills feature cleans
|
|
10129
|
-
* orphan.
|
|
10825
|
+
* skill. Once the command is removed from every source tree's `commands/`
|
|
10826
|
+
* directory, the directory stops matching and the skills feature cleans
|
|
10827
|
+
* it up as a regular orphan.
|
|
10130
10828
|
*/
|
|
10131
|
-
async function rulesyncCommandSlugExists({
|
|
10132
|
-
return (await findFilesByGlobs(join(
|
|
10829
|
+
async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
|
|
10830
|
+
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs(join(root, COMMANDS_FEATURE_SUBDIR, "**", "*.md"))))).flat().some((filePath) => commandSlug(basename(filePath)) === dirName);
|
|
10133
10831
|
}
|
|
10134
10832
|
//#endregion
|
|
10135
10833
|
//#region src/features/commands/devin-command.ts
|
|
@@ -10819,11 +11517,10 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
10819
11517
|
* output is lost — so this warns rather than failing the run the way the
|
|
10820
11518
|
* Hermes check does, where the two surfaces really do write the same path.
|
|
10821
11519
|
*/
|
|
10822
|
-
static async validateRulesyncCommands({
|
|
11520
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands, logger }) {
|
|
10823
11521
|
const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
|
|
10824
11522
|
if (commandNames.size === 0) return;
|
|
10825
|
-
const
|
|
10826
|
-
const shadowed = (await findFilesByGlobs(join(skillsRoot, "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
|
|
11523
|
+
const shadowed = (await Promise.all(inputRoots.map((root) => findFilesByGlobs(join(root, SKILLS_FEATURE_SUBDIR, "**", "SKILL.md"))))).flat().map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
|
|
10827
11524
|
if (shadowed.length > 0) logger.warn(`Grok CLI resolves skills before commands, so these skills shadow the same-named commands, which will never be reachable: ${[...new Set(shadowed)].toSorted().join(", ")}. Rename either side to make both invocable.`);
|
|
10828
11525
|
}
|
|
10829
11526
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
@@ -11595,7 +12292,7 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11595
12292
|
static getExtraSharedWritePaths() {
|
|
11596
12293
|
return getHermesagentSharedConfigWritePaths();
|
|
11597
12294
|
}
|
|
11598
|
-
static async validateRulesyncCommands({
|
|
12295
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands }) {
|
|
11599
12296
|
const commandSlugs = /* @__PURE__ */ new Set();
|
|
11600
12297
|
const commandOrigins = /* @__PURE__ */ new Map();
|
|
11601
12298
|
for (const command of rulesyncCommands.filter((candidate) => this.isTargetedByRulesyncCommand(candidate))) {
|
|
@@ -11607,13 +12304,25 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11607
12304
|
commandOrigins.set(slug, origin);
|
|
11608
12305
|
commandSlugs.add(slug);
|
|
11609
12306
|
}
|
|
11610
|
-
const
|
|
11611
|
-
const
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
12307
|
+
const skillsByName = /* @__PURE__ */ new Map();
|
|
12308
|
+
for (const rootPath of inputRoots) {
|
|
12309
|
+
const skillsRoot = join(rootPath, SKILLS_FEATURE_SUBDIR);
|
|
12310
|
+
const skillFiles = await findFilesByGlobs(join(skillsRoot, "**", "SKILL.md"));
|
|
12311
|
+
const loaded = await Promise.all(skillFiles.map(async (filePath) => {
|
|
12312
|
+
const dirName = toPosixPath(relative(skillsRoot, dirname(filePath)));
|
|
12313
|
+
return {
|
|
12314
|
+
rulesyncSkill: await RulesyncSkill.fromDir({
|
|
12315
|
+
outputRoot: rootPath,
|
|
12316
|
+
relativeDirPath: SKILLS_FEATURE_SUBDIR,
|
|
12317
|
+
dirName
|
|
12318
|
+
}),
|
|
12319
|
+
dirName,
|
|
12320
|
+
rootPath
|
|
12321
|
+
};
|
|
12322
|
+
}));
|
|
12323
|
+
for (const entry of loaded) skillsByName.set(caseFoldIdentity(entry.dirName), entry);
|
|
12324
|
+
}
|
|
12325
|
+
const collisions = [...skillsByName.values()].map(({ rulesyncSkill }) => rulesyncSkill).filter((skill) => HermesagentSkill.isTargetedByRulesyncSkill(skill)).map((skill) => hermesSlashName(skill.getFrontmatter().name)).filter((slug) => commandSlugs.has(slug));
|
|
11617
12326
|
if (collisions.length > 0) throw new Error(`Hermes command and skill slash-name collision: ${[...new Set(collisions)].toSorted().join(", ")}`);
|
|
11618
12327
|
}
|
|
11619
12328
|
static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
|
|
@@ -11746,6 +12455,8 @@ const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
|
|
|
11746
12455
|
const JUNIE_IGNORE_FILE_NAME = ".aiignore";
|
|
11747
12456
|
const JUNIE_RULE_FILE_NAME = "AGENTS.md";
|
|
11748
12457
|
const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
|
|
12458
|
+
const JUNIE_RULES_DIR_NAME = "rules";
|
|
12459
|
+
const JUNIE_PLAYBOOK_FILE_NAME = "playbook.md";
|
|
11749
12460
|
//#endregion
|
|
11750
12461
|
//#region src/features/commands/junie-command.ts
|
|
11751
12462
|
const JunieCommandFrontmatterSchema = z.looseObject({ description: z.optional(z.string()) });
|
|
@@ -13706,10 +14417,10 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13706
14417
|
global;
|
|
13707
14418
|
getFactory;
|
|
13708
14419
|
flattenedCommandNaming;
|
|
13709
|
-
constructor({ outputRoot = process.cwd(),
|
|
14420
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$5, dryRun = false, flattenedCommandNaming = "basename", logger }) {
|
|
13710
14421
|
super({
|
|
13711
14422
|
outputRoot,
|
|
13712
|
-
|
|
14423
|
+
inputRoots,
|
|
13713
14424
|
dryRun,
|
|
13714
14425
|
logger
|
|
13715
14426
|
});
|
|
@@ -13724,7 +14435,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13724
14435
|
const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
|
|
13725
14436
|
const factory = this.getFactory(this.toolTarget);
|
|
13726
14437
|
await factory.class.validateRulesyncCommands?.({
|
|
13727
|
-
|
|
14438
|
+
inputRoots: this.inputRoots,
|
|
13728
14439
|
rulesyncCommands,
|
|
13729
14440
|
logger: this.logger
|
|
13730
14441
|
});
|
|
@@ -13776,16 +14487,36 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13776
14487
|
return rel;
|
|
13777
14488
|
}
|
|
13778
14489
|
/**
|
|
13779
|
-
*
|
|
13780
|
-
*
|
|
14490
|
+
* Load rulesync command files from a single source-tree's `commands/`
|
|
14491
|
+
* subtree. `sourceTree` is the source tree itself (e.g.
|
|
14492
|
+
* `/repo/.rulesync` or `/repo/.rulesync.local`).
|
|
13781
14493
|
*/
|
|
13782
|
-
async
|
|
13783
|
-
const
|
|
14494
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
14495
|
+
const treeParent = dirname(sourceTree);
|
|
14496
|
+
const treeName = basename(sourceTree);
|
|
14497
|
+
const treeCommandsDirPath = join(treeName, COMMANDS_FEATURE_SUBDIR);
|
|
14498
|
+
const basePath = join(sourceTree, COMMANDS_FEATURE_SUBDIR);
|
|
13784
14499
|
const rulesyncCommandPaths = await findFilesByGlobs(join(basePath, "**", "*.md"));
|
|
13785
|
-
|
|
13786
|
-
outputRoot:
|
|
14500
|
+
return await Promise.all(rulesyncCommandPaths.map((path) => RulesyncCommand.fromFile({
|
|
14501
|
+
outputRoot: treeParent,
|
|
14502
|
+
relativeDirPath: treeCommandsDirPath,
|
|
13787
14503
|
relativeFilePath: this.safeRelativePath(basePath, path)
|
|
13788
14504
|
})));
|
|
14505
|
+
}
|
|
14506
|
+
/**
|
|
14507
|
+
* Implementation of abstract method from FeatureProcessor
|
|
14508
|
+
* Load and parse rulesync command files from every configured input root's
|
|
14509
|
+
* `.rulesync/commands/` directory, merging by relative path so a command
|
|
14510
|
+
* with the same target path from a later root replaces the earlier root's
|
|
14511
|
+
* copy.
|
|
14512
|
+
*/
|
|
14513
|
+
async loadRulesyncFiles() {
|
|
14514
|
+
const rulesyncCommands = mergeByCaseInsensitiveIdentity({
|
|
14515
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
14516
|
+
identity: (command) => command.getRelativeFilePath(),
|
|
14517
|
+
artifactName: "command",
|
|
14518
|
+
logger: this.logger
|
|
14519
|
+
});
|
|
13789
14520
|
this.logger.debug(`Successfully loaded ${rulesyncCommands.length} rulesync commands`);
|
|
13790
14521
|
return rulesyncCommands;
|
|
13791
14522
|
}
|
|
@@ -19425,6 +20156,7 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
19425
20156
|
const entry = raw;
|
|
19426
20157
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
19427
20158
|
if (vibeEvent === void 0) return null;
|
|
20159
|
+
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
19428
20160
|
const canonicalEvent = VIBE_TO_CANONICAL_EVENT_NAMES[vibeEvent] ?? vibeEvent;
|
|
19429
20161
|
const def = { type: "command" };
|
|
19430
20162
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
@@ -19973,10 +20705,10 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
19973
20705
|
var HooksProcessor = class extends FeatureProcessor {
|
|
19974
20706
|
toolTarget;
|
|
19975
20707
|
global;
|
|
19976
|
-
constructor({ outputRoot = process.cwd(),
|
|
20708
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
19977
20709
|
super({
|
|
19978
20710
|
outputRoot,
|
|
19979
|
-
|
|
20711
|
+
inputRoots,
|
|
19980
20712
|
dryRun,
|
|
19981
20713
|
logger
|
|
19982
20714
|
});
|
|
@@ -19986,9 +20718,17 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
19986
20718
|
this.global = global;
|
|
19987
20719
|
}
|
|
19988
20720
|
async loadRulesyncFiles() {
|
|
20721
|
+
const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncHooks.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
|
|
20722
|
+
const sourceTree = await pickLastRootWithFile({
|
|
20723
|
+
inputRoots: this.inputRoots,
|
|
20724
|
+
relativePaths,
|
|
20725
|
+
logger: this.logger,
|
|
20726
|
+
artifactName: "The hooks file"
|
|
20727
|
+
}) ?? this.inputRoots[0];
|
|
19989
20728
|
try {
|
|
19990
20729
|
return [await RulesyncHooks.fromFile({
|
|
19991
|
-
outputRoot:
|
|
20730
|
+
outputRoot: dirname(sourceTree),
|
|
20731
|
+
relativeDirPath: basename(sourceTree),
|
|
19992
20732
|
validate: true
|
|
19993
20733
|
})];
|
|
19994
20734
|
} catch (error) {
|
|
@@ -21530,10 +22270,10 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21530
22270
|
getFactory;
|
|
21531
22271
|
featureOptions;
|
|
21532
22272
|
global;
|
|
21533
|
-
constructor({ outputRoot = process.cwd(),
|
|
22273
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$4, global = false, dryRun = false, logger, featureOptions }) {
|
|
21534
22274
|
super({
|
|
21535
22275
|
outputRoot,
|
|
21536
|
-
|
|
22276
|
+
inputRoots,
|
|
21537
22277
|
dryRun,
|
|
21538
22278
|
logger
|
|
21539
22279
|
});
|
|
@@ -21550,11 +22290,35 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21550
22290
|
}
|
|
21551
22291
|
/**
|
|
21552
22292
|
* Implementation of abstract method from FeatureProcessor
|
|
21553
|
-
*
|
|
22293
|
+
*
|
|
22294
|
+
* Load and parse the rulesync ignore file. `inputRoots[i]` is a source
|
|
22295
|
+
* tree itself (e.g. `/repo/.rulesync.local`); the recommended `.aiignore`
|
|
22296
|
+
* lives directly inside it. The legacy `.rulesyncignore` is shared at the
|
|
22297
|
+
* project root, so it is intentionally not considered when choosing which
|
|
22298
|
+
* source tree wins; `RulesyncIgnore.fromFile` still uses it as a fallback
|
|
22299
|
+
* for the chosen tree.
|
|
22300
|
+
*
|
|
22301
|
+
* When multiple input roots are configured, the last root that provides
|
|
22302
|
+
* an ignore file wins entirely (whole-file replacement — no line-level
|
|
22303
|
+
* merge in this slice; see the "Deliberately out of scope" section of
|
|
22304
|
+
* the inputRoots plan for context). If no root has the file, fall back
|
|
22305
|
+
* to the primary root's path so the underlying `RulesyncIgnore.fromFile`
|
|
22306
|
+
* surfaces the same missing-file error it would in the single-root case.
|
|
21554
22307
|
*/
|
|
21555
22308
|
async loadRulesyncFiles() {
|
|
22309
|
+
const paths = RulesyncIgnore.getSettablePaths();
|
|
22310
|
+
const relativePaths = getRulesyncSourceCandidates({ paths }).filter((candidate) => candidate.relativeDirPath === paths.recommended.relativeDirPath).map((candidate) => candidate.relativeFilePath);
|
|
22311
|
+
const sourceTree = await pickLastRootWithFile({
|
|
22312
|
+
inputRoots: this.inputRoots,
|
|
22313
|
+
relativePaths,
|
|
22314
|
+
logger: this.logger,
|
|
22315
|
+
artifactName: "The ignore file (.aiignore)"
|
|
22316
|
+
}) ?? this.inputRoots[0];
|
|
21556
22317
|
try {
|
|
21557
|
-
return [await RulesyncIgnore.fromFile({
|
|
22318
|
+
return [await RulesyncIgnore.fromFile({
|
|
22319
|
+
outputRoot: dirname(sourceTree),
|
|
22320
|
+
relativeDirPath: basename(sourceTree)
|
|
22321
|
+
})];
|
|
21558
22322
|
} catch (error) {
|
|
21559
22323
|
this.logger.error(`Failed to load rulesync ignore file (${RULESYNC_AIIGNORE_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
21560
22324
|
return [];
|
|
@@ -26589,11 +27353,86 @@ function disabledNamesOf(config) {
|
|
|
26589
27353
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
26590
27354
|
}
|
|
26591
27355
|
/**
|
|
27356
|
+
* The value `mcp.mcpConfigPath` needs so Rovo Dev reads the project-scope
|
|
27357
|
+
* `.rovodev/mcp.json`. A config-file value, not a filesystem path, so it is
|
|
27358
|
+
* always POSIX-separated.
|
|
27359
|
+
*/
|
|
27360
|
+
const ROVODEV_PROJECT_MCP_CONFIG_POINTER = posix.join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME);
|
|
27361
|
+
/**
|
|
27362
|
+
* The keys that make an entry in `mcp.json` something Rovo Dev can start:
|
|
27363
|
+
* a local process, or a remote endpoint under either spelling the canonical
|
|
27364
|
+
* config accepts.
|
|
27365
|
+
*/
|
|
27366
|
+
const MCP_SERVER_ENDPOINT_KEYS = [
|
|
27367
|
+
"command",
|
|
27368
|
+
"url",
|
|
27369
|
+
"httpUrl"
|
|
27370
|
+
];
|
|
27371
|
+
function normalizeMcpConfigPathValue(value) {
|
|
27372
|
+
return toPosixPath(value).replace(/^\.\//, "");
|
|
27373
|
+
}
|
|
27374
|
+
/**
|
|
27375
|
+
* Point `mcp.mcpConfigPath` at the project-scope `mcp.json` rulesync writes,
|
|
27376
|
+
* and report whether the block gained a value it did not already carry.
|
|
27377
|
+
*
|
|
27378
|
+
* Rovo Dev's `mcpConfigPath` defaults to a file under the user's home
|
|
27379
|
+
* directory, so a repo-committed `.rovodev/mcp.json` is inert until the active
|
|
27380
|
+
* config points at it — the Bitbucket Agentic Pipelines guide documents
|
|
27381
|
+
* registering the server and setting the pointer as two required steps. Global
|
|
27382
|
+
* scope is left alone: there the default already resolves to the file rulesync
|
|
27383
|
+
* writes.
|
|
27384
|
+
*
|
|
27385
|
+
* The pointer names one config rather than merging with the default, so it is
|
|
27386
|
+
* written only when this project actually has a Rovo Dev server to run — a
|
|
27387
|
+
* server that targets `rovodev` and is not disabled. Otherwise `mcp.json` is
|
|
27388
|
+
* generated empty, and pointing at it would take away the user's global
|
|
27389
|
+
* servers for this repository in exchange for nothing.
|
|
27390
|
+
*
|
|
27391
|
+
* That condition can also stop holding after the fact, once the last server is
|
|
27392
|
+
* removed from the canonical config or switched off. Rulesync does not take
|
|
27393
|
+
* the pointer back out — it cannot tell its own past value from a user who
|
|
27394
|
+
* typed the same string — but it says so, since the file is now a live setting
|
|
27395
|
+
* that resolves to nothing.
|
|
27396
|
+
*
|
|
27397
|
+
* A pointer the user aimed somewhere else is theirs, not ours: overwriting it
|
|
27398
|
+
* would silently redirect Rovo Dev away from a file they chose. It is named in
|
|
27399
|
+
* a warning instead, because the generated `mcp.json` is unread while it
|
|
27400
|
+
* stands.
|
|
27401
|
+
*
|
|
27402
|
+
* Whatever the outcome, it is logged: writing the pointer turns servers that
|
|
27403
|
+
* were generated-but-never-read into servers Rovo Dev actually spawns, and
|
|
27404
|
+
* points it away from the global MCP config, so it is not something to do
|
|
27405
|
+
* quietly.
|
|
27406
|
+
*
|
|
27407
|
+
* @see https://support.atlassian.com/bitbucket-cloud/docs/rovo-dev-advanced-agentic-configuration/
|
|
27408
|
+
* @see https://support.atlassian.com/rovo/docs/manage-rovo-dev-cli-settings/
|
|
27409
|
+
*/
|
|
27410
|
+
function applyProjectMcpConfigPointer({ existingMcp, global, hasLiveServers, logger }) {
|
|
27411
|
+
if (global) return false;
|
|
27412
|
+
const existing = existingMcp.mcpConfigPath;
|
|
27413
|
+
const pointsAtGeneratedFile = typeof existing === "string" && normalizeMcpConfigPathValue(existing) === ROVODEV_PROJECT_MCP_CONFIG_POINTER;
|
|
27414
|
+
if (!hasLiveServers) {
|
|
27415
|
+
if (pointsAtGeneratedFile) logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} points at ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which now has no enabled server. Rovo Dev reads that file instead of the global MCP config, so this project has no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to the global config.`);
|
|
27416
|
+
return false;
|
|
27417
|
+
}
|
|
27418
|
+
if (existing === void 0) {
|
|
27419
|
+
existingMcp.mcpConfigPath = ROVODEV_PROJECT_MCP_CONFIG_POINTER;
|
|
27420
|
+
logger?.info(`Rovo Dev MCP: setting mcp.mcpConfigPath to "${ROVODEV_PROJECT_MCP_CONFIG_POINTER}" in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Rovo Dev will now launch the servers in ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} for this project instead of the ones in the default global MCP config.`);
|
|
27421
|
+
return true;
|
|
27422
|
+
}
|
|
27423
|
+
if (pointsAtGeneratedFile) return false;
|
|
27424
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${JSON.stringify(existing)} in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Rovo Dev reads MCP servers from that path, so the generated ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} is unused until it is set to "${ROVODEV_PROJECT_MCP_CONFIG_POINTER}".`);
|
|
27425
|
+
return false;
|
|
27426
|
+
}
|
|
27427
|
+
/**
|
|
26592
27428
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
26593
27429
|
* `~/.rovodev/config.yml` (global). Carries `disabledMcpServers` — the key
|
|
26594
|
-
* Rovo Dev actually consults to switch a server off —
|
|
26595
|
-
*
|
|
26596
|
-
*
|
|
27430
|
+
* Rovo Dev actually consults to switch a server off — plus `mcpConfigPath`,
|
|
27431
|
+
* which rulesync authors in project scope when the key is absent and this
|
|
27432
|
+
* project has a server to run (see `applyProjectMcpConfigPointer`). The block
|
|
27433
|
+
* is recomputed from the existing one, so user keys (`allowedMcpServers`,
|
|
27434
|
+
* ...), a `mcpConfigPath` the user aimed elsewhere, and disabled names for
|
|
27435
|
+
* servers rulesync does not manage all survive.
|
|
26597
27436
|
*/
|
|
26598
27437
|
var RovodevMcpConfigYaml = class extends ToolFile {
|
|
26599
27438
|
isDeletable() {
|
|
@@ -26726,7 +27565,17 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
26726
27565
|
const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
|
|
26727
27566
|
if (mergedDisabled.length > 0) existingMcp.disabledMcpServers = mergedDisabled;
|
|
26728
27567
|
else delete existingMcp.disabledMcpServers;
|
|
26729
|
-
|
|
27568
|
+
const wrotePointer = applyProjectMcpConfigPointer({
|
|
27569
|
+
existingMcp,
|
|
27570
|
+
global,
|
|
27571
|
+
hasLiveServers: managedNames.filter((name) => {
|
|
27572
|
+
if (disabledNames.includes(name)) return false;
|
|
27573
|
+
const server = servers[name];
|
|
27574
|
+
return isRecord$1(server) && MCP_SERVER_ENDPOINT_KEYS.some((endpointKey) => server[endpointKey] !== void 0);
|
|
27575
|
+
}).length > 0,
|
|
27576
|
+
logger
|
|
27577
|
+
});
|
|
27578
|
+
if (mergedDisabled.length === 0 && !wrotePointer && existingContent.trim() === "") return [];
|
|
26730
27579
|
const fileContent = applySharedConfigPatch({
|
|
26731
27580
|
fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
|
|
26732
27581
|
feature: "mcp",
|
|
@@ -27819,10 +28668,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27819
28668
|
toolTarget;
|
|
27820
28669
|
global;
|
|
27821
28670
|
getFactory;
|
|
27822
|
-
constructor({ outputRoot = process.cwd(),
|
|
28671
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$3, dryRun = false, logger }) {
|
|
27823
28672
|
super({
|
|
27824
28673
|
outputRoot,
|
|
27825
|
-
|
|
28674
|
+
inputRoots,
|
|
27826
28675
|
dryRun,
|
|
27827
28676
|
logger
|
|
27828
28677
|
});
|
|
@@ -27838,7 +28687,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27838
28687
|
*/
|
|
27839
28688
|
async loadRulesyncFiles() {
|
|
27840
28689
|
try {
|
|
27841
|
-
return [await RulesyncMcp.
|
|
28690
|
+
return [await RulesyncMcp.fromRoots({
|
|
28691
|
+
inputRoots: this.inputRoots,
|
|
28692
|
+
logger: this.logger
|
|
28693
|
+
})];
|
|
27842
28694
|
} catch (error) {
|
|
27843
28695
|
this.logger.error(`Failed to load a Rulesync MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
27844
28696
|
return [];
|
|
@@ -29464,8 +30316,9 @@ function deepMergeRecords(base, patch) {
|
|
|
29464
30316
|
* emits them only under `--global`.
|
|
29465
30317
|
*
|
|
29466
30318
|
* Deliberately NOT listed:
|
|
29467
|
-
* - `bwrapPath` / `socatPath`:
|
|
29468
|
-
*
|
|
30319
|
+
* - `ripgrep` / `bwrapPath` / `socatPath`: each names an executable, so
|
|
30320
|
+
* `CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL` refuses them in both scopes
|
|
30321
|
+
* rather than emitting them under `--global`.
|
|
29469
30322
|
* - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
|
|
29470
30323
|
* unit is the individual entry's mode, not the settings key, and the same
|
|
29471
30324
|
* lists carry `deny` entries that project settings *do* honor — dropping a
|
|
@@ -29473,7 +30326,6 @@ function deepMergeRecords(base, patch) {
|
|
|
29473
30326
|
* filters those lists per entry instead.
|
|
29474
30327
|
*
|
|
29475
30328
|
* @see https://code.claude.com/docs/en/sandboxing
|
|
29476
|
-
* @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
|
|
29477
30329
|
*/
|
|
29478
30330
|
const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
29479
30331
|
["filesystem", "disabled"],
|
|
@@ -29482,37 +30334,309 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
|
29482
30334
|
["credentials", "allowPlaintextInject"],
|
|
29483
30335
|
["credentials", "awsPairs"],
|
|
29484
30336
|
["credentials", "sigv4"],
|
|
29485
|
-
["allowAppleEvents"]
|
|
29486
|
-
["ripgrep"]
|
|
30337
|
+
["allowAppleEvents"]
|
|
29487
30338
|
];
|
|
29488
30339
|
/**
|
|
29489
|
-
*
|
|
29490
|
-
*
|
|
29491
|
-
*
|
|
29492
|
-
*
|
|
30340
|
+
* `sandbox.*` paths documented with a `Managed` scope: Claude Code reads them
|
|
30341
|
+
* only from the settings file an organization deploys. Neither file rulesync
|
|
30342
|
+
* writes is that file, so they are dropped in **both** scopes — the `sandbox`
|
|
30343
|
+
* counterpart of {@link CLAUDECODE_UNHONORED_KEY_SOURCES}, which does the same
|
|
30344
|
+
* for top-level `Managed` keys.
|
|
30345
|
+
*
|
|
30346
|
+
* Both only ever *narrow* the policy — they stop a lower-scoped file from
|
|
30347
|
+
* re-opening what managed settings blocked — so neither is trust-widening. They
|
|
30348
|
+
* are dropped rather than written for the opposite reason: written into a
|
|
30349
|
+
* project or user file they do nothing at all, and a `sandbox` block that reads
|
|
30350
|
+
* as though it locked the policy to managed values while Claude Code ignores it
|
|
30351
|
+
* is the more dangerous of the two failure modes.
|
|
30352
|
+
*
|
|
30353
|
+
* Import keeps them, unlike the command-executing paths: the value in an
|
|
30354
|
+
* existing `settings.json` was hand-written to be honored somewhere, and
|
|
30355
|
+
* round-tripping it preserves the author's intent for the day it moves into a
|
|
30356
|
+
* managed file. The cost is a warning on every generate until it is removed,
|
|
30357
|
+
* which the refusal message points at.
|
|
30358
|
+
*
|
|
30359
|
+
* @see https://code.claude.com/docs/en/settings-reference#sandbox-filesystem-allowmanagedreadpathsonly
|
|
30360
|
+
* — "Scope: `Managed`"; the `network` entry says the same.
|
|
30361
|
+
*/
|
|
30362
|
+
const CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS = [["filesystem", "allowManagedReadPathsOnly"], ["network", "allowManagedDomainsOnly"]];
|
|
30363
|
+
/**
|
|
30364
|
+
* Walks `segments` from `root`, returning the record they name or `undefined` if
|
|
30365
|
+
* any step is missing or not a record. Shared by everything below that addresses
|
|
30366
|
+
* a `sandbox` path, so a nested path added to one of the tables is actually
|
|
30367
|
+
* traversed rather than silently skipped.
|
|
30368
|
+
*/
|
|
30369
|
+
function resolveSandboxParent({ root, segments }) {
|
|
30370
|
+
let parent = root;
|
|
30371
|
+
for (const segment of segments) {
|
|
30372
|
+
const next = parent[segment];
|
|
30373
|
+
if (!isPlainRecord(next)) return void 0;
|
|
30374
|
+
parent = next;
|
|
30375
|
+
}
|
|
30376
|
+
return parent;
|
|
30377
|
+
}
|
|
30378
|
+
/**
|
|
30379
|
+
* Deletes `path` from `target` in place and reports whether anything was there,
|
|
30380
|
+
* dropping a container the removal emptied so no `"network": {}` noise is left
|
|
30381
|
+
* behind.
|
|
30382
|
+
*/
|
|
30383
|
+
function deleteSandboxPath({ target, path }) {
|
|
30384
|
+
const leaf = path.at(-1);
|
|
30385
|
+
if (leaf === void 0) return false;
|
|
30386
|
+
const parentPath = path.slice(0, -1);
|
|
30387
|
+
const parent = resolveSandboxParent({
|
|
30388
|
+
root: target,
|
|
30389
|
+
segments: parentPath
|
|
30390
|
+
});
|
|
30391
|
+
if (parent === void 0 || parent[leaf] === void 0) return false;
|
|
30392
|
+
delete parent[leaf];
|
|
30393
|
+
for (let depth = parentPath.length; depth > 0; depth--) {
|
|
30394
|
+
const container = resolveSandboxParent({
|
|
30395
|
+
root: target,
|
|
30396
|
+
segments: parentPath.slice(0, depth)
|
|
30397
|
+
});
|
|
30398
|
+
if (container === void 0 || Object.keys(container).length > 0) break;
|
|
30399
|
+
const holder = resolveSandboxParent({
|
|
30400
|
+
root: target,
|
|
30401
|
+
segments: parentPath.slice(0, depth - 1)
|
|
30402
|
+
});
|
|
30403
|
+
const name = parentPath[depth - 1];
|
|
30404
|
+
if (holder === void 0 || name === void 0) break;
|
|
30405
|
+
delete holder[name];
|
|
30406
|
+
}
|
|
30407
|
+
return true;
|
|
30408
|
+
}
|
|
30409
|
+
/**
|
|
30410
|
+
* The one warning that names every trust-affecting setting this generate wrote
|
|
30411
|
+
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
30412
|
+
* matter, but the "review this as you would a hook" framing only needs saying
|
|
30413
|
+
* once, and repeating it per key buries the reasons in boilerplate.
|
|
29493
30414
|
*/
|
|
29494
|
-
function
|
|
29495
|
-
|
|
29496
|
-
|
|
30415
|
+
function warnOnTrustAffectingEntries({ entries, relativeFilePath, logger }) {
|
|
30416
|
+
if (entries.length === 0) return;
|
|
30417
|
+
const one = entries.length === 1;
|
|
30418
|
+
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
30419
|
+
logger?.warn(`Claude Code permissions: writing ${entries.length} trust-affecting ${one ? "setting" : "settings"} to ${relativeFilePath}; review ${one ? "it" : "them"} as you would a hook, especially if this permissions file came from 'rulesync fetch'. ${details}.`);
|
|
30420
|
+
}
|
|
30421
|
+
/**
|
|
30422
|
+
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
30423
|
+
* than the default. `plan` and `default` are absent because they do not widen
|
|
30424
|
+
* anything.
|
|
30425
|
+
*/
|
|
30426
|
+
const CLAUDECODE_WIDENING_DEFAULT_MODES = {
|
|
30427
|
+
acceptEdits: "every file edit is then applied without a prompt",
|
|
30428
|
+
auto: "shell commands are then auto-approved by a classifier rather than by you",
|
|
30429
|
+
bypassPermissions: "every session then starts with no permission prompts at all"
|
|
30430
|
+
};
|
|
30431
|
+
/**
|
|
30432
|
+
* The `permissions` fields that widen rather than restrict: a `defaultMode` that
|
|
30433
|
+
* removes prompts, and `additionalDirectories`, which moves the
|
|
30434
|
+
* working-directory boundary. Reported for the same reason `disableAllHooks` is:
|
|
30435
|
+
* a shareable permissions file should not loosen the permission system quietly.
|
|
30436
|
+
*/
|
|
30437
|
+
function collectWideningPermissionFields({ fields }) {
|
|
30438
|
+
const entries = [];
|
|
30439
|
+
const defaultMode = fields.defaultMode;
|
|
30440
|
+
if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, defaultMode)) entries.push({
|
|
30441
|
+
label: `permissions.defaultMode: "${defaultMode}"`,
|
|
30442
|
+
reason: CLAUDECODE_WIDENING_DEFAULT_MODES[defaultMode]
|
|
30443
|
+
});
|
|
30444
|
+
const additionalDirectories = fields.additionalDirectories;
|
|
30445
|
+
if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0)) entries.push({
|
|
30446
|
+
label: "permissions.additionalDirectories",
|
|
30447
|
+
reason: "moves the boundary of what Claude Code may read and edit outside the project"
|
|
30448
|
+
});
|
|
30449
|
+
return entries;
|
|
30450
|
+
}
|
|
30451
|
+
/**
|
|
30452
|
+
* `sandbox` paths whose value names a binary Claude Code runs. `sandbox` has its
|
|
30453
|
+
* own merge branch, so the top-level refusal in `stripUnhonoredTopLevelKeys`
|
|
30454
|
+
* never sees them — they are refused here on the same grounds, in both scopes:
|
|
30455
|
+
* a fetched `.rulesync/permissions.jsonc` must not be able to point Claude Code
|
|
30456
|
+
* at an executable of its choosing.
|
|
30457
|
+
*
|
|
30458
|
+
* @see https://code.claude.com/docs/en/sandboxing
|
|
30459
|
+
*/
|
|
30460
|
+
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
30461
|
+
["ripgrep"],
|
|
30462
|
+
["bwrapPath"],
|
|
30463
|
+
["socatPath"]
|
|
30464
|
+
];
|
|
30465
|
+
/**
|
|
30466
|
+
* The predicates the "which value actually widens?" tables are built from.
|
|
30467
|
+
* Each names the value that does *not* widen and reports everything else, never
|
|
30468
|
+
* the reverse: the override is authored JSONC, so a key can carry any value at
|
|
30469
|
+
* all, and one Claude Code coerces is still honored. Reporting an off-type value
|
|
30470
|
+
* keeps the warning fail-safe — silence has to mean "this cannot loosen
|
|
30471
|
+
* anything", not "this is not the type the table expected".
|
|
30472
|
+
*/
|
|
30473
|
+
const isNotFalse = (value) => value !== false;
|
|
30474
|
+
const isNotTrue = (value) => value !== true;
|
|
30475
|
+
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
30476
|
+
const isNonEmptyMap = (value) => !isPlainRecord(value) || Object.keys(value).length > 0;
|
|
30477
|
+
/**
|
|
30478
|
+
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
30479
|
+
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
30480
|
+
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
30481
|
+
* too common to refuse — but never silently, because a fetched override should
|
|
30482
|
+
* not be able to open the sandbox without saying so. `widens` keeps the warning
|
|
30483
|
+
* to the value that actually loosens the policy, so authoring the restrictive
|
|
30484
|
+
* value (`allowUnsandboxedCommands: false`, an empty `excludedCommands`) stays
|
|
30485
|
+
* quiet. The `allow*` lists are here for a structural reason: Claude Code merges
|
|
30486
|
+
* a list across every settings scope rather than replacing it, so a project file
|
|
30487
|
+
* can only ever add to them. Their counterparts — `denyRead`, `denyWrite`,
|
|
30488
|
+
* `deniedDomains` — merge the same way, but adding to a deny list only ever
|
|
30489
|
+
* narrows the policy, so they are absent.
|
|
30490
|
+
*
|
|
30491
|
+
* @see https://code.claude.com/docs/en/sandboxing
|
|
30492
|
+
*/
|
|
30493
|
+
const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
30494
|
+
{
|
|
30495
|
+
path: ["allowAppleEvents"],
|
|
30496
|
+
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
30497
|
+
widens: isNotFalse
|
|
30498
|
+
},
|
|
30499
|
+
{
|
|
30500
|
+
path: ["allowUnsandboxedCommands"],
|
|
30501
|
+
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
30502
|
+
widens: isNotFalse
|
|
30503
|
+
},
|
|
30504
|
+
{
|
|
30505
|
+
path: ["autoAllowBashIfSandboxed"],
|
|
30506
|
+
reason: "controls whether every Bash command the sandbox accepts runs without a prompt",
|
|
30507
|
+
widens: isNotFalse
|
|
30508
|
+
},
|
|
30509
|
+
{
|
|
30510
|
+
path: ["enableWeakerNestedSandbox"],
|
|
30511
|
+
reason: "runs the Linux sandbox inside an unprivileged container, which weakens it",
|
|
30512
|
+
widens: isNotFalse
|
|
30513
|
+
},
|
|
30514
|
+
{
|
|
30515
|
+
path: ["enableWeakerNetworkIsolation"],
|
|
30516
|
+
reason: "weakens the sandbox's network isolation on macOS",
|
|
30517
|
+
widens: isNotFalse
|
|
30518
|
+
},
|
|
30519
|
+
{
|
|
30520
|
+
path: ["enabled"],
|
|
30521
|
+
reason: "turns the sandbox on, and sandboxed Bash commands then run without a permission prompt unless `autoAllowBashIfSandboxed` is false",
|
|
30522
|
+
widens: isNotFalse
|
|
30523
|
+
},
|
|
30524
|
+
{
|
|
30525
|
+
path: ["excludedCommands"],
|
|
30526
|
+
reason: "names commands that always run outside the sandbox, with no sandbox policy applied",
|
|
30527
|
+
widens: isNonEmptyList
|
|
30528
|
+
},
|
|
30529
|
+
{
|
|
30530
|
+
path: ["filesystem", "allowRead"],
|
|
30531
|
+
reason: "re-opens reading inside a region the sandbox's `denyRead` blocks",
|
|
30532
|
+
widens: isNonEmptyList
|
|
30533
|
+
},
|
|
30534
|
+
{
|
|
30535
|
+
path: ["filesystem", "allowWrite"],
|
|
30536
|
+
reason: "adds paths sandboxed commands may write to, outside the working directory",
|
|
30537
|
+
widens: isNonEmptyList
|
|
30538
|
+
},
|
|
30539
|
+
{
|
|
30540
|
+
path: ["ignoreViolations"],
|
|
30541
|
+
reason: "hides the sandbox violations it names, so a blocked access stops being reported",
|
|
30542
|
+
widens: (value) => isNonEmptyMap(value) && isNotFalse(value)
|
|
30543
|
+
},
|
|
30544
|
+
{
|
|
30545
|
+
path: ["network", "allowAllUnixSockets"],
|
|
30546
|
+
reason: "lets sandboxed commands connect to every Unix socket",
|
|
30547
|
+
widens: isNotFalse
|
|
30548
|
+
},
|
|
30549
|
+
{
|
|
30550
|
+
path: ["network", "allowedDomains"],
|
|
30551
|
+
reason: "pre-allows domains sandboxed commands may reach without a prompt",
|
|
30552
|
+
widens: isNonEmptyList
|
|
30553
|
+
},
|
|
30554
|
+
{
|
|
30555
|
+
path: ["network", "allowLocalBinding"],
|
|
30556
|
+
reason: "lets sandboxed commands bind local ports",
|
|
30557
|
+
widens: isNotFalse
|
|
30558
|
+
},
|
|
30559
|
+
{
|
|
30560
|
+
path: ["network", "allowMachLookup"],
|
|
30561
|
+
reason: "names the macOS services sandboxed commands may reach, and `*` means every service",
|
|
30562
|
+
widens: isNonEmptyList
|
|
30563
|
+
},
|
|
30564
|
+
{
|
|
30565
|
+
path: ["network", "allowUnixSockets"],
|
|
30566
|
+
reason: "names Unix sockets sandboxed commands may reach, and one such as `/var/run/docker.sock` is host access",
|
|
30567
|
+
widens: isNonEmptyList
|
|
30568
|
+
},
|
|
30569
|
+
{
|
|
30570
|
+
path: ["network", "httpProxyPort"],
|
|
30571
|
+
reason: "routes the sandbox's HTTP traffic through the port it names",
|
|
30572
|
+
widens: () => true
|
|
30573
|
+
},
|
|
30574
|
+
{
|
|
30575
|
+
path: ["network", "socksProxyPort"],
|
|
30576
|
+
reason: "routes the sandbox's SOCKS traffic through the port it names",
|
|
30577
|
+
widens: () => true
|
|
30578
|
+
}
|
|
30579
|
+
];
|
|
30580
|
+
/**
|
|
30581
|
+
* Every authored `sandbox` path that loosens the sandbox. Nothing is removed —
|
|
30582
|
+
* the values are written, just not silently. Called on the filtered `sandbox`
|
|
30583
|
+
* so it never claims to be writing a path the scope filters dropped.
|
|
30584
|
+
*/
|
|
30585
|
+
function collectTrustAffectingSandboxPaths({ sandbox }) {
|
|
30586
|
+
const entries = [];
|
|
30587
|
+
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
29497
30588
|
const leaf = path.at(-1);
|
|
29498
30589
|
if (leaf === void 0) continue;
|
|
29499
|
-
const
|
|
29500
|
-
|
|
29501
|
-
|
|
29502
|
-
|
|
29503
|
-
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
29507
|
-
|
|
29508
|
-
|
|
29509
|
-
|
|
29510
|
-
|
|
29511
|
-
|
|
29512
|
-
|
|
29513
|
-
|
|
29514
|
-
|
|
29515
|
-
|
|
30590
|
+
const parent = resolveSandboxParent({
|
|
30591
|
+
root: sandbox,
|
|
30592
|
+
segments: path.slice(0, -1)
|
|
30593
|
+
});
|
|
30594
|
+
if (parent === void 0) continue;
|
|
30595
|
+
const value = parent[leaf];
|
|
30596
|
+
if (value === void 0 || !widens(value)) continue;
|
|
30597
|
+
entries.push({
|
|
30598
|
+
label: `sandbox.${path.join(".")}`,
|
|
30599
|
+
reason
|
|
30600
|
+
});
|
|
30601
|
+
}
|
|
30602
|
+
return entries;
|
|
30603
|
+
}
|
|
30604
|
+
/** Paths that name an executable Claude Code runs. Refused in both scopes. */
|
|
30605
|
+
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL = {
|
|
30606
|
+
paths: CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS,
|
|
30607
|
+
warn: ({ label, relativeFilePath }) => `Claude Code permissions: '${label}' names an executable Claude Code runs, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; set this path in ${relativeFilePath} by hand.`
|
|
30608
|
+
};
|
|
30609
|
+
/**
|
|
30610
|
+
* Paths Claude Code honors only from managed settings. Refused in both scopes,
|
|
30611
|
+
* like the command-executing paths, because managed settings are not a file
|
|
30612
|
+
* rulesync writes in either of them.
|
|
30613
|
+
*/
|
|
30614
|
+
const CLAUDECODE_MANAGED_ONLY_SANDBOX_REFUSAL = {
|
|
30615
|
+
paths: CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS,
|
|
30616
|
+
warn: ({ label, relativeFilePath }) => `Claude Code permissions: '${label}' is only honored in managed settings, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in the managed settings file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`
|
|
30617
|
+
};
|
|
30618
|
+
/** Paths Claude Code honors only above project scope. Refused at project scope. */
|
|
30619
|
+
const CLAUDECODE_GLOBAL_ONLY_SANDBOX_REFUSAL = {
|
|
30620
|
+
paths: CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS,
|
|
30621
|
+
warn: ({ label, relativeFilePath }) => `Claude Code permissions: '${label}' is only honored in user/managed/--settings settings, so it is not written to the project-scoped ${relativeFilePath}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`
|
|
30622
|
+
};
|
|
30623
|
+
/**
|
|
30624
|
+
* Copy of the authored `sandbox` override with every path of every passed
|
|
30625
|
+
* refusal removed, warning once per dropped path. Only the override copy is
|
|
30626
|
+
* filtered — a value already hand-written in the target file is left untouched,
|
|
30627
|
+
* matching the `qwencode` `security.allowPrivateNetworkHooks` precedent.
|
|
30628
|
+
*/
|
|
30629
|
+
function stripSandboxPaths({ sandbox, refusals, relativeFilePath, logger }) {
|
|
30630
|
+
const filtered = structuredClone(sandbox);
|
|
30631
|
+
for (const { paths, warn } of refusals) for (const path of paths) {
|
|
30632
|
+
if (!deleteSandboxPath({
|
|
30633
|
+
target: filtered,
|
|
30634
|
+
path
|
|
30635
|
+
})) continue;
|
|
30636
|
+
logger?.warn(warn({
|
|
30637
|
+
label: `sandbox.${path.join(".")}`,
|
|
30638
|
+
relativeFilePath
|
|
30639
|
+
}));
|
|
29516
30640
|
}
|
|
29517
30641
|
return filtered;
|
|
29518
30642
|
}
|
|
@@ -29536,7 +30660,7 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
|
|
|
29536
30660
|
* such entries to `deny`), so the "reads as masked but isn't" state this guards
|
|
29537
30661
|
* against cannot slip through a differently-spelled value.
|
|
29538
30662
|
*
|
|
29539
|
-
* Like `
|
|
30663
|
+
* Like `stripSandboxPaths`, only the override copy is filtered — a
|
|
29540
30664
|
* value already in the target file is left untouched, which is why the warning
|
|
29541
30665
|
* points at it.
|
|
29542
30666
|
*
|
|
@@ -29567,6 +30691,271 @@ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
|
|
|
29567
30691
|
else filtered.credentials = filteredCredentials;
|
|
29568
30692
|
return filtered;
|
|
29569
30693
|
}
|
|
30694
|
+
/**
|
|
30695
|
+
* Top-level `.claude/settings.json` keys another feature owns, derived from
|
|
30696
|
+
* {@link SHARED_CONFIG_OWNERSHIP} rather than restated here so a feature that
|
|
30697
|
+
* starts owning a new key is excluded from the passthrough automatically
|
|
30698
|
+
* (today: `hooks`, from the hooks feature). Only `replace-owned-keys` entries
|
|
30699
|
+
* name keys; the `custom` policies on this file (`ignore`, `permissions`) own
|
|
30700
|
+
* entries *inside* `permissions`, which the passthrough excludes wholesale.
|
|
30701
|
+
*/
|
|
30702
|
+
const CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS = Object.entries(SHARED_CONFIG_OWNERSHIP[".claude/settings.json"]?.features ?? {}).flatMap(([feature, policy]) => feature !== "permissions" && policy.kind === "replace-owned-keys" ? policy.ownedKeys : []);
|
|
30703
|
+
/**
|
|
30704
|
+
* Top-level `.claude/settings.json` keys the generic `claudecode` override
|
|
30705
|
+
* passthrough must not carry. `permissions` and `sandbox` have their own merge
|
|
30706
|
+
* branches (the managed `allow`/`ask`/`deny` arrays and the scope filtering
|
|
30707
|
+
* respectively), `permission` is rulesync's own canonical tool-scoped block
|
|
30708
|
+
* rather than a settings key, `$schema` is an editor pointer rather than a
|
|
30709
|
+
* Claude Code setting, and the rest belong to the other features writing this
|
|
30710
|
+
* shared file.
|
|
30711
|
+
*/
|
|
30712
|
+
const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
30713
|
+
"permission",
|
|
30714
|
+
"permissions",
|
|
30715
|
+
"sandbox",
|
|
30716
|
+
"$schema",
|
|
30717
|
+
...CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS
|
|
30718
|
+
]);
|
|
30719
|
+
/**
|
|
30720
|
+
* Top-level settings keys Claude Code reads only from user settings, managed
|
|
30721
|
+
* settings and the `--settings` CLI flag — the same restriction
|
|
30722
|
+
* `CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS` records for the `sandbox` subtree, and
|
|
30723
|
+
* the reason the passthrough drops them at project scope instead of committing
|
|
30724
|
+
* a setting that never applies. `rulesync generate --global` writes the user
|
|
30725
|
+
* settings file, so they are emitted there.
|
|
30726
|
+
*
|
|
30727
|
+
* Derived from the per-key **Scope** column of the settings reference: every
|
|
30728
|
+
* top-level key documented as `User or managed` or `User, local, or managed`.
|
|
30729
|
+
*
|
|
30730
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
30731
|
+
*/
|
|
30732
|
+
const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
30733
|
+
"askUserQuestionTimeout",
|
|
30734
|
+
"autoMode",
|
|
30735
|
+
"dialogExpiry",
|
|
30736
|
+
"enableArtifact",
|
|
30737
|
+
"footerLinksRegexes",
|
|
30738
|
+
"pluginConfigs",
|
|
30739
|
+
"skipAutoPermissionPrompt",
|
|
30740
|
+
"skipDangerousModePermissionPrompt",
|
|
30741
|
+
"spellcheck",
|
|
30742
|
+
"sshConfigs",
|
|
30743
|
+
"syncClaudeAiSkills",
|
|
30744
|
+
"useAutoModeDuringPlan",
|
|
30745
|
+
"vimInsertModeRemaps"
|
|
30746
|
+
]);
|
|
30747
|
+
/**
|
|
30748
|
+
* Top-level settings keys neither file rulesync writes can honor, with the file
|
|
30749
|
+
* that does. `Managed` keys are read only from the settings file an organization
|
|
30750
|
+
* deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
|
|
30751
|
+
* `.claude/settings.json` and `~/.claude/settings.json`, so authoring either
|
|
30752
|
+
* kind through the override would produce a policy that silently never applies.
|
|
30753
|
+
*
|
|
30754
|
+
* Derived from the per-key **Scope** column of the settings reference.
|
|
30755
|
+
*
|
|
30756
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
30757
|
+
*/
|
|
30758
|
+
const CLAUDECODE_UNHONORED_KEY_SOURCES = {
|
|
30759
|
+
allowAllClaudeAiMcps: "managed settings",
|
|
30760
|
+
allowedChannelPlugins: "managed settings",
|
|
30761
|
+
allowManagedHooksOnly: "managed settings",
|
|
30762
|
+
allowManagedMcpServersOnly: "managed settings",
|
|
30763
|
+
allowManagedPermissionRulesOnly: "managed settings",
|
|
30764
|
+
autoConnectIde: "~/.claude.json",
|
|
30765
|
+
autoInstallIdeExtension: "~/.claude.json",
|
|
30766
|
+
blockedMarketplaces: "managed settings",
|
|
30767
|
+
browserExternalPageTools: "managed settings",
|
|
30768
|
+
channelsEnabled: "managed settings",
|
|
30769
|
+
claudeMd: "managed settings",
|
|
30770
|
+
diffTool: "~/.claude.json",
|
|
30771
|
+
disableBrowserExternalNavigation: "managed settings",
|
|
30772
|
+
disableCommandPluginSources: "managed settings",
|
|
30773
|
+
disableMobileSimulatorTools: "managed settings",
|
|
30774
|
+
disableSideloadFlags: "managed settings",
|
|
30775
|
+
externalEditorContext: "~/.claude.json",
|
|
30776
|
+
forceLoginGatewayUrl: "managed settings",
|
|
30777
|
+
forceRemoteSettingsRefresh: "managed settings",
|
|
30778
|
+
parentSettingsBehavior: "managed settings",
|
|
30779
|
+
permissionExplainerEnabled: "~/.claude.json",
|
|
30780
|
+
pluginSuggestionMarketplaces: "managed settings",
|
|
30781
|
+
pluginTrustMessage: "managed settings",
|
|
30782
|
+
requiredMaximumVersion: "managed settings",
|
|
30783
|
+
requiredMinimumVersion: "managed settings",
|
|
30784
|
+
sshHostAllowlist: "managed settings",
|
|
30785
|
+
strictKnownMarketplaces: "managed settings",
|
|
30786
|
+
strictPluginOnlyCustomization: "managed settings",
|
|
30787
|
+
teammateDefaultModel: "~/.claude.json",
|
|
30788
|
+
wslInheritsWindowsSettings: "managed settings"
|
|
30789
|
+
};
|
|
30790
|
+
/**
|
|
30791
|
+
* Top-level settings keys whose value Claude Code **executes**. The generic
|
|
30792
|
+
* passthrough refuses them outright rather than warning: `.rulesync/*` files
|
|
30793
|
+
* are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
|
|
30794
|
+
* straight into a project — and a file named for *restricting* what an agent
|
|
30795
|
+
* may do is not somewhere a reviewer looks for a command to run. Commands
|
|
30796
|
+
* belong in `.rulesync/hooks.jsonc` and `.rulesync/.mcp.json`, which are read
|
|
30797
|
+
* as executable by anyone reviewing them. Set these by hand in the settings
|
|
30798
|
+
* file if you need them.
|
|
30799
|
+
*
|
|
30800
|
+
* The value is the reason, spliced into the warning.
|
|
30801
|
+
*
|
|
30802
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
30803
|
+
*/
|
|
30804
|
+
const CLAUDECODE_COMMAND_EXECUTING_KEYS = {
|
|
30805
|
+
apiKeyHelper: "runs the script it names to mint an API key",
|
|
30806
|
+
awsAuthRefresh: "runs the command it names to refresh AWS credentials",
|
|
30807
|
+
awsCredentialExport: "runs the command it names to export AWS credentials",
|
|
30808
|
+
fileSuggestion: "runs its `command` on every `@` file completion",
|
|
30809
|
+
gcpAuthRefresh: "runs the command it names to refresh Google Cloud credentials",
|
|
30810
|
+
otelHeadersHelper: "runs the script it names to build OpenTelemetry headers",
|
|
30811
|
+
policyHelper: "runs the executable it names to compute the managed settings",
|
|
30812
|
+
processWrapper: "wraps every process Claude Code spawns",
|
|
30813
|
+
statusLine: "runs its `command` on every status-line render",
|
|
30814
|
+
subagentStatusLine: "runs its `command` on every subagent status row"
|
|
30815
|
+
};
|
|
30816
|
+
/**
|
|
30817
|
+
* Top-level settings keys the passthrough does write, but never silently: each
|
|
30818
|
+
* one widens what Claude Code trusts or where it sends data, so a value that
|
|
30819
|
+
* arrived with a fetched `.rulesync/permissions.jsonc` should be looked at
|
|
30820
|
+
* deliberately. Warning on write follows the precedent set for Warp's
|
|
30821
|
+
* `command_denylist`, which also replaces a protection when rulesync writes it.
|
|
30822
|
+
*
|
|
30823
|
+
* The value is the reason, spliced into the warning.
|
|
30824
|
+
*/
|
|
30825
|
+
const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
30826
|
+
agent: "starts every session as the named subagent, with that subagent's prompt, tools and model",
|
|
30827
|
+
allowedHttpHookUrls: "limits which URLs an HTTP hook may target, and an empty list means every URL",
|
|
30828
|
+
allowedMcpServers: "allowlists the MCP servers that may be used, and entries from every settings file merge into one list, so an entry here widens an allowlist deployed elsewhere",
|
|
30829
|
+
autoMode: "auto-approves shell commands with a classifier rather than with a prompt",
|
|
30830
|
+
claudeMdExcludes: "skips the CLAUDE.md files its patterns match, so the instructions a repository relies on can be dropped from every session",
|
|
30831
|
+
companyAnnouncements: "prints the strings it names at startup as your organization's announcement, so a fetched value speaks to the reader with your organization's voice",
|
|
30832
|
+
crossSessionInbound: "decides what a session does with messages arriving from your other Claude Code sessions, and `accept` delivers them straight to Claude",
|
|
30833
|
+
disableAllHooks: "controls whether hooks run at all",
|
|
30834
|
+
disableSkillShellExecution: "re-opens the inline shell commands in a skill or custom command that a user setting had turned off",
|
|
30835
|
+
enableAllProjectMcpServers: "auto-approves every server in the project `.mcp.json`",
|
|
30836
|
+
enabledMcpjsonServers: "auto-approves the named servers in the project `.mcp.json`",
|
|
30837
|
+
enabledPlugins: "enables plugins, which can ship their own hooks",
|
|
30838
|
+
env: "sets environment variables for every process Claude Code spawns, so a value such as `NODE_OPTIONS` or `PATH` runs code and `ANTHROPIC_BASE_URL` redirects every prompt",
|
|
30839
|
+
extraKnownMarketplaces: "registers plugin marketplace sources",
|
|
30840
|
+
httpHookAllowedEnvVars: "controls which environment variables an HTTP hook may put in a request header, credentials included",
|
|
30841
|
+
modelOverrides: "maps the model IDs it names to provider-specific model IDs, so an entry decides which inference profile or deployment every call for that model is routed to",
|
|
30842
|
+
outputStyle: "replaces the system prompt every session runs with",
|
|
30843
|
+
prUrlTemplate: "rewrites the pull-request links Claude Code renders, so they can point at a host of the template's choosing rather than at the reviewed PR",
|
|
30844
|
+
remoteControlAtStartup: "connects Remote Control automatically at session start, and the transcript of a connected session is stored on Anthropic servers to sync it across devices",
|
|
30845
|
+
skipAutoPermissionPrompt: "removes the confirmation shown before auto-approval mode starts",
|
|
30846
|
+
skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check starts",
|
|
30847
|
+
skipWebFetchPreflight: "turns off the WebFetch domain safety check, so WebFetch retrieves any URL without consulting Anthropic's blocklist"
|
|
30848
|
+
};
|
|
30849
|
+
/**
|
|
30850
|
+
* The keys from the table above that only widen at one particular value.
|
|
30851
|
+
* `disableSkillShellExecution: true` turns inline shell execution off, which
|
|
30852
|
+
* restricts; anything else re-opens it, and that is what a fetched override
|
|
30853
|
+
* could use to undo a user setting. The rest default to off, so only a value
|
|
30854
|
+
* other than the one that leaves them off is worth a line — and for the
|
|
30855
|
+
* list-valued and map-valued keys, only a non-empty one, since an empty list or
|
|
30856
|
+
* map excludes, announces and overrides nothing.
|
|
30857
|
+
*/
|
|
30858
|
+
const CLAUDECODE_TRUST_KEY_WIDENING_VALUES = {
|
|
30859
|
+
claudeMdExcludes: isNonEmptyList,
|
|
30860
|
+
companyAnnouncements: isNonEmptyList,
|
|
30861
|
+
crossSessionInbound: (value) => value !== "hold" && value !== "refuse",
|
|
30862
|
+
disableSkillShellExecution: isNotTrue,
|
|
30863
|
+
modelOverrides: isNonEmptyMap,
|
|
30864
|
+
remoteControlAtStartup: isNotFalse,
|
|
30865
|
+
skipWebFetchPreflight: isNotFalse
|
|
30866
|
+
};
|
|
30867
|
+
/**
|
|
30868
|
+
* Top-level keys a project-scoped `.claude/settings.json` honors at one value
|
|
30869
|
+
* but ignores at another — the value-level counterpart of
|
|
30870
|
+
* {@link CLAUDECODE_USER_SCOPE_ONLY_KEYS}, which is scoped per key. The ignored
|
|
30871
|
+
* value is dropped at project scope for the same reason a wholly unhonored key
|
|
30872
|
+
* is: committing it would read as a policy that never applies.
|
|
30873
|
+
*
|
|
30874
|
+
* `remoteControlAtStartup` is the only entry whose honored value can be decided
|
|
30875
|
+
* from the value alone. Claude Code honors a `false` from project or local
|
|
30876
|
+
* settings — a repository may turn auto-connect off for its own checkout — but
|
|
30877
|
+
* ignores a `true`, so that a checked-in file cannot turn Remote Control on for
|
|
30878
|
+
* everyone who opens the repository.
|
|
30879
|
+
*
|
|
30880
|
+
* `crossSessionInbound` is on the same documented list but deliberately absent
|
|
30881
|
+
* here: it is a ladder (`accept` < `hold` < `refuse`) whose project value is
|
|
30882
|
+
* honored only when it is stricter than the one above it, which no per-value
|
|
30883
|
+
* predicate can decide without reading the user's own settings. It is warned
|
|
30884
|
+
* about through {@link CLAUDECODE_TRUST_AFFECTING_KEYS} instead, since under
|
|
30885
|
+
* `--global` its loosening value is honored outright.
|
|
30886
|
+
*
|
|
30887
|
+
* @see https://code.claude.com/docs/en/settings#security-keys-where-the-stricter-value-applies
|
|
30888
|
+
*/
|
|
30889
|
+
const CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES = { remoteControlAtStartup: {
|
|
30890
|
+
ignored: isNotFalse,
|
|
30891
|
+
note: "Claude Code honors only a `false` there, so that a checked-in file cannot turn Remote Control on for everyone who opens the repository"
|
|
30892
|
+
} };
|
|
30893
|
+
/**
|
|
30894
|
+
* A key name is authored data that ends up in a log line, so strip the control
|
|
30895
|
+
* characters that would let it forge a line or hide the warnings beside it, and
|
|
30896
|
+
* cap the length.
|
|
30897
|
+
*/
|
|
30898
|
+
function displayKey(key) {
|
|
30899
|
+
const stripped = stripControlCharacters(key);
|
|
30900
|
+
return stripped.length > 80 ? `${stripped.slice(0, 80)}…` : stripped;
|
|
30901
|
+
}
|
|
30902
|
+
/**
|
|
30903
|
+
* Alternate spellings Claude Code accepts for a top-level settings key, mapped
|
|
30904
|
+
* to the canonical key whose **Scope** the alias inherits: "In any settings
|
|
30905
|
+
* file that accepts the canonical key, Claude Code reads the alias exactly as
|
|
30906
|
+
* it reads the canonical key." Resolving through this map before the scope
|
|
30907
|
+
* check keeps an alias from slipping past a restriction its canonical spelling
|
|
30908
|
+
* is caught by — `allowedMarketplaces` is `Managed`, like
|
|
30909
|
+
* `strictKnownMarketplaces`. Both aliases require Claude Code v2.1.232+.
|
|
30910
|
+
*
|
|
30911
|
+
* @see https://code.claude.com/docs/en/settings-reference#marketplace-key-aliases
|
|
30912
|
+
*/
|
|
30913
|
+
const CLAUDECODE_SETTINGS_KEY_ALIASES = {
|
|
30914
|
+
additionalMarketplaces: "extraKnownMarketplaces",
|
|
30915
|
+
allowedMarketplaces: "strictKnownMarketplaces"
|
|
30916
|
+
};
|
|
30917
|
+
/**
|
|
30918
|
+
* Copy of the authored top-level passthrough with the keys the target file
|
|
30919
|
+
* cannot honor removed, warning once per dropped key. Like
|
|
30920
|
+
* `stripSandboxPaths`, only the override copy is filtered — a value
|
|
30921
|
+
* already hand-written in the target file is left untouched, which is why the
|
|
30922
|
+
* warning points at it.
|
|
30923
|
+
*/
|
|
30924
|
+
function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logger }) {
|
|
30925
|
+
const filtered = {};
|
|
30926
|
+
const trustAffecting = [];
|
|
30927
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
30928
|
+
const shown = displayKey(key);
|
|
30929
|
+
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
30930
|
+
if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) {
|
|
30931
|
+
logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
|
|
30932
|
+
continue;
|
|
30933
|
+
}
|
|
30934
|
+
if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
|
|
30935
|
+
logger?.warn(`Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
|
|
30936
|
+
continue;
|
|
30937
|
+
}
|
|
30938
|
+
if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
|
|
30939
|
+
logger?.warn(`Claude Code permissions: '${shown}' is not honored in the project-scoped ${relativeFilePath}, so it is not written there — Claude Code reads it from user, local or managed settings. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
30940
|
+
continue;
|
|
30941
|
+
}
|
|
30942
|
+
const projectIgnored = Object.hasOwn(CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES, canonicalKey) ? CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES[canonicalKey] : void 0;
|
|
30943
|
+
if (!global && projectIgnored !== void 0 && projectIgnored.ignored(value)) {
|
|
30944
|
+
logger?.warn(`Claude Code permissions: this value of '${shown}' is not honored in the project-scoped ${relativeFilePath}, so it is not written there — ${projectIgnored.note}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
30945
|
+
continue;
|
|
30946
|
+
}
|
|
30947
|
+
const widensAtValue = Object.hasOwn(CLAUDECODE_TRUST_KEY_WIDENING_VALUES, canonicalKey) ? CLAUDECODE_TRUST_KEY_WIDENING_VALUES[canonicalKey] : void 0;
|
|
30948
|
+
if (Object.hasOwn(CLAUDECODE_TRUST_AFFECTING_KEYS, canonicalKey) && (widensAtValue === void 0 || widensAtValue(value))) trustAffecting.push({
|
|
30949
|
+
label: shown,
|
|
30950
|
+
reason: CLAUDECODE_TRUST_AFFECTING_KEYS[canonicalKey]
|
|
30951
|
+
});
|
|
30952
|
+
filtered[key] = value;
|
|
30953
|
+
}
|
|
30954
|
+
return {
|
|
30955
|
+
filtered,
|
|
30956
|
+
trustAffecting
|
|
30957
|
+
};
|
|
30958
|
+
}
|
|
29570
30959
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
29571
30960
|
Write: "Edit",
|
|
29572
30961
|
NotebookEdit: "Edit",
|
|
@@ -29635,9 +31024,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29635
31024
|
config,
|
|
29636
31025
|
logger
|
|
29637
31026
|
});
|
|
31027
|
+
const trustAffecting = [];
|
|
29638
31028
|
const overridePermissions = config.claudecode?.permissions;
|
|
29639
31029
|
if (overridePermissions && typeof overridePermissions === "object") {
|
|
29640
|
-
const { allow: _a, ask: _k, deny: _d, ...
|
|
31030
|
+
const { allow: _a, ask: _k, deny: _d, ...rest } = overridePermissions;
|
|
31031
|
+
const nonListFields = Object.fromEntries(Object.entries(rest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
31032
|
+
trustAffecting.push(...collectWideningPermissionFields({ fields: nonListFields }));
|
|
29641
31033
|
settings.permissions = {
|
|
29642
31034
|
...settings.permissions,
|
|
29643
31035
|
...nonListFields
|
|
@@ -29645,17 +31037,44 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29645
31037
|
}
|
|
29646
31038
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
29647
31039
|
if (isPlainRecord(overrideSandbox)) {
|
|
29648
|
-
const
|
|
29649
|
-
sandbox:
|
|
29650
|
-
|
|
29651
|
-
|
|
29652
|
-
|
|
29653
|
-
|
|
31040
|
+
const honorableSandbox = stripSandboxPaths({
|
|
31041
|
+
sandbox: overrideSandbox,
|
|
31042
|
+
refusals: [
|
|
31043
|
+
CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL,
|
|
31044
|
+
CLAUDECODE_MANAGED_ONLY_SANDBOX_REFUSAL,
|
|
31045
|
+
...global ? [] : [CLAUDECODE_GLOBAL_ONLY_SANDBOX_REFUSAL]
|
|
31046
|
+
],
|
|
31047
|
+
relativeFilePath: paths.relativeFilePath,
|
|
31048
|
+
logger
|
|
31049
|
+
});
|
|
31050
|
+
const scopedSandbox = global ? honorableSandbox : stripProjectIgnoredMaskEntries({
|
|
31051
|
+
sandbox: honorableSandbox,
|
|
29654
31052
|
relativeFilePath: paths.relativeFilePath,
|
|
29655
31053
|
logger
|
|
29656
31054
|
});
|
|
31055
|
+
trustAffecting.push(...collectTrustAffectingSandboxPaths({ sandbox: scopedSandbox }));
|
|
29657
31056
|
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
29658
31057
|
}
|
|
31058
|
+
const overrideTopLevel = {};
|
|
31059
|
+
for (const [key, value] of Object.entries(config.claudecode ?? {})) {
|
|
31060
|
+
if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
|
|
31061
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
31062
|
+
if (value === void 0) continue;
|
|
31063
|
+
overrideTopLevel[key] = value;
|
|
31064
|
+
}
|
|
31065
|
+
const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
|
|
31066
|
+
overrides: overrideTopLevel,
|
|
31067
|
+
global,
|
|
31068
|
+
relativeFilePath: paths.relativeFilePath,
|
|
31069
|
+
logger
|
|
31070
|
+
});
|
|
31071
|
+
trustAffecting.push(...trustAffectingTopLevel);
|
|
31072
|
+
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
31073
|
+
warnOnTrustAffectingEntries({
|
|
31074
|
+
entries: trustAffecting,
|
|
31075
|
+
relativeFilePath: paths.relativeFilePath,
|
|
31076
|
+
logger
|
|
31077
|
+
});
|
|
29659
31078
|
const managedToolNames = managedClaudeToolNames(config);
|
|
29660
31079
|
const merged = applyPermissions({
|
|
29661
31080
|
settings,
|
|
@@ -29688,12 +31107,33 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29688
31107
|
ask: permissions.ask ?? [],
|
|
29689
31108
|
deny: permissions.deny ?? []
|
|
29690
31109
|
});
|
|
29691
|
-
const { allow: _a, ask: _k, deny: _d, ...
|
|
31110
|
+
const { allow: _a, ask: _k, deny: _d, ...permissionsRest } = permissions;
|
|
31111
|
+
const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
29692
31112
|
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
29693
31113
|
const { sandbox } = settings;
|
|
29694
|
-
if (isPlainRecord(sandbox)
|
|
31114
|
+
if (isPlainRecord(sandbox)) {
|
|
31115
|
+
const importedSandbox = structuredClone(sandbox);
|
|
31116
|
+
for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
|
|
31117
|
+
target: importedSandbox,
|
|
31118
|
+
path
|
|
31119
|
+
});
|
|
31120
|
+
if (Object.keys(importedSandbox).length > 0) config.claudecode = {
|
|
31121
|
+
...config.claudecode,
|
|
31122
|
+
sandbox: importedSandbox
|
|
31123
|
+
};
|
|
31124
|
+
}
|
|
31125
|
+
const topLevelPassthrough = {};
|
|
31126
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
31127
|
+
if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
|
|
31128
|
+
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
31129
|
+
if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) continue;
|
|
31130
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
31131
|
+
if (value === void 0) continue;
|
|
31132
|
+
topLevelPassthrough[key] = value;
|
|
31133
|
+
}
|
|
31134
|
+
if (Object.keys(topLevelPassthrough).length > 0) config.claudecode = {
|
|
29695
31135
|
...config.claudecode,
|
|
29696
|
-
|
|
31136
|
+
...topLevelPassthrough
|
|
29697
31137
|
};
|
|
29698
31138
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
29699
31139
|
}
|
|
@@ -34612,11 +36052,8 @@ const TOOL_KEY_TO_CATEGORY = {
|
|
|
34612
36052
|
updateConfluencePage: "edit"
|
|
34613
36053
|
};
|
|
34614
36054
|
const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
|
|
34615
|
-
const OWNED_TOOL_PERMISSION_KEYS = [
|
|
34616
|
-
|
|
34617
|
-
"allowedExternalPaths",
|
|
34618
|
-
"default"
|
|
34619
|
-
];
|
|
36055
|
+
const OWNED_TOOL_PERMISSION_KEYS = ["allowedExternalPaths", "default"];
|
|
36056
|
+
const MANAGED_BASH_KEYS = ["default", "commands"];
|
|
34620
36057
|
/**
|
|
34621
36058
|
* Permissions adapter for Rovo Dev CLI.
|
|
34622
36059
|
*
|
|
@@ -34752,6 +36189,22 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
34752
36189
|
}
|
|
34753
36190
|
};
|
|
34754
36191
|
/**
|
|
36192
|
+
* Report a `toolPermissions.bash.runInSandbox: false` that this generate is
|
|
36193
|
+
* about to carry through.
|
|
36194
|
+
*
|
|
36195
|
+
* The unmanaged `bash` siblings survive untouched, which for this one means a
|
|
36196
|
+
* generate is not the reset a user might read it as: every command the agent
|
|
36197
|
+
* runs stays outside the sandbox. Rulesync never authors the key and will not
|
|
36198
|
+
* start owning it, but a setting that persists on a committed file and loosens
|
|
36199
|
+
* containment should not do so without saying anything — least of all on the
|
|
36200
|
+
* path where the user just tightened `.rulesync/permissions.*`.
|
|
36201
|
+
*/
|
|
36202
|
+
function warnAboutPreservedSandboxOptOut({ existingToolPermissions, filePath, logger }) {
|
|
36203
|
+
const existingBash = existingToolPermissions.bash;
|
|
36204
|
+
if (!isRecord$1(existingBash) || existingBash.runInSandbox !== false) return;
|
|
36205
|
+
logger?.warn(`${filePath}: keeping toolPermissions.bash.runInSandbox: false — rulesync does not manage that key, so regenerating does not restore Rovo Dev's sandbox for bash commands.`);
|
|
36206
|
+
}
|
|
36207
|
+
/**
|
|
34755
36208
|
* Resolve the `toolPermissions` block to write, merging the generated levels
|
|
34756
36209
|
* over the existing file. Every other top-level key of `config.yml` is the
|
|
34757
36210
|
* caller's to preserve; inside this block, keys rulesync manages are owned and
|
|
@@ -34759,6 +36212,11 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
34759
36212
|
*/
|
|
34760
36213
|
function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
|
|
34761
36214
|
const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
|
|
36215
|
+
warnAboutPreservedSandboxOptOut({
|
|
36216
|
+
existingToolPermissions,
|
|
36217
|
+
filePath,
|
|
36218
|
+
logger
|
|
36219
|
+
});
|
|
34762
36220
|
if (Object.keys(generated).length === 0 && sourceStatesRules) {
|
|
34763
36221
|
if (!isRecord$1(existing)) return;
|
|
34764
36222
|
const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
|
|
@@ -34767,9 +36225,12 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
34767
36225
|
}
|
|
34768
36226
|
const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
|
|
34769
36227
|
const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
|
|
36228
|
+
const hasExistingBashRecord = isRecord$1(existingToolPermissions.bash);
|
|
36229
|
+
const existingBash = hasExistingBashRecord ? { ...existingToolPermissions.bash } : {};
|
|
34770
36230
|
warnAboutDroppedOwnedKeys({
|
|
34771
36231
|
existingToolPermissions,
|
|
34772
36232
|
existingTools,
|
|
36233
|
+
existingBash,
|
|
34773
36234
|
generated,
|
|
34774
36235
|
filePath,
|
|
34775
36236
|
logger
|
|
@@ -34779,15 +36240,22 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
34779
36240
|
delete existingToolPermissions[toolKey];
|
|
34780
36241
|
delete existingTools[toolKey];
|
|
34781
36242
|
}
|
|
36243
|
+
for (const bashKey of MANAGED_BASH_KEYS) delete existingBash[bashKey];
|
|
34782
36244
|
const tools = {
|
|
34783
36245
|
...existingTools,
|
|
34784
36246
|
...generated.tools
|
|
34785
36247
|
};
|
|
36248
|
+
const bash = {
|
|
36249
|
+
...existingBash,
|
|
36250
|
+
...generated.bash
|
|
36251
|
+
};
|
|
34786
36252
|
if (hasExistingToolsRecord) delete existingToolPermissions.tools;
|
|
36253
|
+
if (hasExistingBashRecord) delete existingToolPermissions.bash;
|
|
34787
36254
|
return {
|
|
34788
36255
|
...existingToolPermissions,
|
|
34789
36256
|
...generated,
|
|
34790
|
-
...Object.keys(tools).length > 0 ? { tools } : {}
|
|
36257
|
+
...Object.keys(tools).length > 0 ? { tools } : {},
|
|
36258
|
+
...Object.keys(bash).length > 0 ? { bash } : {}
|
|
34791
36259
|
};
|
|
34792
36260
|
}
|
|
34793
36261
|
/**
|
|
@@ -34796,9 +36264,14 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
34796
36264
|
* `/directories` and by an "always allow" answer to a prompt — so their removal
|
|
34797
36265
|
* must not be silent.
|
|
34798
36266
|
*/
|
|
34799
|
-
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, generated, filePath, logger }) {
|
|
36267
|
+
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, existingBash, generated, filePath, logger }) {
|
|
34800
36268
|
const newTools = generated.tools ?? {};
|
|
34801
|
-
const
|
|
36269
|
+
const newBash = generated.bash ?? {};
|
|
36270
|
+
const droppedKeys = [
|
|
36271
|
+
...OWNED_TOOL_PERMISSION_KEYS.filter((ownedKey) => existingToolPermissions[ownedKey] !== void 0 && generated[ownedKey] === void 0),
|
|
36272
|
+
...MANAGED_TOOL_KEYS.filter((toolKey) => existingTools[toolKey] !== void 0 && newTools[toolKey] === void 0).map((toolKey) => `tools.${toolKey}`),
|
|
36273
|
+
...MANAGED_BASH_KEYS.filter((bashKey) => existingBash[bashKey] !== void 0 && newBash[bashKey] === void 0).map((bashKey) => `bash.${bashKey}`)
|
|
36274
|
+
];
|
|
34802
36275
|
if (droppedKeys.length > 0) logger?.warn(`Rovo Dev permissions: removing ${droppedKeys.map((key) => `"${key}"`).join(", ")} from ${filePath} because the rulesync source no longer produces them.`);
|
|
34803
36276
|
}
|
|
34804
36277
|
/**
|
|
@@ -36906,10 +38379,10 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
36906
38379
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
36907
38380
|
toolTarget;
|
|
36908
38381
|
global;
|
|
36909
|
-
constructor({ outputRoot = process.cwd(),
|
|
38382
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
36910
38383
|
super({
|
|
36911
38384
|
outputRoot,
|
|
36912
|
-
|
|
38385
|
+
inputRoots,
|
|
36913
38386
|
dryRun,
|
|
36914
38387
|
logger
|
|
36915
38388
|
});
|
|
@@ -36919,9 +38392,17 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
36919
38392
|
this.global = global;
|
|
36920
38393
|
}
|
|
36921
38394
|
async loadRulesyncFiles() {
|
|
38395
|
+
const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncPermissions.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
|
|
38396
|
+
const sourceTree = await pickLastRootWithFile({
|
|
38397
|
+
inputRoots: this.inputRoots,
|
|
38398
|
+
relativePaths,
|
|
38399
|
+
logger: this.logger,
|
|
38400
|
+
artifactName: "The permissions file"
|
|
38401
|
+
}) ?? this.inputRoots[0];
|
|
36922
38402
|
try {
|
|
36923
38403
|
return [await RulesyncPermissions.fromFile({
|
|
36924
|
-
outputRoot:
|
|
38404
|
+
outputRoot: dirname(sourceTree),
|
|
38405
|
+
relativeDirPath: basename(sourceTree),
|
|
36925
38406
|
validate: true
|
|
36926
38407
|
})];
|
|
36927
38408
|
} catch (error) {
|
|
@@ -37345,13 +38826,26 @@ var RovodevSkill = class RovodevSkill extends ToolSkill {
|
|
|
37345
38826
|
//#region src/types/dir-feature-processor.ts
|
|
37346
38827
|
var DirFeatureProcessor = class {
|
|
37347
38828
|
outputRoot;
|
|
37348
|
-
|
|
38829
|
+
/**
|
|
38830
|
+
* Ordered, non-empty list of rulesync source-tree directories. Each entry
|
|
38831
|
+
* is a source tree itself — the directory that directly contains
|
|
38832
|
+
* feature subdirectories (`rules/`, `skills/`, …) and single-file
|
|
38833
|
+
* features (`mcp.jsonc`, `hooks.jsonc`, …). Later entries take precedence
|
|
38834
|
+
* when two trees supply the same relative path. Defaults to
|
|
38835
|
+
* `[join(process.cwd(), ".rulesync")]`.
|
|
38836
|
+
*
|
|
38837
|
+
* The singular user-facing alias (`inputRoot` in `rulesync.jsonc` / the
|
|
38838
|
+
* `--input-root` CLI flag / `GenerateOptions.inputRoot`) is deprecated
|
|
38839
|
+
* and collapsed into `[join(inputRoot, ".rulesync")]` before it ever
|
|
38840
|
+
* reaches a processor.
|
|
38841
|
+
*/
|
|
38842
|
+
inputRoots;
|
|
37349
38843
|
dryRun;
|
|
37350
38844
|
avoidBlockScalars;
|
|
37351
38845
|
logger;
|
|
37352
|
-
constructor({ outputRoot = process.cwd(),
|
|
38846
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, avoidBlockScalars = false, logger }) {
|
|
37353
38847
|
this.outputRoot = outputRoot;
|
|
37354
|
-
this.
|
|
38848
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
37355
38849
|
this.dryRun = dryRun;
|
|
37356
38850
|
this.avoidBlockScalars = avoidBlockScalars;
|
|
37357
38851
|
this.logger = logger;
|
|
@@ -39610,9 +41104,9 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
39610
41104
|
* slug is owned by the commands feature: it must not be imported as a
|
|
39611
41105
|
* skill nor deleted as an orphan skill.
|
|
39612
41106
|
*/
|
|
39613
|
-
static async isDirOwned({ dirName,
|
|
41107
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
39614
41108
|
return !await rulesyncCommandSlugExists({
|
|
39615
|
-
|
|
41109
|
+
inputRoots,
|
|
39616
41110
|
dirName
|
|
39617
41111
|
});
|
|
39618
41112
|
}
|
|
@@ -39662,7 +41156,11 @@ const FactorydroidSkillFrontmatterSchema = z.looseObject({
|
|
|
39662
41156
|
"user-invocable": z.optional(z.boolean()),
|
|
39663
41157
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
39664
41158
|
enabled: z.optional(z.boolean()),
|
|
39665
|
-
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
41159
|
+
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
41160
|
+
license: z.optional(z.unknown()),
|
|
41161
|
+
compatibility: z.optional(z.unknown()),
|
|
41162
|
+
metadata: z.optional(z.unknown()),
|
|
41163
|
+
version: z.optional(z.unknown())
|
|
39666
41164
|
});
|
|
39667
41165
|
/**
|
|
39668
41166
|
* Represents a Factory Droid skill directory.
|
|
@@ -39715,16 +41213,10 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
39715
41213
|
};
|
|
39716
41214
|
}
|
|
39717
41215
|
toRulesyncSkill() {
|
|
39718
|
-
const
|
|
39719
|
-
const factorydroidBlock = {
|
|
39720
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
39721
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
39722
|
-
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
39723
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
39724
|
-
};
|
|
41216
|
+
const { name, description, ...factorydroidBlock } = this.getFrontmatter();
|
|
39725
41217
|
const rulesyncFrontmatter = {
|
|
39726
|
-
name
|
|
39727
|
-
description
|
|
41218
|
+
name,
|
|
41219
|
+
description,
|
|
39728
41220
|
targets: ["*"],
|
|
39729
41221
|
...Object.keys(factorydroidBlock).length > 0 && { factorydroid: factorydroidBlock }
|
|
39730
41222
|
};
|
|
@@ -39751,13 +41243,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
39751
41243
|
rootFrontmatter: rulesyncFrontmatter,
|
|
39752
41244
|
section: factorydroidSection
|
|
39753
41245
|
});
|
|
41246
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = factorydroidSection ?? {};
|
|
39754
41247
|
const factorydroidFrontmatter = {
|
|
39755
41248
|
name: rulesyncFrontmatter.name,
|
|
39756
41249
|
description: rulesyncFrontmatter.description,
|
|
41250
|
+
...section,
|
|
39757
41251
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
39758
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
39759
|
-
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
39760
|
-
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
41252
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
39761
41253
|
};
|
|
39762
41254
|
return new FactorydroidSkill({
|
|
39763
41255
|
outputRoot,
|
|
@@ -40169,7 +41661,10 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
40169
41661
|
}
|
|
40170
41662
|
}
|
|
40171
41663
|
static getSettablePaths(_options) {
|
|
40172
|
-
return {
|
|
41664
|
+
return {
|
|
41665
|
+
relativeDirPath: JUNIE_SKILLS_DIR_PATH,
|
|
41666
|
+
importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
|
|
41667
|
+
};
|
|
40173
41668
|
}
|
|
40174
41669
|
getFrontmatter() {
|
|
40175
41670
|
return JunieSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
@@ -42331,9 +43826,9 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
42331
43826
|
* slug is owned by the commands feature: it must not be imported as a
|
|
42332
43827
|
* skill nor deleted as an orphan skill.
|
|
42333
43828
|
*/
|
|
42334
|
-
static async isDirOwned({ dirName,
|
|
43829
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
42335
43830
|
return !await rulesyncCommandSlugExists({
|
|
42336
|
-
|
|
43831
|
+
inputRoots,
|
|
42337
43832
|
dirName
|
|
42338
43833
|
});
|
|
42339
43834
|
}
|
|
@@ -42891,10 +44386,10 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
42891
44386
|
toolTarget;
|
|
42892
44387
|
global;
|
|
42893
44388
|
getFactory;
|
|
42894
|
-
constructor({ outputRoot = process.cwd(),
|
|
44389
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
42895
44390
|
super({
|
|
42896
44391
|
outputRoot,
|
|
42897
|
-
|
|
44392
|
+
inputRoots,
|
|
42898
44393
|
dryRun,
|
|
42899
44394
|
avoidBlockScalars: toolTarget === "cursor",
|
|
42900
44395
|
logger
|
|
@@ -42932,39 +44427,67 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
42932
44427
|
return rulesyncSkills;
|
|
42933
44428
|
}
|
|
42934
44429
|
/**
|
|
42935
|
-
*
|
|
42936
|
-
*
|
|
42937
|
-
*
|
|
42938
|
-
*
|
|
44430
|
+
* Load rulesync skill directories from a single source-tree's `skills/`
|
|
44431
|
+
* (and `skills/.curated/`) subtree. `sourceTree` is the source tree
|
|
44432
|
+
* itself (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`). Intra-tree:
|
|
44433
|
+
* local skills take precedence over curated skills with the same name.
|
|
42939
44434
|
*/
|
|
42940
|
-
async
|
|
42941
|
-
const
|
|
44435
|
+
async loadRulesyncDirsForRoot(sourceTree) {
|
|
44436
|
+
const treeParent = dirname(sourceTree);
|
|
44437
|
+
const treeName = basename(sourceTree);
|
|
44438
|
+
const treeSkillsDirPath = join(treeName, SKILLS_FEATURE_SUBDIR);
|
|
44439
|
+
const treeCuratedSkillsDirPath = join(treeName, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
44440
|
+
const localDirNames = [...await getLocalSkillDirNames(sourceTree)];
|
|
42942
44441
|
const localSkills = await Promise.all(localDirNames.map((dirName) => RulesyncSkill.fromDir({
|
|
42943
|
-
outputRoot:
|
|
44442
|
+
outputRoot: treeParent,
|
|
44443
|
+
relativeDirPath: treeSkillsDirPath,
|
|
42944
44444
|
dirName,
|
|
42945
44445
|
global: this.global
|
|
42946
44446
|
})));
|
|
42947
|
-
const
|
|
42948
|
-
const curatedDirPath = join(
|
|
44447
|
+
const localSkillNamesByIdentity = groupSpellingsByCaseFoldedIdentity(localDirNames);
|
|
44448
|
+
const curatedDirPath = join(sourceTree, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
42949
44449
|
let curatedSkills = [];
|
|
42950
44450
|
if (await directoryExists(curatedDirPath)) {
|
|
42951
44451
|
const nonConflicting = (await findFilesByGlobs(join(curatedDirPath, "*"), { type: "dir" })).map((path) => basename(path)).filter((name) => {
|
|
42952
|
-
|
|
42953
|
-
|
|
42954
|
-
|
|
42955
|
-
|
|
42956
|
-
|
|
44452
|
+
const spellings = localSkillNamesByIdentity.get(caseFoldIdentity(name));
|
|
44453
|
+
if (spellings === void 0) return true;
|
|
44454
|
+
if (spellings.includes(name)) this.logger.debug(`Skipping curated skill "${name}": local skill takes precedence.`);
|
|
44455
|
+
else this.logger.warn(formatCuratedCaseCollisionWarning({
|
|
44456
|
+
artifactKind: "skill",
|
|
44457
|
+
entryNoun: "skill",
|
|
44458
|
+
treeDirPath: treeSkillsDirPath,
|
|
44459
|
+
curatedSpelling: name,
|
|
44460
|
+
localSpellings: spellings
|
|
44461
|
+
}));
|
|
44462
|
+
return false;
|
|
42957
44463
|
});
|
|
42958
|
-
const curatedRelativeDirPath = RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH;
|
|
42959
44464
|
curatedSkills = await Promise.all(nonConflicting.map((dirName) => RulesyncSkill.fromDir({
|
|
42960
|
-
outputRoot:
|
|
42961
|
-
relativeDirPath:
|
|
44465
|
+
outputRoot: treeParent,
|
|
44466
|
+
relativeDirPath: treeCuratedSkillsDirPath,
|
|
42962
44467
|
dirName,
|
|
42963
44468
|
global: this.global
|
|
42964
44469
|
})));
|
|
42965
44470
|
}
|
|
42966
|
-
|
|
42967
|
-
|
|
44471
|
+
return [...localSkills, ...curatedSkills];
|
|
44472
|
+
}
|
|
44473
|
+
/**
|
|
44474
|
+
* Implementation of abstract method from DirFeatureProcessor.
|
|
44475
|
+
*
|
|
44476
|
+
* Load and parse rulesync skill directories from every configured input
|
|
44477
|
+
* root's `.rulesync/skills/` tree (each root also honours its own
|
|
44478
|
+
* `.curated/` subdirectory). When two roots supply a skill with the same
|
|
44479
|
+
* directory name, the later root's skill replaces the earlier root's copy
|
|
44480
|
+
* atomically (companion files included) — an overlay always ships a whole
|
|
44481
|
+
* skill directory, never a partial patch.
|
|
44482
|
+
*/
|
|
44483
|
+
async loadRulesyncDirs() {
|
|
44484
|
+
const allSkills = mergeByCaseInsensitiveIdentity({
|
|
44485
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncDirsForRoot(root))),
|
|
44486
|
+
identity: (skill) => skill.getDirName(),
|
|
44487
|
+
artifactName: "skill",
|
|
44488
|
+
logger: this.logger
|
|
44489
|
+
});
|
|
44490
|
+
this.logger.debug(`Successfully loaded ${allSkills.length} rulesync skills`);
|
|
42968
44491
|
return allSkills;
|
|
42969
44492
|
}
|
|
42970
44493
|
/**
|
|
@@ -42980,7 +44503,17 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
42980
44503
|
}) : [];
|
|
42981
44504
|
const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
|
|
42982
44505
|
const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
|
|
42983
|
-
const
|
|
44506
|
+
const claimedSkillNames = new ClaimedIdentities();
|
|
44507
|
+
const claimSkillName = ({ skill, relativeDirPath, sourcePath }) => {
|
|
44508
|
+
const skillName = skill.getImportIdentity();
|
|
44509
|
+
const claimed = claimedSkillNames.claim({
|
|
44510
|
+
identity: skillName,
|
|
44511
|
+
source: relativeDirPath
|
|
44512
|
+
});
|
|
44513
|
+
if (claimed === null) return true;
|
|
44514
|
+
if (claimed.spelling !== skillName) this.logger.warn(`Case-insensitive ${this.toolTarget} skill collision: "${claimed.spelling}" and "${skillName}" resolve to the same skill directory. Keeping "${claimed.spelling}" from ${claimed.source === relativeDirPath ? "earlier in the same root" : `the higher-precedence ${claimed.source}`} and ignoring ${sourcePath}, which is not imported.`);
|
|
44515
|
+
return false;
|
|
44516
|
+
};
|
|
42984
44517
|
const toolSkills = [];
|
|
42985
44518
|
for (const root of roots) {
|
|
42986
44519
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
@@ -42996,54 +44529,60 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
42996
44529
|
outputRoot: rootOutputRoot,
|
|
42997
44530
|
relativeDirPath,
|
|
42998
44531
|
dirName,
|
|
42999
|
-
|
|
44532
|
+
inputRoots: this.inputRoots
|
|
43000
44533
|
})) continue;
|
|
43001
44534
|
ownedDirNames.push(dirName);
|
|
43002
44535
|
}
|
|
43003
44536
|
const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
|
|
44537
|
+
const sourcePath = join(relativeDirPath, dirName);
|
|
43004
44538
|
try {
|
|
43005
|
-
return
|
|
43006
|
-
|
|
43007
|
-
|
|
43008
|
-
|
|
43009
|
-
|
|
43010
|
-
|
|
44539
|
+
return {
|
|
44540
|
+
skill: await factory.class.fromDir({
|
|
44541
|
+
outputRoot: rootOutputRoot,
|
|
44542
|
+
relativeDirPath,
|
|
44543
|
+
dirName,
|
|
44544
|
+
global: this.global
|
|
44545
|
+
}),
|
|
44546
|
+
sourcePath
|
|
44547
|
+
};
|
|
43011
44548
|
} catch (error) {
|
|
43012
44549
|
if (!isLenientRoot) throw error;
|
|
43013
|
-
this.logger.warn(`Skipping ${
|
|
44550
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43014
44551
|
return null;
|
|
43015
44552
|
}
|
|
43016
|
-
}))).filter((
|
|
43017
|
-
for (const skill of directorySkills) {
|
|
43018
|
-
|
|
43019
|
-
|
|
43020
|
-
|
|
43021
|
-
|
|
43022
|
-
}
|
|
44553
|
+
}))).filter((loaded) => loaded !== null);
|
|
44554
|
+
for (const { skill, sourcePath } of directorySkills) if (claimSkillName({
|
|
44555
|
+
skill,
|
|
44556
|
+
relativeDirPath,
|
|
44557
|
+
sourcePath
|
|
44558
|
+
})) toolSkills.push(skill);
|
|
43023
44559
|
if (!factory.class.fromFlatFile) continue;
|
|
43024
44560
|
const fromFlatFile = factory.class.fromFlatFile;
|
|
43025
44561
|
const directoryStems = new Set(ownedDirNames);
|
|
43026
44562
|
const flatFilePaths = (await findFilesByGlobs(join(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has(basename(filePath, ".md")));
|
|
43027
44563
|
const flatSkills = (await Promise.all(flatFilePaths.map(async (filePath) => {
|
|
44564
|
+
const sourcePath = join(relativeDirPath, basename(filePath));
|
|
43028
44565
|
try {
|
|
43029
|
-
return
|
|
43030
|
-
|
|
43031
|
-
|
|
43032
|
-
|
|
43033
|
-
|
|
43034
|
-
|
|
44566
|
+
return {
|
|
44567
|
+
skill: await fromFlatFile({
|
|
44568
|
+
outputRoot: rootOutputRoot,
|
|
44569
|
+
relativeDirPath,
|
|
44570
|
+
relativeFilePath: basename(filePath),
|
|
44571
|
+
global: this.global
|
|
44572
|
+
}),
|
|
44573
|
+
sourcePath
|
|
44574
|
+
};
|
|
43035
44575
|
} catch (error) {
|
|
43036
44576
|
if (!isLenientRoot) throw error;
|
|
43037
|
-
this.logger.warn(`Skipping ${
|
|
44577
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43038
44578
|
return null;
|
|
43039
44579
|
}
|
|
43040
|
-
}))).filter((
|
|
43041
|
-
for (const skill of flatSkills) {
|
|
43042
|
-
|
|
43043
|
-
|
|
43044
|
-
|
|
43045
|
-
|
|
43046
|
-
}
|
|
44580
|
+
}))).filter((loaded) => loaded !== null);
|
|
44581
|
+
for (const { skill, sourcePath } of flatSkills) if (claimSkillName({
|
|
44582
|
+
skill,
|
|
44583
|
+
relativeDirPath,
|
|
44584
|
+
sourcePath
|
|
44585
|
+
})) toolSkills.push(skill);
|
|
43047
44586
|
}
|
|
43048
44587
|
this.logger.debug(`Successfully loaded ${toolSkills.length} skills from ${roots.length} root(s)`);
|
|
43049
44588
|
return toolSkills;
|
|
@@ -43073,7 +44612,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43073
44612
|
outputRoot: this.outputRoot,
|
|
43074
44613
|
relativeDirPath: root,
|
|
43075
44614
|
dirName,
|
|
43076
|
-
|
|
44615
|
+
inputRoots: this.inputRoots
|
|
43077
44616
|
})) continue;
|
|
43078
44617
|
const toolSkill = factory.class.forDeletion({
|
|
43079
44618
|
outputRoot: this.outputRoot,
|
|
@@ -45831,6 +47370,7 @@ const JunieSubagentFrontmatterSchema = z.looseObject({
|
|
|
45831
47370
|
disallowedTools: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
45832
47371
|
mcpServers: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
45833
47372
|
model: z.optional(z.string()),
|
|
47373
|
+
permissionMode: z.optional(z.string()),
|
|
45834
47374
|
reasoningLevel: z.optional(z.string()),
|
|
45835
47375
|
maxTurns: z.optional(z.number()),
|
|
45836
47376
|
skills: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
@@ -47811,14 +49351,25 @@ const subagentsProcessorToolTargetsSimulated = allToolTargetKeys$1.filter((targe
|
|
|
47811
49351
|
const subagentsProcessorToolTargetsGlobal = allToolTargetKeys$1.filter((target) => {
|
|
47812
49352
|
return toolSubagentFactories.get(target)?.meta.supportsGlobal ?? false;
|
|
47813
49353
|
});
|
|
49354
|
+
/**
|
|
49355
|
+
* Stands in for a discovery root when a subagent came from a tool's own config
|
|
49356
|
+
* file rather than a directory (see `loadAdditionalImportFiles`). The angle
|
|
49357
|
+
* brackets keep it from ever matching a real relative directory path.
|
|
49358
|
+
*/
|
|
49359
|
+
const INLINE_SOURCE = "<inline>";
|
|
49360
|
+
/**
|
|
49361
|
+
* The single "root" of the post-conversion output guard, which de-duplicates
|
|
49362
|
+
* `.rulesync/subagents/` paths rather than discovery roots.
|
|
49363
|
+
*/
|
|
49364
|
+
const OUTPUT_SOURCE = "<output>";
|
|
47814
49365
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
47815
49366
|
toolTarget;
|
|
47816
49367
|
global;
|
|
47817
49368
|
getFactory;
|
|
47818
|
-
constructor({ outputRoot = process.cwd(),
|
|
49369
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
47819
49370
|
super({
|
|
47820
49371
|
outputRoot,
|
|
47821
|
-
|
|
49372
|
+
inputRoots,
|
|
47822
49373
|
dryRun,
|
|
47823
49374
|
logger
|
|
47824
49375
|
});
|
|
@@ -47864,24 +49415,31 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
47864
49415
|
rulesyncSubagents.push(toolSubagent.toRulesyncSubagent());
|
|
47865
49416
|
}
|
|
47866
49417
|
const uniqueRulesyncSubagents = [];
|
|
47867
|
-
const
|
|
49418
|
+
const claimedOutputPaths = new ClaimedIdentities();
|
|
47868
49419
|
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
47869
49420
|
const outputPath = join(rulesyncSubagent.getRelativeDirPath(), rulesyncSubagent.getRelativeFilePath());
|
|
47870
|
-
|
|
47871
|
-
|
|
49421
|
+
const claimed = claimedOutputPaths.claim({
|
|
49422
|
+
identity: outputPath,
|
|
49423
|
+
source: OUTPUT_SOURCE
|
|
49424
|
+
});
|
|
49425
|
+
if (claimed !== null) {
|
|
49426
|
+
this.logger.warn(claimed.spelling === outputPath ? `Multiple ${this.toolTarget} subagents resolve to "${outputPath}"; keeping the first and ignoring this copy.` : `${this.toolTarget} subagent "${outputPath}" differs only in case from "${claimed.spelling}", which is the same file on a case-insensitive filesystem; keeping the first and ignoring this copy.`);
|
|
47872
49427
|
continue;
|
|
47873
49428
|
}
|
|
47874
|
-
seenOutputPaths.add(outputPath);
|
|
47875
49429
|
uniqueRulesyncSubagents.push(rulesyncSubagent);
|
|
47876
49430
|
}
|
|
47877
49431
|
return uniqueRulesyncSubagents;
|
|
47878
49432
|
}
|
|
47879
49433
|
/**
|
|
47880
|
-
*
|
|
47881
|
-
*
|
|
49434
|
+
* Load subagent files from a single source-tree's `subagents/` subtree.
|
|
49435
|
+
* `sourceTree` is the source tree itself (e.g. `/repo/.rulesync` or
|
|
49436
|
+
* `/repo/.rulesync.local`).
|
|
47882
49437
|
*/
|
|
47883
|
-
async
|
|
47884
|
-
const
|
|
49438
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
49439
|
+
const treeParent = dirname(sourceTree);
|
|
49440
|
+
const treeName = basename(sourceTree);
|
|
49441
|
+
const treeSubagentsDirPath = join(treeName, SUBAGENTS_FEATURE_SUBDIR);
|
|
49442
|
+
const subagentsDir = join(sourceTree, SUBAGENTS_FEATURE_SUBDIR);
|
|
47885
49443
|
if (!await directoryExists(subagentsDir)) {
|
|
47886
49444
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
47887
49445
|
return [];
|
|
@@ -47897,7 +49455,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
47897
49455
|
const filepath = join(subagentsDir, mdFile);
|
|
47898
49456
|
try {
|
|
47899
49457
|
const rulesyncSubagent = await RulesyncSubagent.fromFile({
|
|
47900
|
-
outputRoot:
|
|
49458
|
+
outputRoot: treeParent,
|
|
49459
|
+
relativeDirPath: treeSubagentsDirPath,
|
|
47901
49460
|
relativeFilePath: mdFile,
|
|
47902
49461
|
validate: true
|
|
47903
49462
|
});
|
|
@@ -47908,8 +49467,24 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
47908
49467
|
continue;
|
|
47909
49468
|
}
|
|
47910
49469
|
}
|
|
49470
|
+
return rulesyncSubagents;
|
|
49471
|
+
}
|
|
49472
|
+
/**
|
|
49473
|
+
* Implementation of abstract method from Processor
|
|
49474
|
+
* Load and parse rulesync subagent files from every configured input root's
|
|
49475
|
+
* `.rulesync/subagents/` directory, merging by relative file path so a
|
|
49476
|
+
* subagent with the same target path from a later root replaces the
|
|
49477
|
+
* earlier root's copy.
|
|
49478
|
+
*/
|
|
49479
|
+
async loadRulesyncFiles() {
|
|
49480
|
+
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
49481
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
49482
|
+
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
49483
|
+
artifactName: "subagent",
|
|
49484
|
+
logger: this.logger
|
|
49485
|
+
});
|
|
47911
49486
|
if (rulesyncSubagents.length === 0) {
|
|
47912
|
-
this.logger.debug(`No valid subagents found
|
|
49487
|
+
this.logger.debug(`No valid subagents found`);
|
|
47913
49488
|
return [];
|
|
47914
49489
|
}
|
|
47915
49490
|
this.logger.debug(`Successfully loaded ${rulesyncSubagents.length} rulesync subagents`);
|
|
@@ -47924,7 +49499,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
47924
49499
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
47925
49500
|
const roots = forDeletion ? [paths.relativeDirPath] : [paths.relativeDirPath, ...paths.importDirPaths ?? []];
|
|
47926
49501
|
const toolSubagents = [];
|
|
47927
|
-
const
|
|
49502
|
+
const claimedRelativeFilePaths = new ClaimedIdentities();
|
|
47928
49503
|
for (const root of roots) {
|
|
47929
49504
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
47930
49505
|
const dirPath = typeof root === "string" ? root : root.relativeDirPath;
|
|
@@ -47963,37 +49538,77 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
47963
49538
|
relativeFilePath: toRelativeFilePath(path),
|
|
47964
49539
|
global: this.global
|
|
47965
49540
|
})));
|
|
47966
|
-
|
|
47967
|
-
|
|
47968
|
-
|
|
47969
|
-
|
|
47970
|
-
|
|
47971
|
-
continue;
|
|
47972
|
-
}
|
|
47973
|
-
seenRelativeFilePaths.add(key);
|
|
47974
|
-
deduped.push(subagent);
|
|
47975
|
-
}
|
|
47976
|
-
toolSubagents.push(...deduped);
|
|
49541
|
+
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
49542
|
+
loaded,
|
|
49543
|
+
dirPath,
|
|
49544
|
+
claimedRelativeFilePaths
|
|
49545
|
+
}));
|
|
47977
49546
|
}
|
|
47978
49547
|
if (!forDeletion && factory.class.loadAdditionalImportFiles) {
|
|
47979
49548
|
const additionalSubagents = await factory.class.loadAdditionalImportFiles({
|
|
47980
49549
|
outputRoot: this.outputRoot,
|
|
47981
49550
|
global: this.global
|
|
47982
49551
|
});
|
|
47983
|
-
|
|
47984
|
-
|
|
47985
|
-
|
|
47986
|
-
|
|
47987
|
-
continue;
|
|
47988
|
-
}
|
|
47989
|
-
seenRelativeFilePaths.add(key);
|
|
47990
|
-
toolSubagents.push(subagent);
|
|
47991
|
-
}
|
|
49552
|
+
toolSubagents.push(...this.claimInlineSubagents({
|
|
49553
|
+
additionalSubagents,
|
|
49554
|
+
claimedRelativeFilePaths
|
|
49555
|
+
}));
|
|
47992
49556
|
}
|
|
47993
49557
|
this.logger.debug(`Successfully loaded ${toolSubagents.length} ${this.toolTarget} subagents from ${roots.length} root(s)`);
|
|
47994
49558
|
return toolSubagents;
|
|
47995
49559
|
}
|
|
47996
49560
|
/**
|
|
49561
|
+
* Keeps the subagents from one discovery root whose import identity is still
|
|
49562
|
+
* unclaimed, warning about each copy that loses. Split out of
|
|
49563
|
+
* `loadToolFiles` so the two de-duplication passes stay readable side by
|
|
49564
|
+
* side (and so that method stays within the linter's complexity budget).
|
|
49565
|
+
*
|
|
49566
|
+
* When more than one discovery root is scanned (e.g. Junie's `.junie/agents/`
|
|
49567
|
+
* plus `.agents/`), two roots can hold a subagent with the same relative
|
|
49568
|
+
* path. Downstream conversion keys by that path, so a later one would
|
|
49569
|
+
* silently overwrite an earlier one. Warn instead of failing, keeping the
|
|
49570
|
+
* earlier (higher-precedence) root's file.
|
|
49571
|
+
*/
|
|
49572
|
+
claimStandaloneSubagents({ loaded, dirPath, claimedRelativeFilePaths }) {
|
|
49573
|
+
const deduped = [];
|
|
49574
|
+
for (const subagent of loaded) {
|
|
49575
|
+
const key = subagent.getImportIdentity();
|
|
49576
|
+
const claimed = claimedRelativeFilePaths.claim({
|
|
49577
|
+
identity: key,
|
|
49578
|
+
source: dirPath
|
|
49579
|
+
});
|
|
49580
|
+
if (claimed === null) {
|
|
49581
|
+
deduped.push(subagent);
|
|
49582
|
+
continue;
|
|
49583
|
+
}
|
|
49584
|
+
const keptFrom = claimed.source === dirPath ? `the earlier one in ${dirPath}` : `the one from the higher-precedence ${claimed.source}`;
|
|
49585
|
+
this.logger.warn(claimed.spelling === key ? `Duplicate ${this.toolTarget} subagent "${key}" found in ${dirPath}; keeping ${keptFrom} and ignoring this copy.` : `Duplicate ${this.toolTarget} subagent "${key}" found in ${dirPath} differs only in case from "${claimed.spelling}"; keeping ${keptFrom} and ignoring this copy.`);
|
|
49586
|
+
}
|
|
49587
|
+
return deduped;
|
|
49588
|
+
}
|
|
49589
|
+
/**
|
|
49590
|
+
* The same claim-or-warn pass for subagents defined inline in a tool's own
|
|
49591
|
+
* config file (see `loadAdditionalImportFiles`), which are scanned after
|
|
49592
|
+
* every standalone file so a Markdown file of the same name wins.
|
|
49593
|
+
*/
|
|
49594
|
+
claimInlineSubagents({ additionalSubagents, claimedRelativeFilePaths }) {
|
|
49595
|
+
const deduped = [];
|
|
49596
|
+
for (const subagent of additionalSubagents) {
|
|
49597
|
+
const key = subagent.getImportIdentity();
|
|
49598
|
+
const claimed = claimedRelativeFilePaths.claim({
|
|
49599
|
+
identity: key,
|
|
49600
|
+
source: INLINE_SOURCE
|
|
49601
|
+
});
|
|
49602
|
+
if (claimed === null) {
|
|
49603
|
+
deduped.push(subagent);
|
|
49604
|
+
continue;
|
|
49605
|
+
}
|
|
49606
|
+
const kept = claimed.source === INLINE_SOURCE ? "the earlier inline definition" : `the standalone file in ${claimed.source}`;
|
|
49607
|
+
this.logger.warn(claimed.spelling === key ? `Duplicate ${this.toolTarget} subagent "${key}" defined inline; keeping ${kept} and ignoring the inline copy.` : `Inline ${this.toolTarget} subagent "${key}" differs only in case from "${claimed.spelling}"; keeping ${kept} and ignoring the inline copy.`);
|
|
49608
|
+
}
|
|
49609
|
+
return deduped;
|
|
49610
|
+
}
|
|
49611
|
+
/**
|
|
47997
49612
|
* Implementation of abstract method from FeatureProcessor
|
|
47998
49613
|
* Return the tool targets that this processor supports
|
|
47999
49614
|
*/
|
|
@@ -50923,17 +52538,32 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
|
|
|
50923
52538
|
* Rule generator for JetBrains Junie AI coding agent
|
|
50924
52539
|
*
|
|
50925
52540
|
* Generates `.junie/AGENTS.md` files based on rulesync rule content. Junie CLI
|
|
50926
|
-
* resolves project guidelines
|
|
50927
|
-
* `AGENTS.md`
|
|
50928
|
-
*
|
|
50929
|
-
*
|
|
50930
|
-
*
|
|
50931
|
-
*
|
|
50932
|
-
*
|
|
52541
|
+
* resolves project guidelines in this order: `.junie/AGENTS.md` → root
|
|
52542
|
+
* `AGENTS.md` **combined with `.junie/playbook.md` and every
|
|
52543
|
+
* `.junie/rules/*.md`** → legacy `.junie/guidelines.md` / `.junie/guidelines/`.
|
|
52544
|
+
* The multi-file branch exists, but it is unreachable while `.junie/AGENTS.md`
|
|
52545
|
+
* is present: that file "is used exclusively and no other guidelines files are
|
|
52546
|
+
* combined with it". So emitting `.junie/rules/*.md` next to the root file
|
|
52547
|
+
* rulesync writes would produce files Junie never reads, and moving the root
|
|
52548
|
+
* output to project-root `AGENTS.md` would both change every existing output
|
|
52549
|
+
* path and collide with the `agentsmd` target. Non-root rules therefore stay
|
|
52550
|
+
* folded into the single root `.junie/AGENTS.md` by the RulesProcessor
|
|
52551
|
+
* (`nonRoot` is `undefined`, mirroring the warp / deepagents targets) — the
|
|
52552
|
+
* fold is lossless, since Junie loads that one file in full. The original
|
|
52553
|
+
* rationale for this shape was recorded in issue #2211 and re-confirmed
|
|
52554
|
+
* against the 2026-08-21 docs revision in issue #2728. The legacy
|
|
50933
52555
|
* `.junie/guidelines.md` is still accepted as an import fallback, but
|
|
50934
52556
|
* generation always targets `.junie/AGENTS.md`. Junie uses plain markdown
|
|
50935
52557
|
* without frontmatter requirements.
|
|
50936
52558
|
*
|
|
52559
|
+
* The multi-file branch is still read on **import**, though: a repo that
|
|
52560
|
+
* authors `.junie/rules/*.md` or `.junie/playbook.md` by hand — the live
|
|
52561
|
+
* layout whenever `.junie/AGENTS.md` is absent, which is exactly the state a
|
|
52562
|
+
* first `rulesync import` finds — declares those paths as `importOnlyRoots`
|
|
52563
|
+
* and imports them as non-root rules. They are never written back there:
|
|
52564
|
+
* generating `.junie/AGENTS.md` moves Junie onto the first branch, where the
|
|
52565
|
+
* folded root file carries the same content.
|
|
52566
|
+
*
|
|
50937
52567
|
* Global (user) scope writes a single `~/.junie/AGENTS.md` file. Junie merges
|
|
50938
52568
|
* these user-scope guidelines with the project guidelines (both are included
|
|
50939
52569
|
* and marked clearly).
|
|
@@ -50955,6 +52585,14 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
50955
52585
|
alternativeRoots: [{
|
|
50956
52586
|
relativeDirPath: buildToolPath(JUNIE_DIR, ".", excludeToolDir),
|
|
50957
52587
|
relativeFilePath: JUNIE_LEGACY_RULE_FILE_NAME
|
|
52588
|
+
}],
|
|
52589
|
+
importOnlyRoots: [{
|
|
52590
|
+
relativeDirPath: buildToolPath(JUNIE_DIR, JUNIE_RULES_DIR_NAME, excludeToolDir),
|
|
52591
|
+
onlyWhenRootAbsent: true
|
|
52592
|
+
}, {
|
|
52593
|
+
relativeDirPath: buildToolPath(JUNIE_DIR, ".", excludeToolDir),
|
|
52594
|
+
relativeFilePath: JUNIE_PLAYBOOK_FILE_NAME,
|
|
52595
|
+
onlyWhenRootAbsent: true
|
|
50958
52596
|
}]
|
|
50959
52597
|
};
|
|
50960
52598
|
}
|
|
@@ -50965,7 +52603,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
50965
52603
|
static isRootRelativeFilePath(relativeFilePath) {
|
|
50966
52604
|
return relativeFilePath === "AGENTS.md" || relativeFilePath === "guidelines.md";
|
|
50967
52605
|
}
|
|
50968
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
52606
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath: relativeDirPathParam, relativeFilePath, validate = true, global = false }) {
|
|
50969
52607
|
if (global) {
|
|
50970
52608
|
const paths = this.getSettablePaths({ global: true });
|
|
50971
52609
|
if (!("root" in paths) || !paths.root) throw new Error("JunieRule global settable paths must include a root path");
|
|
@@ -50979,7 +52617,8 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
50979
52617
|
root: true
|
|
50980
52618
|
});
|
|
50981
52619
|
}
|
|
50982
|
-
const
|
|
52620
|
+
const settablePaths = this.getSettablePaths();
|
|
52621
|
+
const relativeDirPath = relativeDirPathParam ?? settablePaths.root.relativeDirPath;
|
|
50983
52622
|
const relativePath = join(relativeDirPath, relativeFilePath);
|
|
50984
52623
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
50985
52624
|
return new JunieRule({
|
|
@@ -50988,7 +52627,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
50988
52627
|
relativeFilePath,
|
|
50989
52628
|
fileContent,
|
|
50990
52629
|
validate,
|
|
50991
|
-
root: JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
52630
|
+
root: relativeDirPath === settablePaths.root.relativeDirPath && JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
50992
52631
|
});
|
|
50993
52632
|
}
|
|
50994
52633
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -53340,10 +54979,23 @@ const defaultGetFactory = (target) => {
|
|
|
53340
54979
|
if (!factory) throw new Error(`Unsupported tool target: ${target}`);
|
|
53341
54980
|
return factory;
|
|
53342
54981
|
};
|
|
53343
|
-
|
|
53344
|
-
|
|
54982
|
+
/**
|
|
54983
|
+
* How many skipped import-only paths a single warning names before it
|
|
54984
|
+
* summarizes the rest. Keeps one line readable when a rules directory holds
|
|
54985
|
+
* dozens of files.
|
|
54986
|
+
*/
|
|
54987
|
+
const MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS = 10;
|
|
54988
|
+
/**
|
|
54989
|
+
* Fall back to a tool's legacy roots when its primary root file is absent.
|
|
54990
|
+
*
|
|
54991
|
+
* The primary hits are passed in rather than globbed here, so that callers
|
|
54992
|
+
* which need "the root file Rulesync generates" — rather than "whatever root
|
|
54993
|
+
* the tool will read" — can keep the two apart. A legacy root is a file
|
|
54994
|
+
* Rulesync reads but never writes, and the difference matters to them.
|
|
54995
|
+
*/
|
|
54996
|
+
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob) => {
|
|
53345
54997
|
if (primaryFilePaths.length > 0) return primaryFilePaths;
|
|
53346
|
-
if (alternativeRoots) return findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
54998
|
+
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
53347
54999
|
return [];
|
|
53348
55000
|
};
|
|
53349
55001
|
var RulesProcessor = class extends FeatureProcessor {
|
|
@@ -53355,10 +55007,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
53355
55007
|
getFactory;
|
|
53356
55008
|
skills;
|
|
53357
55009
|
featureOptions;
|
|
53358
|
-
constructor({ outputRoot = process.cwd(),
|
|
55010
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
53359
55011
|
super({
|
|
53360
55012
|
outputRoot,
|
|
53361
|
-
|
|
55013
|
+
inputRoots,
|
|
53362
55014
|
dryRun,
|
|
53363
55015
|
logger
|
|
53364
55016
|
});
|
|
@@ -53733,25 +55385,45 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
53733
55385
|
claimedBy.set(target.toLowerCase(), source);
|
|
53734
55386
|
continue;
|
|
53735
55387
|
}
|
|
53736
|
-
this.logger.warn(`Both ${previous} and ${source} import to ${join(RULESYNC_RULES_RELATIVE_DIR_PATH, target)} (compared case-insensitively, as on macOS and Windows); the last one wins wherever they collide.`);
|
|
55388
|
+
this.logger.warn(`Both ${stripControlCharacters(previous)} and ${stripControlCharacters(source)} import to ${stripControlCharacters(join(RULESYNC_RULES_RELATIVE_DIR_PATH, target))} (compared case-insensitively, as on macOS and Windows); the last one wins wherever they collide.`);
|
|
53737
55389
|
}
|
|
53738
55390
|
return rulesyncRules;
|
|
53739
55391
|
}
|
|
53740
55392
|
/**
|
|
53741
|
-
*
|
|
53742
|
-
*
|
|
55393
|
+
* Load rulesync rule files from a single source-tree's `rules/` (and
|
|
55394
|
+
* `rules/.curated/`) subtree. `sourceTree` is the source tree itself
|
|
55395
|
+
* (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`), NOT its parent.
|
|
55396
|
+
*
|
|
55397
|
+
* Intra-tree behavior — the local-wins-over-curated rule and the
|
|
55398
|
+
* case-insensitive collision handling — is preserved from the previous
|
|
55399
|
+
* single-root implementation. See `loadRulesyncFiles` for how the
|
|
55400
|
+
* per-root results are combined into the effective set.
|
|
53743
55401
|
*/
|
|
53744
|
-
async
|
|
53745
|
-
const
|
|
53746
|
-
const
|
|
55402
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
55403
|
+
const treeParent = dirname(sourceTree);
|
|
55404
|
+
const treeName = basename(sourceTree);
|
|
55405
|
+
const treeRulesDirPath = join(treeName, RULES_FEATURE_SUBDIR);
|
|
55406
|
+
const rulesyncOutputRoot = join(sourceTree, RULES_FEATURE_SUBDIR);
|
|
55407
|
+
const curatedOutputRoot = join(sourceTree, CURATED_RULES_FEATURE_SUBDIR);
|
|
53747
55408
|
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([findFilesByGlobs(join(rulesyncOutputRoot, "**", "*.md")), findFilesByGlobs(join(curatedOutputRoot, "**", "*.md"))]);
|
|
53748
55409
|
const files = [.../* @__PURE__ */ new Set([...discoveredFiles, ...discoveredCuratedFiles])];
|
|
53749
55410
|
const localFiles = files.filter((file) => !relative(rulesyncOutputRoot, file).startsWith(`.curated${sep}`));
|
|
53750
|
-
const
|
|
55411
|
+
const localRelativePathsByIdentity = groupSpellingsByCaseFoldedIdentity(localFiles.map((file) => relative(rulesyncOutputRoot, file)));
|
|
53751
55412
|
const curatedFiles = files.filter((file) => relative(rulesyncOutputRoot, file).startsWith(`.curated${sep}`)).map((file) => ({
|
|
53752
55413
|
file,
|
|
53753
55414
|
relativeFilePath: relative(curatedOutputRoot, file)
|
|
53754
|
-
})).filter(({ relativeFilePath }) =>
|
|
55415
|
+
})).filter(({ relativeFilePath }) => {
|
|
55416
|
+
const spellings = localRelativePathsByIdentity.get(caseFoldIdentity(relativeFilePath));
|
|
55417
|
+
if (spellings === void 0) return true;
|
|
55418
|
+
if (!spellings.includes(relativeFilePath)) this.logger.warn(formatCuratedCaseCollisionWarning({
|
|
55419
|
+
artifactKind: "rule",
|
|
55420
|
+
entryNoun: "file",
|
|
55421
|
+
treeDirPath: treeRulesDirPath,
|
|
55422
|
+
curatedSpelling: join(".curated", relativeFilePath),
|
|
55423
|
+
localSpellings: spellings
|
|
55424
|
+
}));
|
|
55425
|
+
return false;
|
|
55426
|
+
});
|
|
53755
55427
|
const selectedFiles = [...localFiles.map((file) => ({
|
|
53756
55428
|
file,
|
|
53757
55429
|
sourceRelativeFilePath: relative(rulesyncOutputRoot, file),
|
|
@@ -53761,25 +55433,41 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
53761
55433
|
sourceRelativeFilePath: join(".curated", relativeFilePath),
|
|
53762
55434
|
relativeFilePath
|
|
53763
55435
|
}))];
|
|
53764
|
-
this.logger.debug(`Found ${selectedFiles.length} rulesync files`);
|
|
53765
|
-
|
|
55436
|
+
this.logger.debug(`Found ${selectedFiles.length} rulesync files under ${rulesyncOutputRoot}`);
|
|
55437
|
+
return await Promise.all(selectedFiles.map(async ({ sourceRelativeFilePath, relativeFilePath }) => {
|
|
53766
55438
|
checkPathTraversal({
|
|
53767
55439
|
relativePath: sourceRelativeFilePath,
|
|
53768
55440
|
intendedRootDir: rulesyncOutputRoot
|
|
53769
55441
|
});
|
|
53770
55442
|
const rule = await RulesyncRule.fromFile({
|
|
53771
|
-
outputRoot:
|
|
55443
|
+
outputRoot: treeParent,
|
|
55444
|
+
relativeDirPath: treeRulesDirPath,
|
|
53772
55445
|
relativeFilePath: sourceRelativeFilePath
|
|
53773
55446
|
});
|
|
53774
55447
|
if (sourceRelativeFilePath === relativeFilePath) return rule;
|
|
53775
55448
|
return new RulesyncRule({
|
|
53776
|
-
outputRoot:
|
|
53777
|
-
relativeDirPath:
|
|
55449
|
+
outputRoot: treeParent,
|
|
55450
|
+
relativeDirPath: treeRulesDirPath,
|
|
53778
55451
|
relativeFilePath,
|
|
53779
55452
|
frontmatter: rule.getFrontmatter(),
|
|
53780
55453
|
body: rule.getBody()
|
|
53781
55454
|
});
|
|
53782
55455
|
}));
|
|
55456
|
+
}
|
|
55457
|
+
/**
|
|
55458
|
+
* Implementation of abstract method from FeatureProcessor
|
|
55459
|
+
* Load and parse rulesync rule files from every configured input root's
|
|
55460
|
+
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
55461
|
+
* with the same target path from a later root replaces the earlier root's
|
|
55462
|
+
* copy (case-insensitive, matching the intra-root collision handling).
|
|
55463
|
+
*/
|
|
55464
|
+
async loadRulesyncFiles() {
|
|
55465
|
+
const rulesyncRules = mergeByCaseInsensitiveIdentity({
|
|
55466
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
55467
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
55468
|
+
artifactName: "rule",
|
|
55469
|
+
logger: this.logger
|
|
55470
|
+
});
|
|
53783
55471
|
const factory = this.getFactory(this.toolTarget);
|
|
53784
55472
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
53785
55473
|
if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
|
|
@@ -53873,23 +55561,42 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
53873
55561
|
});
|
|
53874
55562
|
}).filter((rule) => rule.isDeletable());
|
|
53875
55563
|
};
|
|
55564
|
+
/**
|
|
55565
|
+
* Import counterpart of {@link buildDeletionRulesFromPaths} for the
|
|
55566
|
+
* root-shaped scans (root, legacy roots, and read-only roots), whose
|
|
55567
|
+
* paths all sit at a directory Rulesync resolves from the file itself.
|
|
55568
|
+
*/
|
|
55569
|
+
const buildImportRulesFromPaths = (filePaths) => Promise.all(filePaths.map((filePath) => {
|
|
55570
|
+
const relativeDirPath = resolveRelativeDirPath(filePath);
|
|
55571
|
+
checkPathTraversal({
|
|
55572
|
+
relativePath: relativeDirPath,
|
|
55573
|
+
intendedRootDir: this.outputRoot
|
|
55574
|
+
});
|
|
55575
|
+
return factory.class.fromFile({
|
|
55576
|
+
outputRoot: this.outputRoot,
|
|
55577
|
+
relativeDirPath,
|
|
55578
|
+
relativeFilePath: basename(filePath),
|
|
55579
|
+
global: this.global
|
|
55580
|
+
});
|
|
55581
|
+
}));
|
|
55582
|
+
/**
|
|
55583
|
+
* The tool's own root file, as opposed to whichever root
|
|
55584
|
+
* `rootToolRules` ends up resolving. A legacy root reached through
|
|
55585
|
+
* `alternativeRoots` is deliberately not counted here: it is a
|
|
55586
|
+
* hand-authored file Rulesync never writes, so it has folded nothing in,
|
|
55587
|
+
* and in Junie's resolution order it ranks *below* the multi-file layout
|
|
55588
|
+
* that `importOnlyRoots` describes. Gating those roots on it would drop
|
|
55589
|
+
* exactly the files the tool is really reading.
|
|
55590
|
+
*
|
|
55591
|
+
* Resolved once, up front, so the two blocks that need it do not depend
|
|
55592
|
+
* on each other's evaluation order.
|
|
55593
|
+
*/
|
|
55594
|
+
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", settablePaths.root.relativeFilePath)) : [];
|
|
53876
55595
|
const rootToolRules = await (async () => {
|
|
53877
55596
|
if (!settablePaths.root) return [];
|
|
53878
|
-
const uniqueRootFilePaths = await findFilesWithFallback(
|
|
55597
|
+
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, alt.relativeFilePath));
|
|
53879
55598
|
if (forDeletion) return buildDeletionRulesFromPaths(uniqueRootFilePaths);
|
|
53880
|
-
return await
|
|
53881
|
-
const relativeDirPath = resolveRelativeDirPath(filePath);
|
|
53882
|
-
checkPathTraversal({
|
|
53883
|
-
relativePath: relativeDirPath,
|
|
53884
|
-
intendedRootDir: this.outputRoot
|
|
53885
|
-
});
|
|
53886
|
-
return factory.class.fromFile({
|
|
53887
|
-
outputRoot: this.outputRoot,
|
|
53888
|
-
relativeFilePath: basename(filePath),
|
|
53889
|
-
relativeDirPath,
|
|
53890
|
-
global: this.global
|
|
53891
|
-
});
|
|
53892
|
-
}));
|
|
55599
|
+
return await buildImportRulesFromPaths(uniqueRootFilePaths);
|
|
53893
55600
|
})();
|
|
53894
55601
|
this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
|
|
53895
55602
|
const localRootToolRules = await (async () => {
|
|
@@ -53901,7 +55608,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
53901
55608
|
fileName
|
|
53902
55609
|
}));
|
|
53903
55610
|
if (!settablePaths.root) return [];
|
|
53904
|
-
return await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName));
|
|
55611
|
+
return await findFilesWithFallback(await findFilesByGlobs(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName)), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName));
|
|
53905
55612
|
})();
|
|
53906
55613
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
53907
55614
|
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
@@ -53977,6 +55684,29 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
53977
55684
|
}));
|
|
53978
55685
|
})();
|
|
53979
55686
|
this.logger.debug(`Found ${nestedToolRules.length} nested tool rule files`);
|
|
55687
|
+
const importOnlyToolRules = await (async () => {
|
|
55688
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
55689
|
+
if (forDeletion || !importOnlyRoots || importOnlyRoots.length === 0) return [];
|
|
55690
|
+
const rootFilePath = primaryRootFilePaths[0];
|
|
55691
|
+
const scannedPaths = [];
|
|
55692
|
+
const skippedPaths = [];
|
|
55693
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
55694
|
+
const matchedPaths = await findFilesByGlobs(join(this.outputRoot, importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), { type: "file" });
|
|
55695
|
+
if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
|
|
55696
|
+
skippedPaths.push(...matchedPaths);
|
|
55697
|
+
continue;
|
|
55698
|
+
}
|
|
55699
|
+
scannedPaths.push(...matchedPaths);
|
|
55700
|
+
}
|
|
55701
|
+
if (skippedPaths.length > 0 && rootFilePath !== void 0) {
|
|
55702
|
+
const skippedNames = skippedPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
|
|
55703
|
+
const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
55704
|
+
const remainingCount = skippedNames.length - listedNames.length;
|
|
55705
|
+
this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters(relative(this.outputRoot, rootFilePath))} exists, and the tool reads that file exclusively. Delete them once you have checked that content is in the root file, or move it into ${RULESYNC_RULES_RELATIVE_DIR_PATH} if it is not.`);
|
|
55706
|
+
}
|
|
55707
|
+
return await buildImportRulesFromPaths(scannedPaths);
|
|
55708
|
+
})();
|
|
55709
|
+
this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);
|
|
53980
55710
|
const nonRootToolRules = await (async () => {
|
|
53981
55711
|
if (!settablePaths.nonRoot) return [];
|
|
53982
55712
|
const nonRootOutputRoot = join(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
|
|
@@ -54009,6 +55739,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54009
55739
|
})();
|
|
54010
55740
|
this.logger.debug(`Found ${nonRootToolRules.length} non-root tool rule files`);
|
|
54011
55741
|
return [
|
|
55742
|
+
...importOnlyToolRules,
|
|
54012
55743
|
...rootToolRules,
|
|
54013
55744
|
...localRootToolRules,
|
|
54014
55745
|
...rootMirrorDeletionRules,
|
|
@@ -54816,14 +56547,53 @@ function warnUnsupportedTargets(params) {
|
|
|
54816
56547
|
}
|
|
54817
56548
|
}
|
|
54818
56549
|
/**
|
|
54819
|
-
*
|
|
54820
|
-
*
|
|
54821
|
-
*
|
|
54822
|
-
*
|
|
54823
|
-
*
|
|
56550
|
+
* Inspect every configured input-root path. The first entry is the required
|
|
56551
|
+
* base source tree; later entries are optional overlays and may be absent.
|
|
56552
|
+
* Each existing entry is a rulesync source tree itself (the directory that
|
|
56553
|
+
* directly holds `rules/`, `skills/`, `mcp.jsonc`, etc.). Existing empty
|
|
56554
|
+
* directories are valid because delete and check workflows still need to
|
|
56555
|
+
* inspect generated outputs.
|
|
54824
56556
|
*/
|
|
54825
|
-
async function
|
|
54826
|
-
|
|
56557
|
+
async function inspectInputRoots(inputRoots) {
|
|
56558
|
+
const existing = [];
|
|
56559
|
+
const missing = [];
|
|
56560
|
+
const invalidOverlays = [];
|
|
56561
|
+
const nonDirectories = /* @__PURE__ */ new Set();
|
|
56562
|
+
for (const [index, root] of inputRoots.entries()) if (await directoryExists(root)) existing.push(root);
|
|
56563
|
+
else {
|
|
56564
|
+
missing.push(root);
|
|
56565
|
+
if (await fileExists(root)) {
|
|
56566
|
+
nonDirectories.add(root);
|
|
56567
|
+
if (index > 0) invalidOverlays.push(root);
|
|
56568
|
+
}
|
|
56569
|
+
}
|
|
56570
|
+
const primaryRoot = inputRoots[0];
|
|
56571
|
+
const displayPrimaryRoot = stripControlCharacters(primaryRoot ?? "");
|
|
56572
|
+
if (primaryRoot === void 0 || existing.includes(primaryRoot)) {
|
|
56573
|
+
const invalidOverlay = invalidOverlays[0];
|
|
56574
|
+
return {
|
|
56575
|
+
existing,
|
|
56576
|
+
missing,
|
|
56577
|
+
message: invalidOverlay === void 0 ? void 0 : `Configured optional input root '${stripControlCharacters(invalidOverlay)}' exists but is not a directory.`
|
|
56578
|
+
};
|
|
56579
|
+
}
|
|
56580
|
+
const defaultRoot = join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH);
|
|
56581
|
+
if (primaryRoot === defaultRoot && !nonDirectories.has(primaryRoot)) return {
|
|
56582
|
+
existing,
|
|
56583
|
+
missing,
|
|
56584
|
+
message: `Rulesync source directory '${defaultRoot}' does not exist. Run 'rulesync init' first.`
|
|
56585
|
+
};
|
|
56586
|
+
const settingHint = `your input root setting ('inputRoots', or the deprecated 'inputRoot')`;
|
|
56587
|
+
if (nonDirectories.has(primaryRoot)) return {
|
|
56588
|
+
existing,
|
|
56589
|
+
missing,
|
|
56590
|
+
message: `Configured primary input root '${displayPrimaryRoot}' exists but is not a directory. Point ${settingHint} at a directory.`
|
|
56591
|
+
};
|
|
56592
|
+
return {
|
|
56593
|
+
existing,
|
|
56594
|
+
missing,
|
|
56595
|
+
message: `Configured primary input root '${displayPrimaryRoot}' does not exist. Create the directory or update ${settingHint}.`
|
|
56596
|
+
};
|
|
54827
56597
|
}
|
|
54828
56598
|
function dependsOnReachable(byId, from, target) {
|
|
54829
56599
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -54954,7 +56724,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
54954
56724
|
const subagentsDirPath = subagentFactory.class.getSettablePaths({ global }).relativeDirPath;
|
|
54955
56725
|
if (subagentsDirPath !== skillFactory.class.getSettablePaths({ global }).relativeDirPath) continue;
|
|
54956
56726
|
const subagentsProcessor = new SubagentsProcessor({
|
|
54957
|
-
|
|
56727
|
+
inputRoots: config.getInputRoots(),
|
|
54958
56728
|
toolTarget,
|
|
54959
56729
|
global,
|
|
54960
56730
|
logger
|
|
@@ -54962,7 +56732,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
54962
56732
|
const subagentNames = new Set((await subagentsProcessor.loadRulesyncFiles()).filter((file) => file instanceof RulesyncSubagent).filter((file) => subagentFactory.class.isTargetedByRulesyncSubagent(file)).map((file) => basename(file.getRelativeFilePath(), extname(file.getRelativeFilePath()))));
|
|
54963
56733
|
if (subagentNames.size === 0) continue;
|
|
54964
56734
|
const skillNames = (await new SkillsProcessor({
|
|
54965
|
-
|
|
56735
|
+
inputRoots: config.getInputRoots(),
|
|
54966
56736
|
toolTarget,
|
|
54967
56737
|
global,
|
|
54968
56738
|
logger
|
|
@@ -55011,6 +56781,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
55011
56781
|
*/
|
|
55012
56782
|
async function generate(params) {
|
|
55013
56783
|
const { config, logger } = params;
|
|
56784
|
+
resetRootShadowingWarnings({ logger });
|
|
55014
56785
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
55015
56786
|
toolTarget,
|
|
55016
56787
|
outputRoot
|
|
@@ -55149,7 +56920,7 @@ async function generateRulesCore(params) {
|
|
|
55149
56920
|
toolTarget,
|
|
55150
56921
|
global: config.getGlobal()
|
|
55151
56922
|
}),
|
|
55152
|
-
|
|
56923
|
+
inputRoots: config.getInputRoots(),
|
|
55153
56924
|
toolTarget,
|
|
55154
56925
|
global: config.getGlobal(),
|
|
55155
56926
|
simulateCommands: config.getSimulateCommands(),
|
|
@@ -55199,7 +56970,7 @@ async function generateIgnoreCore(params) {
|
|
|
55199
56970
|
for (const outputRoot of config.getOutputRoots(toolTarget)) try {
|
|
55200
56971
|
const processor = new IgnoreProcessor({
|
|
55201
56972
|
outputRoot,
|
|
55202
|
-
|
|
56973
|
+
inputRoots: config.getInputRoots(),
|
|
55203
56974
|
toolTarget,
|
|
55204
56975
|
global,
|
|
55205
56976
|
dryRun: config.isPreviewMode(),
|
|
@@ -55246,7 +57017,7 @@ async function generateMcpCore(params) {
|
|
|
55246
57017
|
toolTarget,
|
|
55247
57018
|
global: config.getGlobal()
|
|
55248
57019
|
}),
|
|
55249
|
-
|
|
57020
|
+
inputRoots: config.getInputRoots(),
|
|
55250
57021
|
toolTarget,
|
|
55251
57022
|
global: config.getGlobal(),
|
|
55252
57023
|
dryRun: config.isPreviewMode(),
|
|
@@ -55292,7 +57063,7 @@ async function generateCommandsCore(params) {
|
|
|
55292
57063
|
toolTarget,
|
|
55293
57064
|
global: config.getGlobal()
|
|
55294
57065
|
}),
|
|
55295
|
-
|
|
57066
|
+
inputRoots: config.getInputRoots(),
|
|
55296
57067
|
toolTarget,
|
|
55297
57068
|
global: config.getGlobal(),
|
|
55298
57069
|
dryRun: config.isPreviewMode(),
|
|
@@ -55339,7 +57110,7 @@ async function generateSubagentsCore(params) {
|
|
|
55339
57110
|
toolTarget,
|
|
55340
57111
|
global: config.getGlobal()
|
|
55341
57112
|
}),
|
|
55342
|
-
|
|
57113
|
+
inputRoots: config.getInputRoots(),
|
|
55343
57114
|
toolTarget,
|
|
55344
57115
|
global: config.getGlobal(),
|
|
55345
57116
|
dryRun: config.isPreviewMode(),
|
|
@@ -55386,7 +57157,7 @@ async function generateSkillsCore(params) {
|
|
|
55386
57157
|
toolTarget,
|
|
55387
57158
|
global: config.getGlobal()
|
|
55388
57159
|
}),
|
|
55389
|
-
|
|
57160
|
+
inputRoots: config.getInputRoots(),
|
|
55390
57161
|
toolTarget,
|
|
55391
57162
|
global: config.getGlobal(),
|
|
55392
57163
|
dryRun: config.isPreviewMode(),
|
|
@@ -55431,7 +57202,7 @@ async function generateHooksCore(params) {
|
|
|
55431
57202
|
toolTarget,
|
|
55432
57203
|
global: config.getGlobal()
|
|
55433
57204
|
}),
|
|
55434
|
-
|
|
57205
|
+
inputRoots: config.getInputRoots(),
|
|
55435
57206
|
toolTarget,
|
|
55436
57207
|
global: config.getGlobal(),
|
|
55437
57208
|
dryRun: config.isPreviewMode(),
|
|
@@ -55473,7 +57244,7 @@ async function generatePermissionsCore(params) {
|
|
|
55473
57244
|
toolTarget,
|
|
55474
57245
|
global: config.getGlobal()
|
|
55475
57246
|
}),
|
|
55476
|
-
|
|
57247
|
+
inputRoots: config.getInputRoots(),
|
|
55477
57248
|
toolTarget,
|
|
55478
57249
|
global: config.getGlobal(),
|
|
55479
57250
|
dryRun: config.isPreviewMode(),
|
|
@@ -55519,7 +57290,7 @@ async function generateChecksCore(params) {
|
|
|
55519
57290
|
toolTarget,
|
|
55520
57291
|
global: config.getGlobal()
|
|
55521
57292
|
}),
|
|
55522
|
-
|
|
57293
|
+
inputRoots: config.getInputRoots(),
|
|
55523
57294
|
toolTarget,
|
|
55524
57295
|
global: config.getGlobal(),
|
|
55525
57296
|
dryRun: config.isPreviewMode(),
|
|
@@ -55891,6 +57662,6 @@ async function importChecksCore(params) {
|
|
|
55891
57662
|
return writtenCount;
|
|
55892
57663
|
}
|
|
55893
57664
|
//#endregion
|
|
55894
|
-
export {
|
|
57665
|
+
export { SourceEntrySchema as $, RULESYNC_MCP_SCHEMA_URL as $t, RulesyncRule as A, writeFileBuffer as At, RulesyncCommandFrontmatterSchema as B, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeDirectoryStrict as Ct, RulesyncSubagentFrontmatterSchema as D, resolvePath as Dt, RulesyncSubagent as E, removeTempDirectory as Et, RulesyncHooks as F, ToolTargetSchema as Ft, SHARED_USER_MANAGED_CONFIG_PATHS as G, RULESYNC_HOOKS_FILE_NAME as Gt, RulesyncCheckFrontmatterSchema as H, RULESYNC_CONFIG_SCHEMA_URL as Ht, getRulesyncSourceCandidates as I, MAX_FILE_SIZE as It, mergeInputRootConfigs as J, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Jt, SKILL_FILE_NAME as K, RULESYNC_HOOKS_LEGACY_FILE_NAME as Kt, resolveRulesyncSourceWritePath as L, RULESYNC_AIIGNORE_FILE_NAME as Lt, RulesyncPermissions as M, ALL_TOOL_TARGETS as Mt, RulesyncMcp as N, ALL_TOOL_TARGETS_WITH_WILDCARD as Nt, RulesyncSkill as O, runWithDirectoryRollback as Ot, RulesyncIgnore as P, PACKAGING_TOOL_TARGETS as Pt, GITIGNORE_DESTINATION_KEY as Q, RULESYNC_MCP_RELATIVE_FILE_PATH as Qt, parseJsonc as R, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeDirectory as St, getLocalSkillDirNames as T, removeFileStrict as Tt, stringifyFrontmatter as U, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Ut, RulesyncCheck as V, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Vt, loadYaml as W, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Wt, CONFLICTING_TARGET_PAIRS as X, RULESYNC_MCP_FILE_NAME as Xt, resolveEffectiveInputRoots as Y, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Yt, ConfigFileSchema as Z, RULESYNC_MCP_LEGACY_FILE_NAME as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, getHomeDirectory as _t, convertFromTool as a, RULESYNC_RELATIVE_DIR_PATH as an, CLIError as at, CLAUDECODE_SKILLS_DIR_PATH as b, readFileContent as bt, SubagentsProcessor as c, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as cn, assertTreeContainsNoSymlinks as ct, IgnoreProcessor as d, ALL_FEATURES_WITH_WILDCARD as dn, createTempDirectory as dt, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as en, findControlCharacter as et, HooksProcessor as f, DEPRECATED_FEATURE_REPLACEMENTS as fn, directoryExists as ft, CLAUDECODE_DIR as g, getFileSize as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, findFilesByGlobs as ht, getProcessorRegistryEntry as i, RULESYNC_PERMISSIONS_SCHEMA_URL as in, warnOnConflictingFlags as it, RulesyncRuleFrontmatterSchema as j, writeFileContent as jt, RulesyncSkillFrontmatterSchema as k, toPosixPath as kt, SkillsProcessor as l, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as ln, assertWritablePathInsideRoot as lt, QWENCODE_DIR as m, fileExists as mt, generate as n, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as nn, JsonLogger as nt, isPackagingToolTarget as o, RULESYNC_RULES_RELATIVE_DIR_PATH as on, ErrorCodes as ot, CommandsProcessor as p, formatError as pn, ensureDir as pt, ConfigResolver as q, RULESYNC_HOOKS_RELATIVE_FILE_PATH as qt, inspectInputRoots as r, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as rn, fallbackLogger as rt, RulesProcessor as s, RULESYNC_SKILLS_RELATIVE_DIR_PATH as sn, assertDirectoryIfExists as st, importFromTool as t, RULESYNC_PERMISSIONS_FILE_NAME as tn, ConsoleLogger as tt, McpProcessor as u, ALL_FEATURES as un, checkPathTraversal as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, isSymlink as vt, stripControlCharacters as w, removeFile as wt, ChecksProcessor as x, readFileContentOrNull as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, listDirectoryFiles as yt, RulesyncCommand as z, RULESYNC_CHECKS_RELATIVE_DIR_PATH as zt };
|
|
55895
57666
|
|
|
55896
|
-
//# sourceMappingURL=import-
|
|
57667
|
+
//# sourceMappingURL=import-DimKwtQ6.js.map
|