rulesync 16.15.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 +203 -54
- package/dist/cli/index.js +203 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BPTCMtUS.js → import-DimKwtQ6.js} +1660 -399
- package/dist/import-DimKwtQ6.js.map +1 -0
- package/dist/{import-nfDFI5X1.cjs → import-NiPCjH6E.cjs} +1684 -411
- 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 +1 -1
- package/dist/import-BPTCMtUS.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
|
|
@@ -5246,15 +5658,17 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
5246
5658
|
error: null
|
|
5247
5659
|
};
|
|
5248
5660
|
}
|
|
5249
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
5661
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true }) {
|
|
5250
5662
|
const paths = RulesyncPermissions.getSettablePaths();
|
|
5663
|
+
const overrideDirPath = relativeDirPath;
|
|
5251
5664
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
5252
|
-
const
|
|
5665
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
5666
|
+
const filePath = join(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
5253
5667
|
if (!await fileExists(filePath)) continue;
|
|
5254
5668
|
const fileContent = await readFileContent(filePath);
|
|
5255
5669
|
return new RulesyncPermissions({
|
|
5256
5670
|
outputRoot,
|
|
5257
|
-
relativeDirPath:
|
|
5671
|
+
relativeDirPath: candidateDirPath,
|
|
5258
5672
|
relativeFilePath: candidate.relativeFilePath,
|
|
5259
5673
|
fileContent,
|
|
5260
5674
|
validate
|
|
@@ -5418,8 +5832,9 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5418
5832
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5419
5833
|
};
|
|
5420
5834
|
}
|
|
5421
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
5422
|
-
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);
|
|
5423
5838
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5424
5839
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5425
5840
|
const result = RulesyncRuleFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5432,7 +5847,7 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5432
5847
|
};
|
|
5433
5848
|
return new RulesyncRule({
|
|
5434
5849
|
outputRoot,
|
|
5435
|
-
relativeDirPath:
|
|
5850
|
+
relativeDirPath: dirPath,
|
|
5436
5851
|
relativeFilePath,
|
|
5437
5852
|
frontmatter: validatedFrontmatter,
|
|
5438
5853
|
body: content.trim(),
|
|
@@ -5680,7 +6095,11 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
5680
6095
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
5681
6096
|
"user-invocable": z.optional(z.boolean()),
|
|
5682
6097
|
enabled: z.optional(z.boolean()),
|
|
5683
|
-
"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())
|
|
5684
6103
|
})),
|
|
5685
6104
|
grokcli: z.optional(z.looseObject({
|
|
5686
6105
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
@@ -5856,8 +6275,9 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5856
6275
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5857
6276
|
};
|
|
5858
6277
|
}
|
|
5859
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
5860
|
-
const
|
|
6278
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
6279
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().relativeDirPath;
|
|
6280
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
5861
6281
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5862
6282
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5863
6283
|
const result = RulesyncSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5865,7 +6285,7 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5865
6285
|
const filename = basename(relativeFilePath);
|
|
5866
6286
|
return new RulesyncSubagent({
|
|
5867
6287
|
outputRoot,
|
|
5868
|
-
relativeDirPath:
|
|
6288
|
+
relativeDirPath: dirPath,
|
|
5869
6289
|
relativeFilePath: filename,
|
|
5870
6290
|
frontmatter: result.data,
|
|
5871
6291
|
body: content.trim()
|
|
@@ -5875,16 +6295,18 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5875
6295
|
//#endregion
|
|
5876
6296
|
//#region src/features/skills/skills-utils.ts
|
|
5877
6297
|
/**
|
|
5878
|
-
* 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`).
|
|
5879
6301
|
*/
|
|
5880
|
-
async function getLocalSkillDirNames(
|
|
5881
|
-
const skillsDir = join(
|
|
6302
|
+
async function getLocalSkillDirNames(sourceTree) {
|
|
6303
|
+
const skillsDir = join(sourceTree, SKILLS_FEATURE_SUBDIR);
|
|
5882
6304
|
const names = /* @__PURE__ */ new Set();
|
|
5883
6305
|
if (!await directoryExists(skillsDir)) return names;
|
|
5884
6306
|
const dirPaths = await findFilesByGlobs(join(skillsDir, "*"), { type: "dir" });
|
|
5885
6307
|
for (const dirPath of dirPaths) {
|
|
5886
6308
|
const name = basename(dirPath);
|
|
5887
|
-
if (name === basename(
|
|
6309
|
+
if (name === basename(CURATED_SKILLS_FEATURE_SUBDIR)) continue;
|
|
5888
6310
|
names.add(name);
|
|
5889
6311
|
}
|
|
5890
6312
|
return names;
|
|
@@ -6197,15 +6619,49 @@ function companionFileContentsEquivalent({ filePath, expected, existing, compose
|
|
|
6197
6619
|
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
6198
6620
|
}
|
|
6199
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
|
|
6200
6643
|
//#region src/types/feature-processor.ts
|
|
6201
6644
|
var FeatureProcessor = class {
|
|
6202
6645
|
outputRoot;
|
|
6203
|
-
|
|
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;
|
|
6204
6660
|
dryRun;
|
|
6205
6661
|
logger;
|
|
6206
|
-
constructor({ outputRoot = process.cwd(),
|
|
6662
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, logger }) {
|
|
6207
6663
|
this.outputRoot = outputRoot;
|
|
6208
|
-
this.
|
|
6664
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
6209
6665
|
this.dryRun = dryRun;
|
|
6210
6666
|
this.logger = logger;
|
|
6211
6667
|
}
|
|
@@ -6272,6 +6728,198 @@ var FeatureProcessor = class {
|
|
|
6272
6728
|
return orphanFiles.length;
|
|
6273
6729
|
}
|
|
6274
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
|
+
};
|
|
6275
6923
|
//#endregion
|
|
6276
6924
|
//#region src/constants/amp-paths.ts
|
|
6277
6925
|
const AMP_DIR = ".amp";
|
|
@@ -8594,10 +9242,10 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8594
9242
|
toolTarget;
|
|
8595
9243
|
global;
|
|
8596
9244
|
getFactory;
|
|
8597
|
-
constructor({ outputRoot = process.cwd(),
|
|
9245
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$6, dryRun = false, logger }) {
|
|
8598
9246
|
super({
|
|
8599
9247
|
outputRoot,
|
|
8600
|
-
|
|
9248
|
+
inputRoots,
|
|
8601
9249
|
dryRun,
|
|
8602
9250
|
logger
|
|
8603
9251
|
});
|
|
@@ -8634,11 +9282,15 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8634
9282
|
return toolFiles.filter((file) => file instanceof ToolCheck).flatMap((toolCheck) => toolCheck.toRulesyncChecks());
|
|
8635
9283
|
}
|
|
8636
9284
|
/**
|
|
8637
|
-
*
|
|
8638
|
-
*
|
|
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`).
|
|
8639
9288
|
*/
|
|
8640
|
-
async
|
|
8641
|
-
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);
|
|
8642
9294
|
if (!await directoryExists(checksDir)) {
|
|
8643
9295
|
this.logger.debug(`Rulesync checks directory not found: ${checksDir}`);
|
|
8644
9296
|
return [];
|
|
@@ -8654,7 +9306,8 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8654
9306
|
const filepath = join(checksDir, mdFile);
|
|
8655
9307
|
try {
|
|
8656
9308
|
const rulesyncCheck = await RulesyncCheck.fromFile({
|
|
8657
|
-
outputRoot:
|
|
9309
|
+
outputRoot: treeParent,
|
|
9310
|
+
relativeDirPath: treeChecksDirPath,
|
|
8658
9311
|
relativeFilePath: mdFile,
|
|
8659
9312
|
validate: true
|
|
8660
9313
|
});
|
|
@@ -8665,6 +9318,21 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8665
9318
|
continue;
|
|
8666
9319
|
}
|
|
8667
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
|
+
});
|
|
8668
9336
|
this.logger.debug(`Successfully loaded ${rulesyncChecks.length} rulesync checks`);
|
|
8669
9337
|
return rulesyncChecks;
|
|
8670
9338
|
}
|
|
@@ -10143,18 +10811,23 @@ function commandSlug(relativeFilePath) {
|
|
|
10143
10811
|
return basename(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
10144
10812
|
}
|
|
10145
10813
|
/**
|
|
10146
|
-
* 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/`.
|
|
10147
10820
|
*
|
|
10148
10821
|
* Used by the skills-surface `isDirOwned` hooks of tools whose commands are
|
|
10149
10822
|
* emitted as `<slug>/SKILL.md` into the skills tree: a directory matching a
|
|
10150
10823
|
* current command slug is owned by the commands feature, so the skills
|
|
10151
10824
|
* feature must neither import it as a skill nor delete it as an orphan
|
|
10152
|
-
* skill. Once the command is removed from
|
|
10153
|
-
* directory stops matching and the skills feature cleans
|
|
10154
|
-
* 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.
|
|
10155
10828
|
*/
|
|
10156
|
-
async function rulesyncCommandSlugExists({
|
|
10157
|
-
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);
|
|
10158
10831
|
}
|
|
10159
10832
|
//#endregion
|
|
10160
10833
|
//#region src/features/commands/devin-command.ts
|
|
@@ -10844,11 +11517,10 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
10844
11517
|
* output is lost — so this warns rather than failing the run the way the
|
|
10845
11518
|
* Hermes check does, where the two surfaces really do write the same path.
|
|
10846
11519
|
*/
|
|
10847
|
-
static async validateRulesyncCommands({
|
|
11520
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands, logger }) {
|
|
10848
11521
|
const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
|
|
10849
11522
|
if (commandNames.size === 0) return;
|
|
10850
|
-
const
|
|
10851
|
-
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));
|
|
10852
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.`);
|
|
10853
11525
|
}
|
|
10854
11526
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
@@ -11620,7 +12292,7 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11620
12292
|
static getExtraSharedWritePaths() {
|
|
11621
12293
|
return getHermesagentSharedConfigWritePaths();
|
|
11622
12294
|
}
|
|
11623
|
-
static async validateRulesyncCommands({
|
|
12295
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands }) {
|
|
11624
12296
|
const commandSlugs = /* @__PURE__ */ new Set();
|
|
11625
12297
|
const commandOrigins = /* @__PURE__ */ new Map();
|
|
11626
12298
|
for (const command of rulesyncCommands.filter((candidate) => this.isTargetedByRulesyncCommand(candidate))) {
|
|
@@ -11632,13 +12304,25 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11632
12304
|
commandOrigins.set(slug, origin);
|
|
11633
12305
|
commandSlugs.add(slug);
|
|
11634
12306
|
}
|
|
11635
|
-
const
|
|
11636
|
-
const
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
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));
|
|
11642
12326
|
if (collisions.length > 0) throw new Error(`Hermes command and skill slash-name collision: ${[...new Set(collisions)].toSorted().join(", ")}`);
|
|
11643
12327
|
}
|
|
11644
12328
|
static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
|
|
@@ -11771,6 +12455,8 @@ const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
|
|
|
11771
12455
|
const JUNIE_IGNORE_FILE_NAME = ".aiignore";
|
|
11772
12456
|
const JUNIE_RULE_FILE_NAME = "AGENTS.md";
|
|
11773
12457
|
const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
|
|
12458
|
+
const JUNIE_RULES_DIR_NAME = "rules";
|
|
12459
|
+
const JUNIE_PLAYBOOK_FILE_NAME = "playbook.md";
|
|
11774
12460
|
//#endregion
|
|
11775
12461
|
//#region src/features/commands/junie-command.ts
|
|
11776
12462
|
const JunieCommandFrontmatterSchema = z.looseObject({ description: z.optional(z.string()) });
|
|
@@ -13731,10 +14417,10 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13731
14417
|
global;
|
|
13732
14418
|
getFactory;
|
|
13733
14419
|
flattenedCommandNaming;
|
|
13734
|
-
constructor({ outputRoot = process.cwd(),
|
|
14420
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$5, dryRun = false, flattenedCommandNaming = "basename", logger }) {
|
|
13735
14421
|
super({
|
|
13736
14422
|
outputRoot,
|
|
13737
|
-
|
|
14423
|
+
inputRoots,
|
|
13738
14424
|
dryRun,
|
|
13739
14425
|
logger
|
|
13740
14426
|
});
|
|
@@ -13749,7 +14435,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13749
14435
|
const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
|
|
13750
14436
|
const factory = this.getFactory(this.toolTarget);
|
|
13751
14437
|
await factory.class.validateRulesyncCommands?.({
|
|
13752
|
-
|
|
14438
|
+
inputRoots: this.inputRoots,
|
|
13753
14439
|
rulesyncCommands,
|
|
13754
14440
|
logger: this.logger
|
|
13755
14441
|
});
|
|
@@ -13801,16 +14487,36 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13801
14487
|
return rel;
|
|
13802
14488
|
}
|
|
13803
14489
|
/**
|
|
13804
|
-
*
|
|
13805
|
-
*
|
|
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`).
|
|
13806
14493
|
*/
|
|
13807
|
-
async
|
|
13808
|
-
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);
|
|
13809
14499
|
const rulesyncCommandPaths = await findFilesByGlobs(join(basePath, "**", "*.md"));
|
|
13810
|
-
|
|
13811
|
-
outputRoot:
|
|
14500
|
+
return await Promise.all(rulesyncCommandPaths.map((path) => RulesyncCommand.fromFile({
|
|
14501
|
+
outputRoot: treeParent,
|
|
14502
|
+
relativeDirPath: treeCommandsDirPath,
|
|
13812
14503
|
relativeFilePath: this.safeRelativePath(basePath, path)
|
|
13813
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
|
+
});
|
|
13814
14520
|
this.logger.debug(`Successfully loaded ${rulesyncCommands.length} rulesync commands`);
|
|
13815
14521
|
return rulesyncCommands;
|
|
13816
14522
|
}
|
|
@@ -19450,6 +20156,7 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
19450
20156
|
const entry = raw;
|
|
19451
20157
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
19452
20158
|
if (vibeEvent === void 0) return null;
|
|
20159
|
+
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
19453
20160
|
const canonicalEvent = VIBE_TO_CANONICAL_EVENT_NAMES[vibeEvent] ?? vibeEvent;
|
|
19454
20161
|
const def = { type: "command" };
|
|
19455
20162
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
@@ -19998,10 +20705,10 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
19998
20705
|
var HooksProcessor = class extends FeatureProcessor {
|
|
19999
20706
|
toolTarget;
|
|
20000
20707
|
global;
|
|
20001
|
-
constructor({ outputRoot = process.cwd(),
|
|
20708
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
20002
20709
|
super({
|
|
20003
20710
|
outputRoot,
|
|
20004
|
-
|
|
20711
|
+
inputRoots,
|
|
20005
20712
|
dryRun,
|
|
20006
20713
|
logger
|
|
20007
20714
|
});
|
|
@@ -20011,9 +20718,17 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
20011
20718
|
this.global = global;
|
|
20012
20719
|
}
|
|
20013
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];
|
|
20014
20728
|
try {
|
|
20015
20729
|
return [await RulesyncHooks.fromFile({
|
|
20016
|
-
outputRoot:
|
|
20730
|
+
outputRoot: dirname(sourceTree),
|
|
20731
|
+
relativeDirPath: basename(sourceTree),
|
|
20017
20732
|
validate: true
|
|
20018
20733
|
})];
|
|
20019
20734
|
} catch (error) {
|
|
@@ -21555,10 +22270,10 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21555
22270
|
getFactory;
|
|
21556
22271
|
featureOptions;
|
|
21557
22272
|
global;
|
|
21558
|
-
constructor({ outputRoot = process.cwd(),
|
|
22273
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$4, global = false, dryRun = false, logger, featureOptions }) {
|
|
21559
22274
|
super({
|
|
21560
22275
|
outputRoot,
|
|
21561
|
-
|
|
22276
|
+
inputRoots,
|
|
21562
22277
|
dryRun,
|
|
21563
22278
|
logger
|
|
21564
22279
|
});
|
|
@@ -21575,11 +22290,35 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21575
22290
|
}
|
|
21576
22291
|
/**
|
|
21577
22292
|
* Implementation of abstract method from FeatureProcessor
|
|
21578
|
-
*
|
|
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.
|
|
21579
22307
|
*/
|
|
21580
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];
|
|
21581
22317
|
try {
|
|
21582
|
-
return [await RulesyncIgnore.fromFile({
|
|
22318
|
+
return [await RulesyncIgnore.fromFile({
|
|
22319
|
+
outputRoot: dirname(sourceTree),
|
|
22320
|
+
relativeDirPath: basename(sourceTree)
|
|
22321
|
+
})];
|
|
21583
22322
|
} catch (error) {
|
|
21584
22323
|
this.logger.error(`Failed to load rulesync ignore file (${RULESYNC_AIIGNORE_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
21585
22324
|
return [];
|
|
@@ -26614,11 +27353,86 @@ function disabledNamesOf(config) {
|
|
|
26614
27353
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
26615
27354
|
}
|
|
26616
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
|
+
/**
|
|
26617
27428
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
26618
27429
|
* `~/.rovodev/config.yml` (global). Carries `disabledMcpServers` — the key
|
|
26619
|
-
* Rovo Dev actually consults to switch a server off —
|
|
26620
|
-
*
|
|
26621
|
-
*
|
|
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.
|
|
26622
27436
|
*/
|
|
26623
27437
|
var RovodevMcpConfigYaml = class extends ToolFile {
|
|
26624
27438
|
isDeletable() {
|
|
@@ -26751,7 +27565,17 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
26751
27565
|
const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
|
|
26752
27566
|
if (mergedDisabled.length > 0) existingMcp.disabledMcpServers = mergedDisabled;
|
|
26753
27567
|
else delete existingMcp.disabledMcpServers;
|
|
26754
|
-
|
|
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 [];
|
|
26755
27579
|
const fileContent = applySharedConfigPatch({
|
|
26756
27580
|
fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
|
|
26757
27581
|
feature: "mcp",
|
|
@@ -27844,10 +28668,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27844
28668
|
toolTarget;
|
|
27845
28669
|
global;
|
|
27846
28670
|
getFactory;
|
|
27847
|
-
constructor({ outputRoot = process.cwd(),
|
|
28671
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$3, dryRun = false, logger }) {
|
|
27848
28672
|
super({
|
|
27849
28673
|
outputRoot,
|
|
27850
|
-
|
|
28674
|
+
inputRoots,
|
|
27851
28675
|
dryRun,
|
|
27852
28676
|
logger
|
|
27853
28677
|
});
|
|
@@ -27863,7 +28687,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27863
28687
|
*/
|
|
27864
28688
|
async loadRulesyncFiles() {
|
|
27865
28689
|
try {
|
|
27866
|
-
return [await RulesyncMcp.
|
|
28690
|
+
return [await RulesyncMcp.fromRoots({
|
|
28691
|
+
inputRoots: this.inputRoots,
|
|
28692
|
+
logger: this.logger
|
|
28693
|
+
})];
|
|
27867
28694
|
} catch (error) {
|
|
27868
28695
|
this.logger.error(`Failed to load a Rulesync MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
27869
28696
|
return [];
|
|
@@ -29405,27 +30232,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
29405
30232
|
return { permission };
|
|
29406
30233
|
}
|
|
29407
30234
|
//#endregion
|
|
29408
|
-
//#region src/utils/control-characters.ts
|
|
29409
|
-
/**
|
|
29410
|
-
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
29411
|
-
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
29412
|
-
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
29413
|
-
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
29414
|
-
* settings file must never reach the terminal with these intact: they let the
|
|
29415
|
-
* text forge log lines, reorder what is printed around them, or inject escape
|
|
29416
|
-
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
29417
|
-
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
29418
|
-
* place to preserve the typography of a right-to-left name.
|
|
29419
|
-
*/
|
|
29420
|
-
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
29421
|
-
/**
|
|
29422
|
-
* Removes every control character from `text` so it is safe to splice into a
|
|
29423
|
-
* log line or other terminal output.
|
|
29424
|
-
*/
|
|
29425
|
-
function stripControlCharacters(text) {
|
|
29426
|
-
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
29427
|
-
}
|
|
29428
|
-
//#endregion
|
|
29429
30235
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
29430
30236
|
/**
|
|
29431
30237
|
* Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
|
|
@@ -29511,8 +30317,8 @@ function deepMergeRecords(base, patch) {
|
|
|
29511
30317
|
*
|
|
29512
30318
|
* Deliberately NOT listed:
|
|
29513
30319
|
* - `ripgrep` / `bwrapPath` / `socatPath`: each names an executable, so
|
|
29514
|
-
* `
|
|
29515
|
-
* emitting them under `--global`.
|
|
30320
|
+
* `CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL` refuses them in both scopes
|
|
30321
|
+
* rather than emitting them under `--global`.
|
|
29516
30322
|
* - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
|
|
29517
30323
|
* unit is the individual entry's mode, not the settings key, and the same
|
|
29518
30324
|
* lists carry `deny` entries that project settings *do* honor — dropping a
|
|
@@ -29531,6 +30337,30 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
|
29531
30337
|
["allowAppleEvents"]
|
|
29532
30338
|
];
|
|
29533
30339
|
/**
|
|
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
|
+
/**
|
|
29534
30364
|
* Walks `segments` from `root`, returning the record they name or `undefined` if
|
|
29535
30365
|
* any step is missing or not a record. Shared by everything below that addresses
|
|
29536
30366
|
* a `sandbox` path, so a nested path added to one of the tables is actually
|
|
@@ -29577,6 +30407,18 @@ function deleteSandboxPath({ target, path }) {
|
|
|
29577
30407
|
return true;
|
|
29578
30408
|
}
|
|
29579
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.
|
|
30414
|
+
*/
|
|
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
|
+
/**
|
|
29580
30422
|
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
29581
30423
|
* than the default. `plan` and `default` are absent because they do not widen
|
|
29582
30424
|
* anything.
|
|
@@ -29589,14 +30431,22 @@ const CLAUDECODE_WIDENING_DEFAULT_MODES = {
|
|
|
29589
30431
|
/**
|
|
29590
30432
|
* The `permissions` fields that widen rather than restrict: a `defaultMode` that
|
|
29591
30433
|
* removes prompts, and `additionalDirectories`, which moves the
|
|
29592
|
-
* working-directory boundary.
|
|
29593
|
-
* shareable permissions file should not loosen the permission system quietly.
|
|
30434
|
+
* working-directory boundary. Reported for the same reason `disableAllHooks` is:
|
|
30435
|
+
* a shareable permissions file should not loosen the permission system quietly.
|
|
29594
30436
|
*/
|
|
29595
|
-
function
|
|
30437
|
+
function collectWideningPermissionFields({ fields }) {
|
|
30438
|
+
const entries = [];
|
|
29596
30439
|
const defaultMode = fields.defaultMode;
|
|
29597
|
-
if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, 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
|
+
});
|
|
29598
30444
|
const additionalDirectories = fields.additionalDirectories;
|
|
29599
|
-
if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0))
|
|
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;
|
|
29600
30450
|
}
|
|
29601
30451
|
/**
|
|
29602
30452
|
* `sandbox` paths whose value names a binary Claude Code runs. `sandbox` has its
|
|
@@ -29613,6 +30463,18 @@ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
|
29613
30463
|
["socatPath"]
|
|
29614
30464
|
];
|
|
29615
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
|
+
/**
|
|
29616
30478
|
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
29617
30479
|
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
29618
30480
|
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
@@ -29632,77 +30494,77 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
29632
30494
|
{
|
|
29633
30495
|
path: ["allowAppleEvents"],
|
|
29634
30496
|
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
29635
|
-
widens:
|
|
30497
|
+
widens: isNotFalse
|
|
29636
30498
|
},
|
|
29637
30499
|
{
|
|
29638
30500
|
path: ["allowUnsandboxedCommands"],
|
|
29639
30501
|
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
29640
|
-
widens:
|
|
30502
|
+
widens: isNotFalse
|
|
29641
30503
|
},
|
|
29642
30504
|
{
|
|
29643
30505
|
path: ["autoAllowBashIfSandboxed"],
|
|
29644
30506
|
reason: "controls whether every Bash command the sandbox accepts runs without a prompt",
|
|
29645
|
-
widens:
|
|
30507
|
+
widens: isNotFalse
|
|
29646
30508
|
},
|
|
29647
30509
|
{
|
|
29648
30510
|
path: ["enableWeakerNestedSandbox"],
|
|
29649
30511
|
reason: "runs the Linux sandbox inside an unprivileged container, which weakens it",
|
|
29650
|
-
widens:
|
|
30512
|
+
widens: isNotFalse
|
|
29651
30513
|
},
|
|
29652
30514
|
{
|
|
29653
30515
|
path: ["enableWeakerNetworkIsolation"],
|
|
29654
30516
|
reason: "weakens the sandbox's network isolation on macOS",
|
|
29655
|
-
widens:
|
|
30517
|
+
widens: isNotFalse
|
|
29656
30518
|
},
|
|
29657
30519
|
{
|
|
29658
30520
|
path: ["enabled"],
|
|
29659
30521
|
reason: "turns the sandbox on, and sandboxed Bash commands then run without a permission prompt unless `autoAllowBashIfSandboxed` is false",
|
|
29660
|
-
widens:
|
|
30522
|
+
widens: isNotFalse
|
|
29661
30523
|
},
|
|
29662
30524
|
{
|
|
29663
30525
|
path: ["excludedCommands"],
|
|
29664
30526
|
reason: "names commands that always run outside the sandbox, with no sandbox policy applied",
|
|
29665
|
-
widens:
|
|
30527
|
+
widens: isNonEmptyList
|
|
29666
30528
|
},
|
|
29667
30529
|
{
|
|
29668
30530
|
path: ["filesystem", "allowRead"],
|
|
29669
30531
|
reason: "re-opens reading inside a region the sandbox's `denyRead` blocks",
|
|
29670
|
-
widens:
|
|
30532
|
+
widens: isNonEmptyList
|
|
29671
30533
|
},
|
|
29672
30534
|
{
|
|
29673
30535
|
path: ["filesystem", "allowWrite"],
|
|
29674
30536
|
reason: "adds paths sandboxed commands may write to, outside the working directory",
|
|
29675
|
-
widens:
|
|
30537
|
+
widens: isNonEmptyList
|
|
29676
30538
|
},
|
|
29677
30539
|
{
|
|
29678
30540
|
path: ["ignoreViolations"],
|
|
29679
30541
|
reason: "hides the sandbox violations it names, so a blocked access stops being reported",
|
|
29680
|
-
widens: (value) =>
|
|
30542
|
+
widens: (value) => isNonEmptyMap(value) && isNotFalse(value)
|
|
29681
30543
|
},
|
|
29682
30544
|
{
|
|
29683
30545
|
path: ["network", "allowAllUnixSockets"],
|
|
29684
30546
|
reason: "lets sandboxed commands connect to every Unix socket",
|
|
29685
|
-
widens:
|
|
30547
|
+
widens: isNotFalse
|
|
29686
30548
|
},
|
|
29687
30549
|
{
|
|
29688
30550
|
path: ["network", "allowedDomains"],
|
|
29689
30551
|
reason: "pre-allows domains sandboxed commands may reach without a prompt",
|
|
29690
|
-
widens:
|
|
30552
|
+
widens: isNonEmptyList
|
|
29691
30553
|
},
|
|
29692
30554
|
{
|
|
29693
30555
|
path: ["network", "allowLocalBinding"],
|
|
29694
30556
|
reason: "lets sandboxed commands bind local ports",
|
|
29695
|
-
widens:
|
|
30557
|
+
widens: isNotFalse
|
|
29696
30558
|
},
|
|
29697
30559
|
{
|
|
29698
30560
|
path: ["network", "allowMachLookup"],
|
|
29699
30561
|
reason: "names the macOS services sandboxed commands may reach, and `*` means every service",
|
|
29700
|
-
widens:
|
|
30562
|
+
widens: isNonEmptyList
|
|
29701
30563
|
},
|
|
29702
30564
|
{
|
|
29703
30565
|
path: ["network", "allowUnixSockets"],
|
|
29704
30566
|
reason: "names Unix sockets sandboxed commands may reach, and one such as `/var/run/docker.sock` is host access",
|
|
29705
|
-
widens:
|
|
30567
|
+
widens: isNonEmptyList
|
|
29706
30568
|
},
|
|
29707
30569
|
{
|
|
29708
30570
|
path: ["network", "httpProxyPort"],
|
|
@@ -29716,11 +30578,12 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
29716
30578
|
}
|
|
29717
30579
|
];
|
|
29718
30580
|
/**
|
|
29719
|
-
*
|
|
29720
|
-
*
|
|
29721
|
-
*
|
|
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.
|
|
29722
30584
|
*/
|
|
29723
|
-
function
|
|
30585
|
+
function collectTrustAffectingSandboxPaths({ sandbox }) {
|
|
30586
|
+
const entries = [];
|
|
29724
30587
|
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
29725
30588
|
const leaf = path.at(-1);
|
|
29726
30589
|
if (leaf === void 0) continue;
|
|
@@ -29731,38 +30594,49 @@ function warnOnTrustAffectingSandboxPaths({ sandbox, relativeFilePath, logger })
|
|
|
29731
30594
|
if (parent === void 0) continue;
|
|
29732
30595
|
const value = parent[leaf];
|
|
29733
30596
|
if (value === void 0 || !widens(value)) continue;
|
|
29734
|
-
|
|
30597
|
+
entries.push({
|
|
30598
|
+
label: `sandbox.${path.join(".")}`,
|
|
30599
|
+
reason
|
|
30600
|
+
});
|
|
29735
30601
|
}
|
|
30602
|
+
return entries;
|
|
29736
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
|
+
};
|
|
29737
30609
|
/**
|
|
29738
|
-
*
|
|
29739
|
-
*
|
|
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.
|
|
29740
30613
|
*/
|
|
29741
|
-
|
|
29742
|
-
|
|
29743
|
-
|
|
29744
|
-
|
|
29745
|
-
|
|
29746
|
-
|
|
29747
|
-
|
|
29748
|
-
|
|
29749
|
-
|
|
29750
|
-
return filtered;
|
|
29751
|
-
}
|
|
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
|
+
};
|
|
29752
30623
|
/**
|
|
29753
|
-
* Copy of the authored `sandbox` override with
|
|
29754
|
-
* removed, warning once per dropped path. Only the override copy is
|
|
29755
|
-
* a value already hand-written in the target file is left untouched,
|
|
29756
|
-
* the `qwencode` `security.allowPrivateNetworkHooks` precedent.
|
|
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.
|
|
29757
30628
|
*/
|
|
29758
|
-
function
|
|
30629
|
+
function stripSandboxPaths({ sandbox, refusals, relativeFilePath, logger }) {
|
|
29759
30630
|
const filtered = structuredClone(sandbox);
|
|
29760
|
-
for (const path of
|
|
30631
|
+
for (const { paths, warn } of refusals) for (const path of paths) {
|
|
29761
30632
|
if (!deleteSandboxPath({
|
|
29762
30633
|
target: filtered,
|
|
29763
30634
|
path
|
|
29764
30635
|
})) continue;
|
|
29765
|
-
logger?.warn(
|
|
30636
|
+
logger?.warn(warn({
|
|
30637
|
+
label: `sandbox.${path.join(".")}`,
|
|
30638
|
+
relativeFilePath
|
|
30639
|
+
}));
|
|
29766
30640
|
}
|
|
29767
30641
|
return filtered;
|
|
29768
30642
|
}
|
|
@@ -29786,7 +30660,7 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
|
|
|
29786
30660
|
* such entries to `deny`), so the "reads as masked but isn't" state this guards
|
|
29787
30661
|
* against cannot slip through a differently-spelled value.
|
|
29788
30662
|
*
|
|
29789
|
-
* Like `
|
|
30663
|
+
* Like `stripSandboxPaths`, only the override copy is filtered — a
|
|
29790
30664
|
* value already in the target file is left untouched, which is why the warning
|
|
29791
30665
|
* points at it.
|
|
29792
30666
|
*
|
|
@@ -29953,6 +30827,9 @@ const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
|
29953
30827
|
allowedHttpHookUrls: "limits which URLs an HTTP hook may target, and an empty list means every URL",
|
|
29954
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",
|
|
29955
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",
|
|
29956
30833
|
disableAllHooks: "controls whether hooks run at all",
|
|
29957
30834
|
disableSkillShellExecution: "re-opens the inline shell commands in a skill or custom command that a user setting had turned off",
|
|
29958
30835
|
enableAllProjectMcpServers: "auto-approves every server in the project `.mcp.json`",
|
|
@@ -29961,17 +30838,58 @@ const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
|
29961
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",
|
|
29962
30839
|
extraKnownMarketplaces: "registers plugin marketplace sources",
|
|
29963
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",
|
|
29964
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",
|
|
29965
30845
|
skipAutoPermissionPrompt: "removes the confirmation shown before auto-approval mode starts",
|
|
29966
|
-
skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check 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"
|
|
29967
30848
|
};
|
|
29968
30849
|
/**
|
|
29969
30850
|
* The keys from the table above that only widen at one particular value.
|
|
29970
30851
|
* `disableSkillShellExecution: true` turns inline shell execution off, which
|
|
29971
|
-
* restricts;
|
|
29972
|
-
* use to undo a user setting, so only
|
|
29973
|
-
|
|
29974
|
-
|
|
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
|
+
} };
|
|
29975
30893
|
/**
|
|
29976
30894
|
* A key name is authored data that ends up in a log line, so strip the control
|
|
29977
30895
|
* characters that would let it forge a line or hide the warnings beside it, and
|
|
@@ -29999,12 +30917,13 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
|
|
|
29999
30917
|
/**
|
|
30000
30918
|
* Copy of the authored top-level passthrough with the keys the target file
|
|
30001
30919
|
* cannot honor removed, warning once per dropped key. Like
|
|
30002
|
-
* `
|
|
30920
|
+
* `stripSandboxPaths`, only the override copy is filtered — a value
|
|
30003
30921
|
* already hand-written in the target file is left untouched, which is why the
|
|
30004
30922
|
* warning points at it.
|
|
30005
30923
|
*/
|
|
30006
30924
|
function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logger }) {
|
|
30007
30925
|
const filtered = {};
|
|
30926
|
+
const trustAffecting = [];
|
|
30008
30927
|
for (const [key, value] of Object.entries(overrides)) {
|
|
30009
30928
|
const shown = displayKey(key);
|
|
30010
30929
|
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
@@ -30020,11 +30939,22 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
|
|
|
30020
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.`);
|
|
30021
30940
|
continue;
|
|
30022
30941
|
}
|
|
30023
|
-
const
|
|
30024
|
-
if (
|
|
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
|
+
});
|
|
30025
30952
|
filtered[key] = value;
|
|
30026
30953
|
}
|
|
30027
|
-
return
|
|
30954
|
+
return {
|
|
30955
|
+
filtered,
|
|
30956
|
+
trustAffecting
|
|
30957
|
+
};
|
|
30028
30958
|
}
|
|
30029
30959
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
30030
30960
|
Write: "Edit",
|
|
@@ -30094,15 +31024,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30094
31024
|
config,
|
|
30095
31025
|
logger
|
|
30096
31026
|
});
|
|
31027
|
+
const trustAffecting = [];
|
|
30097
31028
|
const overridePermissions = config.claudecode?.permissions;
|
|
30098
31029
|
if (overridePermissions && typeof overridePermissions === "object") {
|
|
30099
31030
|
const { allow: _a, ask: _k, deny: _d, ...rest } = overridePermissions;
|
|
30100
31031
|
const nonListFields = Object.fromEntries(Object.entries(rest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
30101
|
-
|
|
30102
|
-
fields: nonListFields,
|
|
30103
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30104
|
-
logger
|
|
30105
|
-
});
|
|
31032
|
+
trustAffecting.push(...collectWideningPermissionFields({ fields: nonListFields }));
|
|
30106
31033
|
settings.permissions = {
|
|
30107
31034
|
...settings.permissions,
|
|
30108
31035
|
...nonListFields
|
|
@@ -30110,25 +31037,22 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30110
31037
|
}
|
|
30111
31038
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
30112
31039
|
if (isPlainRecord(overrideSandbox)) {
|
|
30113
|
-
const
|
|
31040
|
+
const honorableSandbox = stripSandboxPaths({
|
|
30114
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
|
+
],
|
|
30115
31047
|
relativeFilePath: paths.relativeFilePath,
|
|
30116
31048
|
logger
|
|
30117
31049
|
});
|
|
30118
|
-
const scopedSandbox = global ?
|
|
30119
|
-
sandbox:
|
|
30120
|
-
sandbox: executableFreeSandbox,
|
|
30121
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30122
|
-
logger
|
|
30123
|
-
}),
|
|
30124
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30125
|
-
logger
|
|
30126
|
-
});
|
|
30127
|
-
warnOnTrustAffectingSandboxPaths({
|
|
30128
|
-
sandbox: scopedSandbox,
|
|
31050
|
+
const scopedSandbox = global ? honorableSandbox : stripProjectIgnoredMaskEntries({
|
|
31051
|
+
sandbox: honorableSandbox,
|
|
30129
31052
|
relativeFilePath: paths.relativeFilePath,
|
|
30130
31053
|
logger
|
|
30131
31054
|
});
|
|
31055
|
+
trustAffecting.push(...collectTrustAffectingSandboxPaths({ sandbox: scopedSandbox }));
|
|
30132
31056
|
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
30133
31057
|
}
|
|
30134
31058
|
const overrideTopLevel = {};
|
|
@@ -30138,13 +31062,19 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30138
31062
|
if (value === void 0) continue;
|
|
30139
31063
|
overrideTopLevel[key] = value;
|
|
30140
31064
|
}
|
|
30141
|
-
const scopedTopLevel = stripUnhonoredTopLevelKeys({
|
|
31065
|
+
const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
|
|
30142
31066
|
overrides: overrideTopLevel,
|
|
30143
31067
|
global,
|
|
30144
31068
|
relativeFilePath: paths.relativeFilePath,
|
|
30145
31069
|
logger
|
|
30146
31070
|
});
|
|
31071
|
+
trustAffecting.push(...trustAffectingTopLevel);
|
|
30147
31072
|
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
31073
|
+
warnOnTrustAffectingEntries({
|
|
31074
|
+
entries: trustAffecting,
|
|
31075
|
+
relativeFilePath: paths.relativeFilePath,
|
|
31076
|
+
logger
|
|
31077
|
+
});
|
|
30148
31078
|
const managedToolNames = managedClaudeToolNames(config);
|
|
30149
31079
|
const merged = applyPermissions({
|
|
30150
31080
|
settings,
|
|
@@ -35122,11 +36052,8 @@ const TOOL_KEY_TO_CATEGORY = {
|
|
|
35122
36052
|
updateConfluencePage: "edit"
|
|
35123
36053
|
};
|
|
35124
36054
|
const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
|
|
35125
|
-
const OWNED_TOOL_PERMISSION_KEYS = [
|
|
35126
|
-
|
|
35127
|
-
"allowedExternalPaths",
|
|
35128
|
-
"default"
|
|
35129
|
-
];
|
|
36055
|
+
const OWNED_TOOL_PERMISSION_KEYS = ["allowedExternalPaths", "default"];
|
|
36056
|
+
const MANAGED_BASH_KEYS = ["default", "commands"];
|
|
35130
36057
|
/**
|
|
35131
36058
|
* Permissions adapter for Rovo Dev CLI.
|
|
35132
36059
|
*
|
|
@@ -35262,6 +36189,22 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
35262
36189
|
}
|
|
35263
36190
|
};
|
|
35264
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
|
+
/**
|
|
35265
36208
|
* Resolve the `toolPermissions` block to write, merging the generated levels
|
|
35266
36209
|
* over the existing file. Every other top-level key of `config.yml` is the
|
|
35267
36210
|
* caller's to preserve; inside this block, keys rulesync manages are owned and
|
|
@@ -35269,6 +36212,11 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
35269
36212
|
*/
|
|
35270
36213
|
function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
|
|
35271
36214
|
const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
|
|
36215
|
+
warnAboutPreservedSandboxOptOut({
|
|
36216
|
+
existingToolPermissions,
|
|
36217
|
+
filePath,
|
|
36218
|
+
logger
|
|
36219
|
+
});
|
|
35272
36220
|
if (Object.keys(generated).length === 0 && sourceStatesRules) {
|
|
35273
36221
|
if (!isRecord$1(existing)) return;
|
|
35274
36222
|
const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
|
|
@@ -35277,9 +36225,12 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35277
36225
|
}
|
|
35278
36226
|
const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
|
|
35279
36227
|
const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
|
|
36228
|
+
const hasExistingBashRecord = isRecord$1(existingToolPermissions.bash);
|
|
36229
|
+
const existingBash = hasExistingBashRecord ? { ...existingToolPermissions.bash } : {};
|
|
35280
36230
|
warnAboutDroppedOwnedKeys({
|
|
35281
36231
|
existingToolPermissions,
|
|
35282
36232
|
existingTools,
|
|
36233
|
+
existingBash,
|
|
35283
36234
|
generated,
|
|
35284
36235
|
filePath,
|
|
35285
36236
|
logger
|
|
@@ -35289,15 +36240,22 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35289
36240
|
delete existingToolPermissions[toolKey];
|
|
35290
36241
|
delete existingTools[toolKey];
|
|
35291
36242
|
}
|
|
36243
|
+
for (const bashKey of MANAGED_BASH_KEYS) delete existingBash[bashKey];
|
|
35292
36244
|
const tools = {
|
|
35293
36245
|
...existingTools,
|
|
35294
36246
|
...generated.tools
|
|
35295
36247
|
};
|
|
36248
|
+
const bash = {
|
|
36249
|
+
...existingBash,
|
|
36250
|
+
...generated.bash
|
|
36251
|
+
};
|
|
35296
36252
|
if (hasExistingToolsRecord) delete existingToolPermissions.tools;
|
|
36253
|
+
if (hasExistingBashRecord) delete existingToolPermissions.bash;
|
|
35297
36254
|
return {
|
|
35298
36255
|
...existingToolPermissions,
|
|
35299
36256
|
...generated,
|
|
35300
|
-
...Object.keys(tools).length > 0 ? { tools } : {}
|
|
36257
|
+
...Object.keys(tools).length > 0 ? { tools } : {},
|
|
36258
|
+
...Object.keys(bash).length > 0 ? { bash } : {}
|
|
35301
36259
|
};
|
|
35302
36260
|
}
|
|
35303
36261
|
/**
|
|
@@ -35306,9 +36264,14 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35306
36264
|
* `/directories` and by an "always allow" answer to a prompt — so their removal
|
|
35307
36265
|
* must not be silent.
|
|
35308
36266
|
*/
|
|
35309
|
-
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, generated, filePath, logger }) {
|
|
36267
|
+
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, existingBash, generated, filePath, logger }) {
|
|
35310
36268
|
const newTools = generated.tools ?? {};
|
|
35311
|
-
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
|
+
];
|
|
35312
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.`);
|
|
35313
36276
|
}
|
|
35314
36277
|
/**
|
|
@@ -37416,10 +38379,10 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
37416
38379
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
37417
38380
|
toolTarget;
|
|
37418
38381
|
global;
|
|
37419
|
-
constructor({ outputRoot = process.cwd(),
|
|
38382
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
37420
38383
|
super({
|
|
37421
38384
|
outputRoot,
|
|
37422
|
-
|
|
38385
|
+
inputRoots,
|
|
37423
38386
|
dryRun,
|
|
37424
38387
|
logger
|
|
37425
38388
|
});
|
|
@@ -37429,9 +38392,17 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
37429
38392
|
this.global = global;
|
|
37430
38393
|
}
|
|
37431
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];
|
|
37432
38402
|
try {
|
|
37433
38403
|
return [await RulesyncPermissions.fromFile({
|
|
37434
|
-
outputRoot:
|
|
38404
|
+
outputRoot: dirname(sourceTree),
|
|
38405
|
+
relativeDirPath: basename(sourceTree),
|
|
37435
38406
|
validate: true
|
|
37436
38407
|
})];
|
|
37437
38408
|
} catch (error) {
|
|
@@ -37855,13 +38826,26 @@ var RovodevSkill = class RovodevSkill extends ToolSkill {
|
|
|
37855
38826
|
//#region src/types/dir-feature-processor.ts
|
|
37856
38827
|
var DirFeatureProcessor = class {
|
|
37857
38828
|
outputRoot;
|
|
37858
|
-
|
|
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;
|
|
37859
38843
|
dryRun;
|
|
37860
38844
|
avoidBlockScalars;
|
|
37861
38845
|
logger;
|
|
37862
|
-
constructor({ outputRoot = process.cwd(),
|
|
38846
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, avoidBlockScalars = false, logger }) {
|
|
37863
38847
|
this.outputRoot = outputRoot;
|
|
37864
|
-
this.
|
|
38848
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [join(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
37865
38849
|
this.dryRun = dryRun;
|
|
37866
38850
|
this.avoidBlockScalars = avoidBlockScalars;
|
|
37867
38851
|
this.logger = logger;
|
|
@@ -40120,9 +41104,9 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
40120
41104
|
* slug is owned by the commands feature: it must not be imported as a
|
|
40121
41105
|
* skill nor deleted as an orphan skill.
|
|
40122
41106
|
*/
|
|
40123
|
-
static async isDirOwned({ dirName,
|
|
41107
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
40124
41108
|
return !await rulesyncCommandSlugExists({
|
|
40125
|
-
|
|
41109
|
+
inputRoots,
|
|
40126
41110
|
dirName
|
|
40127
41111
|
});
|
|
40128
41112
|
}
|
|
@@ -40172,7 +41156,11 @@ const FactorydroidSkillFrontmatterSchema = z.looseObject({
|
|
|
40172
41156
|
"user-invocable": z.optional(z.boolean()),
|
|
40173
41157
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
40174
41158
|
enabled: z.optional(z.boolean()),
|
|
40175
|
-
"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())
|
|
40176
41164
|
});
|
|
40177
41165
|
/**
|
|
40178
41166
|
* Represents a Factory Droid skill directory.
|
|
@@ -40225,16 +41213,10 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
40225
41213
|
};
|
|
40226
41214
|
}
|
|
40227
41215
|
toRulesyncSkill() {
|
|
40228
|
-
const
|
|
40229
|
-
const factorydroidBlock = {
|
|
40230
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
40231
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
40232
|
-
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
40233
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
40234
|
-
};
|
|
41216
|
+
const { name, description, ...factorydroidBlock } = this.getFrontmatter();
|
|
40235
41217
|
const rulesyncFrontmatter = {
|
|
40236
|
-
name
|
|
40237
|
-
description
|
|
41218
|
+
name,
|
|
41219
|
+
description,
|
|
40238
41220
|
targets: ["*"],
|
|
40239
41221
|
...Object.keys(factorydroidBlock).length > 0 && { factorydroid: factorydroidBlock }
|
|
40240
41222
|
};
|
|
@@ -40261,13 +41243,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
40261
41243
|
rootFrontmatter: rulesyncFrontmatter,
|
|
40262
41244
|
section: factorydroidSection
|
|
40263
41245
|
});
|
|
41246
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = factorydroidSection ?? {};
|
|
40264
41247
|
const factorydroidFrontmatter = {
|
|
40265
41248
|
name: rulesyncFrontmatter.name,
|
|
40266
41249
|
description: rulesyncFrontmatter.description,
|
|
41250
|
+
...section,
|
|
40267
41251
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
40268
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
40269
|
-
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
40270
|
-
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
41252
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
40271
41253
|
};
|
|
40272
41254
|
return new FactorydroidSkill({
|
|
40273
41255
|
outputRoot,
|
|
@@ -40679,7 +41661,10 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
40679
41661
|
}
|
|
40680
41662
|
}
|
|
40681
41663
|
static getSettablePaths(_options) {
|
|
40682
|
-
return {
|
|
41664
|
+
return {
|
|
41665
|
+
relativeDirPath: JUNIE_SKILLS_DIR_PATH,
|
|
41666
|
+
importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
|
|
41667
|
+
};
|
|
40683
41668
|
}
|
|
40684
41669
|
getFrontmatter() {
|
|
40685
41670
|
return JunieSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
@@ -42841,9 +43826,9 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
42841
43826
|
* slug is owned by the commands feature: it must not be imported as a
|
|
42842
43827
|
* skill nor deleted as an orphan skill.
|
|
42843
43828
|
*/
|
|
42844
|
-
static async isDirOwned({ dirName,
|
|
43829
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
42845
43830
|
return !await rulesyncCommandSlugExists({
|
|
42846
|
-
|
|
43831
|
+
inputRoots,
|
|
42847
43832
|
dirName
|
|
42848
43833
|
});
|
|
42849
43834
|
}
|
|
@@ -43401,10 +44386,10 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43401
44386
|
toolTarget;
|
|
43402
44387
|
global;
|
|
43403
44388
|
getFactory;
|
|
43404
|
-
constructor({ outputRoot = process.cwd(),
|
|
44389
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
43405
44390
|
super({
|
|
43406
44391
|
outputRoot,
|
|
43407
|
-
|
|
44392
|
+
inputRoots,
|
|
43408
44393
|
dryRun,
|
|
43409
44394
|
avoidBlockScalars: toolTarget === "cursor",
|
|
43410
44395
|
logger
|
|
@@ -43442,39 +44427,67 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43442
44427
|
return rulesyncSkills;
|
|
43443
44428
|
}
|
|
43444
44429
|
/**
|
|
43445
|
-
*
|
|
43446
|
-
*
|
|
43447
|
-
*
|
|
43448
|
-
*
|
|
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.
|
|
43449
44434
|
*/
|
|
43450
|
-
async
|
|
43451
|
-
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)];
|
|
43452
44441
|
const localSkills = await Promise.all(localDirNames.map((dirName) => RulesyncSkill.fromDir({
|
|
43453
|
-
outputRoot:
|
|
44442
|
+
outputRoot: treeParent,
|
|
44443
|
+
relativeDirPath: treeSkillsDirPath,
|
|
43454
44444
|
dirName,
|
|
43455
44445
|
global: this.global
|
|
43456
44446
|
})));
|
|
43457
|
-
const
|
|
43458
|
-
const curatedDirPath = join(
|
|
44447
|
+
const localSkillNamesByIdentity = groupSpellingsByCaseFoldedIdentity(localDirNames);
|
|
44448
|
+
const curatedDirPath = join(sourceTree, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
43459
44449
|
let curatedSkills = [];
|
|
43460
44450
|
if (await directoryExists(curatedDirPath)) {
|
|
43461
44451
|
const nonConflicting = (await findFilesByGlobs(join(curatedDirPath, "*"), { type: "dir" })).map((path) => basename(path)).filter((name) => {
|
|
43462
|
-
|
|
43463
|
-
|
|
43464
|
-
|
|
43465
|
-
|
|
43466
|
-
|
|
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;
|
|
43467
44463
|
});
|
|
43468
|
-
const curatedRelativeDirPath = RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH;
|
|
43469
44464
|
curatedSkills = await Promise.all(nonConflicting.map((dirName) => RulesyncSkill.fromDir({
|
|
43470
|
-
outputRoot:
|
|
43471
|
-
relativeDirPath:
|
|
44465
|
+
outputRoot: treeParent,
|
|
44466
|
+
relativeDirPath: treeCuratedSkillsDirPath,
|
|
43472
44467
|
dirName,
|
|
43473
44468
|
global: this.global
|
|
43474
44469
|
})));
|
|
43475
44470
|
}
|
|
43476
|
-
|
|
43477
|
-
|
|
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`);
|
|
43478
44491
|
return allSkills;
|
|
43479
44492
|
}
|
|
43480
44493
|
/**
|
|
@@ -43490,7 +44503,17 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43490
44503
|
}) : [];
|
|
43491
44504
|
const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
|
|
43492
44505
|
const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
|
|
43493
|
-
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
|
+
};
|
|
43494
44517
|
const toolSkills = [];
|
|
43495
44518
|
for (const root of roots) {
|
|
43496
44519
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
@@ -43506,54 +44529,60 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43506
44529
|
outputRoot: rootOutputRoot,
|
|
43507
44530
|
relativeDirPath,
|
|
43508
44531
|
dirName,
|
|
43509
|
-
|
|
44532
|
+
inputRoots: this.inputRoots
|
|
43510
44533
|
})) continue;
|
|
43511
44534
|
ownedDirNames.push(dirName);
|
|
43512
44535
|
}
|
|
43513
44536
|
const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
|
|
44537
|
+
const sourcePath = join(relativeDirPath, dirName);
|
|
43514
44538
|
try {
|
|
43515
|
-
return
|
|
43516
|
-
|
|
43517
|
-
|
|
43518
|
-
|
|
43519
|
-
|
|
43520
|
-
|
|
44539
|
+
return {
|
|
44540
|
+
skill: await factory.class.fromDir({
|
|
44541
|
+
outputRoot: rootOutputRoot,
|
|
44542
|
+
relativeDirPath,
|
|
44543
|
+
dirName,
|
|
44544
|
+
global: this.global
|
|
44545
|
+
}),
|
|
44546
|
+
sourcePath
|
|
44547
|
+
};
|
|
43521
44548
|
} catch (error) {
|
|
43522
44549
|
if (!isLenientRoot) throw error;
|
|
43523
|
-
this.logger.warn(`Skipping ${
|
|
44550
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43524
44551
|
return null;
|
|
43525
44552
|
}
|
|
43526
|
-
}))).filter((
|
|
43527
|
-
for (const skill of directorySkills) {
|
|
43528
|
-
|
|
43529
|
-
|
|
43530
|
-
|
|
43531
|
-
|
|
43532
|
-
}
|
|
44553
|
+
}))).filter((loaded) => loaded !== null);
|
|
44554
|
+
for (const { skill, sourcePath } of directorySkills) if (claimSkillName({
|
|
44555
|
+
skill,
|
|
44556
|
+
relativeDirPath,
|
|
44557
|
+
sourcePath
|
|
44558
|
+
})) toolSkills.push(skill);
|
|
43533
44559
|
if (!factory.class.fromFlatFile) continue;
|
|
43534
44560
|
const fromFlatFile = factory.class.fromFlatFile;
|
|
43535
44561
|
const directoryStems = new Set(ownedDirNames);
|
|
43536
44562
|
const flatFilePaths = (await findFilesByGlobs(join(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has(basename(filePath, ".md")));
|
|
43537
44563
|
const flatSkills = (await Promise.all(flatFilePaths.map(async (filePath) => {
|
|
44564
|
+
const sourcePath = join(relativeDirPath, basename(filePath));
|
|
43538
44565
|
try {
|
|
43539
|
-
return
|
|
43540
|
-
|
|
43541
|
-
|
|
43542
|
-
|
|
43543
|
-
|
|
43544
|
-
|
|
44566
|
+
return {
|
|
44567
|
+
skill: await fromFlatFile({
|
|
44568
|
+
outputRoot: rootOutputRoot,
|
|
44569
|
+
relativeDirPath,
|
|
44570
|
+
relativeFilePath: basename(filePath),
|
|
44571
|
+
global: this.global
|
|
44572
|
+
}),
|
|
44573
|
+
sourcePath
|
|
44574
|
+
};
|
|
43545
44575
|
} catch (error) {
|
|
43546
44576
|
if (!isLenientRoot) throw error;
|
|
43547
|
-
this.logger.warn(`Skipping ${
|
|
44577
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43548
44578
|
return null;
|
|
43549
44579
|
}
|
|
43550
|
-
}))).filter((
|
|
43551
|
-
for (const skill of flatSkills) {
|
|
43552
|
-
|
|
43553
|
-
|
|
43554
|
-
|
|
43555
|
-
|
|
43556
|
-
}
|
|
44580
|
+
}))).filter((loaded) => loaded !== null);
|
|
44581
|
+
for (const { skill, sourcePath } of flatSkills) if (claimSkillName({
|
|
44582
|
+
skill,
|
|
44583
|
+
relativeDirPath,
|
|
44584
|
+
sourcePath
|
|
44585
|
+
})) toolSkills.push(skill);
|
|
43557
44586
|
}
|
|
43558
44587
|
this.logger.debug(`Successfully loaded ${toolSkills.length} skills from ${roots.length} root(s)`);
|
|
43559
44588
|
return toolSkills;
|
|
@@ -43583,7 +44612,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43583
44612
|
outputRoot: this.outputRoot,
|
|
43584
44613
|
relativeDirPath: root,
|
|
43585
44614
|
dirName,
|
|
43586
|
-
|
|
44615
|
+
inputRoots: this.inputRoots
|
|
43587
44616
|
})) continue;
|
|
43588
44617
|
const toolSkill = factory.class.forDeletion({
|
|
43589
44618
|
outputRoot: this.outputRoot,
|
|
@@ -46341,6 +47370,7 @@ const JunieSubagentFrontmatterSchema = z.looseObject({
|
|
|
46341
47370
|
disallowedTools: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
46342
47371
|
mcpServers: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
46343
47372
|
model: z.optional(z.string()),
|
|
47373
|
+
permissionMode: z.optional(z.string()),
|
|
46344
47374
|
reasoningLevel: z.optional(z.string()),
|
|
46345
47375
|
maxTurns: z.optional(z.number()),
|
|
46346
47376
|
skills: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
@@ -48321,14 +49351,25 @@ const subagentsProcessorToolTargetsSimulated = allToolTargetKeys$1.filter((targe
|
|
|
48321
49351
|
const subagentsProcessorToolTargetsGlobal = allToolTargetKeys$1.filter((target) => {
|
|
48322
49352
|
return toolSubagentFactories.get(target)?.meta.supportsGlobal ?? false;
|
|
48323
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>";
|
|
48324
49365
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
48325
49366
|
toolTarget;
|
|
48326
49367
|
global;
|
|
48327
49368
|
getFactory;
|
|
48328
|
-
constructor({ outputRoot = process.cwd(),
|
|
49369
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
48329
49370
|
super({
|
|
48330
49371
|
outputRoot,
|
|
48331
|
-
|
|
49372
|
+
inputRoots,
|
|
48332
49373
|
dryRun,
|
|
48333
49374
|
logger
|
|
48334
49375
|
});
|
|
@@ -48374,24 +49415,31 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48374
49415
|
rulesyncSubagents.push(toolSubagent.toRulesyncSubagent());
|
|
48375
49416
|
}
|
|
48376
49417
|
const uniqueRulesyncSubagents = [];
|
|
48377
|
-
const
|
|
49418
|
+
const claimedOutputPaths = new ClaimedIdentities();
|
|
48378
49419
|
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
48379
49420
|
const outputPath = join(rulesyncSubagent.getRelativeDirPath(), rulesyncSubagent.getRelativeFilePath());
|
|
48380
|
-
|
|
48381
|
-
|
|
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.`);
|
|
48382
49427
|
continue;
|
|
48383
49428
|
}
|
|
48384
|
-
seenOutputPaths.add(outputPath);
|
|
48385
49429
|
uniqueRulesyncSubagents.push(rulesyncSubagent);
|
|
48386
49430
|
}
|
|
48387
49431
|
return uniqueRulesyncSubagents;
|
|
48388
49432
|
}
|
|
48389
49433
|
/**
|
|
48390
|
-
*
|
|
48391
|
-
*
|
|
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`).
|
|
48392
49437
|
*/
|
|
48393
|
-
async
|
|
48394
|
-
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);
|
|
48395
49443
|
if (!await directoryExists(subagentsDir)) {
|
|
48396
49444
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
48397
49445
|
return [];
|
|
@@ -48407,7 +49455,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48407
49455
|
const filepath = join(subagentsDir, mdFile);
|
|
48408
49456
|
try {
|
|
48409
49457
|
const rulesyncSubagent = await RulesyncSubagent.fromFile({
|
|
48410
|
-
outputRoot:
|
|
49458
|
+
outputRoot: treeParent,
|
|
49459
|
+
relativeDirPath: treeSubagentsDirPath,
|
|
48411
49460
|
relativeFilePath: mdFile,
|
|
48412
49461
|
validate: true
|
|
48413
49462
|
});
|
|
@@ -48418,8 +49467,24 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48418
49467
|
continue;
|
|
48419
49468
|
}
|
|
48420
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
|
+
});
|
|
48421
49486
|
if (rulesyncSubagents.length === 0) {
|
|
48422
|
-
this.logger.debug(`No valid subagents found
|
|
49487
|
+
this.logger.debug(`No valid subagents found`);
|
|
48423
49488
|
return [];
|
|
48424
49489
|
}
|
|
48425
49490
|
this.logger.debug(`Successfully loaded ${rulesyncSubagents.length} rulesync subagents`);
|
|
@@ -48434,7 +49499,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48434
49499
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
48435
49500
|
const roots = forDeletion ? [paths.relativeDirPath] : [paths.relativeDirPath, ...paths.importDirPaths ?? []];
|
|
48436
49501
|
const toolSubagents = [];
|
|
48437
|
-
const
|
|
49502
|
+
const claimedRelativeFilePaths = new ClaimedIdentities();
|
|
48438
49503
|
for (const root of roots) {
|
|
48439
49504
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
48440
49505
|
const dirPath = typeof root === "string" ? root : root.relativeDirPath;
|
|
@@ -48473,37 +49538,77 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48473
49538
|
relativeFilePath: toRelativeFilePath(path),
|
|
48474
49539
|
global: this.global
|
|
48475
49540
|
})));
|
|
48476
|
-
|
|
48477
|
-
|
|
48478
|
-
|
|
48479
|
-
|
|
48480
|
-
|
|
48481
|
-
continue;
|
|
48482
|
-
}
|
|
48483
|
-
seenRelativeFilePaths.add(key);
|
|
48484
|
-
deduped.push(subagent);
|
|
48485
|
-
}
|
|
48486
|
-
toolSubagents.push(...deduped);
|
|
49541
|
+
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
49542
|
+
loaded,
|
|
49543
|
+
dirPath,
|
|
49544
|
+
claimedRelativeFilePaths
|
|
49545
|
+
}));
|
|
48487
49546
|
}
|
|
48488
49547
|
if (!forDeletion && factory.class.loadAdditionalImportFiles) {
|
|
48489
49548
|
const additionalSubagents = await factory.class.loadAdditionalImportFiles({
|
|
48490
49549
|
outputRoot: this.outputRoot,
|
|
48491
49550
|
global: this.global
|
|
48492
49551
|
});
|
|
48493
|
-
|
|
48494
|
-
|
|
48495
|
-
|
|
48496
|
-
|
|
48497
|
-
continue;
|
|
48498
|
-
}
|
|
48499
|
-
seenRelativeFilePaths.add(key);
|
|
48500
|
-
toolSubagents.push(subagent);
|
|
48501
|
-
}
|
|
49552
|
+
toolSubagents.push(...this.claimInlineSubagents({
|
|
49553
|
+
additionalSubagents,
|
|
49554
|
+
claimedRelativeFilePaths
|
|
49555
|
+
}));
|
|
48502
49556
|
}
|
|
48503
49557
|
this.logger.debug(`Successfully loaded ${toolSubagents.length} ${this.toolTarget} subagents from ${roots.length} root(s)`);
|
|
48504
49558
|
return toolSubagents;
|
|
48505
49559
|
}
|
|
48506
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
|
+
/**
|
|
48507
49612
|
* Implementation of abstract method from FeatureProcessor
|
|
48508
49613
|
* Return the tool targets that this processor supports
|
|
48509
49614
|
*/
|
|
@@ -51433,17 +52538,32 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
|
|
|
51433
52538
|
* Rule generator for JetBrains Junie AI coding agent
|
|
51434
52539
|
*
|
|
51435
52540
|
* Generates `.junie/AGENTS.md` files based on rulesync rule content. Junie CLI
|
|
51436
|
-
* resolves project guidelines
|
|
51437
|
-
* `AGENTS.md`
|
|
51438
|
-
*
|
|
51439
|
-
*
|
|
51440
|
-
*
|
|
51441
|
-
*
|
|
51442
|
-
*
|
|
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
|
|
51443
52555
|
* `.junie/guidelines.md` is still accepted as an import fallback, but
|
|
51444
52556
|
* generation always targets `.junie/AGENTS.md`. Junie uses plain markdown
|
|
51445
52557
|
* without frontmatter requirements.
|
|
51446
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
|
+
*
|
|
51447
52567
|
* Global (user) scope writes a single `~/.junie/AGENTS.md` file. Junie merges
|
|
51448
52568
|
* these user-scope guidelines with the project guidelines (both are included
|
|
51449
52569
|
* and marked clearly).
|
|
@@ -51465,6 +52585,14 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51465
52585
|
alternativeRoots: [{
|
|
51466
52586
|
relativeDirPath: buildToolPath(JUNIE_DIR, ".", excludeToolDir),
|
|
51467
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
|
|
51468
52596
|
}]
|
|
51469
52597
|
};
|
|
51470
52598
|
}
|
|
@@ -51475,7 +52603,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51475
52603
|
static isRootRelativeFilePath(relativeFilePath) {
|
|
51476
52604
|
return relativeFilePath === "AGENTS.md" || relativeFilePath === "guidelines.md";
|
|
51477
52605
|
}
|
|
51478
|
-
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 }) {
|
|
51479
52607
|
if (global) {
|
|
51480
52608
|
const paths = this.getSettablePaths({ global: true });
|
|
51481
52609
|
if (!("root" in paths) || !paths.root) throw new Error("JunieRule global settable paths must include a root path");
|
|
@@ -51489,7 +52617,8 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51489
52617
|
root: true
|
|
51490
52618
|
});
|
|
51491
52619
|
}
|
|
51492
|
-
const
|
|
52620
|
+
const settablePaths = this.getSettablePaths();
|
|
52621
|
+
const relativeDirPath = relativeDirPathParam ?? settablePaths.root.relativeDirPath;
|
|
51493
52622
|
const relativePath = join(relativeDirPath, relativeFilePath);
|
|
51494
52623
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
51495
52624
|
return new JunieRule({
|
|
@@ -51498,7 +52627,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51498
52627
|
relativeFilePath,
|
|
51499
52628
|
fileContent,
|
|
51500
52629
|
validate,
|
|
51501
|
-
root: JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
52630
|
+
root: relativeDirPath === settablePaths.root.relativeDirPath && JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
51502
52631
|
});
|
|
51503
52632
|
}
|
|
51504
52633
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -53850,10 +54979,23 @@ const defaultGetFactory = (target) => {
|
|
|
53850
54979
|
if (!factory) throw new Error(`Unsupported tool target: ${target}`);
|
|
53851
54980
|
return factory;
|
|
53852
54981
|
};
|
|
53853
|
-
|
|
53854
|
-
|
|
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) => {
|
|
53855
54997
|
if (primaryFilePaths.length > 0) return primaryFilePaths;
|
|
53856
|
-
if (alternativeRoots) return findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
54998
|
+
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
53857
54999
|
return [];
|
|
53858
55000
|
};
|
|
53859
55001
|
var RulesProcessor = class extends FeatureProcessor {
|
|
@@ -53865,10 +55007,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
53865
55007
|
getFactory;
|
|
53866
55008
|
skills;
|
|
53867
55009
|
featureOptions;
|
|
53868
|
-
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 }) {
|
|
53869
55011
|
super({
|
|
53870
55012
|
outputRoot,
|
|
53871
|
-
|
|
55013
|
+
inputRoots,
|
|
53872
55014
|
dryRun,
|
|
53873
55015
|
logger
|
|
53874
55016
|
});
|
|
@@ -54243,25 +55385,45 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54243
55385
|
claimedBy.set(target.toLowerCase(), source);
|
|
54244
55386
|
continue;
|
|
54245
55387
|
}
|
|
54246
|
-
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.`);
|
|
54247
55389
|
}
|
|
54248
55390
|
return rulesyncRules;
|
|
54249
55391
|
}
|
|
54250
55392
|
/**
|
|
54251
|
-
*
|
|
54252
|
-
*
|
|
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.
|
|
54253
55401
|
*/
|
|
54254
|
-
async
|
|
54255
|
-
const
|
|
54256
|
-
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);
|
|
54257
55408
|
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([findFilesByGlobs(join(rulesyncOutputRoot, "**", "*.md")), findFilesByGlobs(join(curatedOutputRoot, "**", "*.md"))]);
|
|
54258
55409
|
const files = [.../* @__PURE__ */ new Set([...discoveredFiles, ...discoveredCuratedFiles])];
|
|
54259
55410
|
const localFiles = files.filter((file) => !relative(rulesyncOutputRoot, file).startsWith(`.curated${sep}`));
|
|
54260
|
-
const
|
|
55411
|
+
const localRelativePathsByIdentity = groupSpellingsByCaseFoldedIdentity(localFiles.map((file) => relative(rulesyncOutputRoot, file)));
|
|
54261
55412
|
const curatedFiles = files.filter((file) => relative(rulesyncOutputRoot, file).startsWith(`.curated${sep}`)).map((file) => ({
|
|
54262
55413
|
file,
|
|
54263
55414
|
relativeFilePath: relative(curatedOutputRoot, file)
|
|
54264
|
-
})).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
|
+
});
|
|
54265
55427
|
const selectedFiles = [...localFiles.map((file) => ({
|
|
54266
55428
|
file,
|
|
54267
55429
|
sourceRelativeFilePath: relative(rulesyncOutputRoot, file),
|
|
@@ -54271,25 +55433,41 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54271
55433
|
sourceRelativeFilePath: join(".curated", relativeFilePath),
|
|
54272
55434
|
relativeFilePath
|
|
54273
55435
|
}))];
|
|
54274
|
-
this.logger.debug(`Found ${selectedFiles.length} rulesync files`);
|
|
54275
|
-
|
|
55436
|
+
this.logger.debug(`Found ${selectedFiles.length} rulesync files under ${rulesyncOutputRoot}`);
|
|
55437
|
+
return await Promise.all(selectedFiles.map(async ({ sourceRelativeFilePath, relativeFilePath }) => {
|
|
54276
55438
|
checkPathTraversal({
|
|
54277
55439
|
relativePath: sourceRelativeFilePath,
|
|
54278
55440
|
intendedRootDir: rulesyncOutputRoot
|
|
54279
55441
|
});
|
|
54280
55442
|
const rule = await RulesyncRule.fromFile({
|
|
54281
|
-
outputRoot:
|
|
55443
|
+
outputRoot: treeParent,
|
|
55444
|
+
relativeDirPath: treeRulesDirPath,
|
|
54282
55445
|
relativeFilePath: sourceRelativeFilePath
|
|
54283
55446
|
});
|
|
54284
55447
|
if (sourceRelativeFilePath === relativeFilePath) return rule;
|
|
54285
55448
|
return new RulesyncRule({
|
|
54286
|
-
outputRoot:
|
|
54287
|
-
relativeDirPath:
|
|
55449
|
+
outputRoot: treeParent,
|
|
55450
|
+
relativeDirPath: treeRulesDirPath,
|
|
54288
55451
|
relativeFilePath,
|
|
54289
55452
|
frontmatter: rule.getFrontmatter(),
|
|
54290
55453
|
body: rule.getBody()
|
|
54291
55454
|
});
|
|
54292
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
|
+
});
|
|
54293
55471
|
const factory = this.getFactory(this.toolTarget);
|
|
54294
55472
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
54295
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}.`);
|
|
@@ -54383,23 +55561,42 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54383
55561
|
});
|
|
54384
55562
|
}).filter((rule) => rule.isDeletable());
|
|
54385
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)) : [];
|
|
54386
55595
|
const rootToolRules = await (async () => {
|
|
54387
55596
|
if (!settablePaths.root) return [];
|
|
54388
|
-
const uniqueRootFilePaths = await findFilesWithFallback(
|
|
55597
|
+
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, alt.relativeFilePath));
|
|
54389
55598
|
if (forDeletion) return buildDeletionRulesFromPaths(uniqueRootFilePaths);
|
|
54390
|
-
return await
|
|
54391
|
-
const relativeDirPath = resolveRelativeDirPath(filePath);
|
|
54392
|
-
checkPathTraversal({
|
|
54393
|
-
relativePath: relativeDirPath,
|
|
54394
|
-
intendedRootDir: this.outputRoot
|
|
54395
|
-
});
|
|
54396
|
-
return factory.class.fromFile({
|
|
54397
|
-
outputRoot: this.outputRoot,
|
|
54398
|
-
relativeFilePath: basename(filePath),
|
|
54399
|
-
relativeDirPath,
|
|
54400
|
-
global: this.global
|
|
54401
|
-
});
|
|
54402
|
-
}));
|
|
55599
|
+
return await buildImportRulesFromPaths(uniqueRootFilePaths);
|
|
54403
55600
|
})();
|
|
54404
55601
|
this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
|
|
54405
55602
|
const localRootToolRules = await (async () => {
|
|
@@ -54411,7 +55608,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54411
55608
|
fileName
|
|
54412
55609
|
}));
|
|
54413
55610
|
if (!settablePaths.root) return [];
|
|
54414
|
-
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));
|
|
54415
55612
|
})();
|
|
54416
55613
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
54417
55614
|
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
@@ -54487,6 +55684,29 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54487
55684
|
}));
|
|
54488
55685
|
})();
|
|
54489
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`);
|
|
54490
55710
|
const nonRootToolRules = await (async () => {
|
|
54491
55711
|
if (!settablePaths.nonRoot) return [];
|
|
54492
55712
|
const nonRootOutputRoot = join(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
|
|
@@ -54519,6 +55739,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54519
55739
|
})();
|
|
54520
55740
|
this.logger.debug(`Found ${nonRootToolRules.length} non-root tool rule files`);
|
|
54521
55741
|
return [
|
|
55742
|
+
...importOnlyToolRules,
|
|
54522
55743
|
...rootToolRules,
|
|
54523
55744
|
...localRootToolRules,
|
|
54524
55745
|
...rootMirrorDeletionRules,
|
|
@@ -55326,14 +56547,53 @@ function warnUnsupportedTargets(params) {
|
|
|
55326
56547
|
}
|
|
55327
56548
|
}
|
|
55328
56549
|
/**
|
|
55329
|
-
*
|
|
55330
|
-
*
|
|
55331
|
-
*
|
|
55332
|
-
*
|
|
55333
|
-
*
|
|
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.
|
|
55334
56556
|
*/
|
|
55335
|
-
async function
|
|
55336
|
-
|
|
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
|
+
};
|
|
55337
56597
|
}
|
|
55338
56598
|
function dependsOnReachable(byId, from, target) {
|
|
55339
56599
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -55464,7 +56724,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
55464
56724
|
const subagentsDirPath = subagentFactory.class.getSettablePaths({ global }).relativeDirPath;
|
|
55465
56725
|
if (subagentsDirPath !== skillFactory.class.getSettablePaths({ global }).relativeDirPath) continue;
|
|
55466
56726
|
const subagentsProcessor = new SubagentsProcessor({
|
|
55467
|
-
|
|
56727
|
+
inputRoots: config.getInputRoots(),
|
|
55468
56728
|
toolTarget,
|
|
55469
56729
|
global,
|
|
55470
56730
|
logger
|
|
@@ -55472,7 +56732,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
55472
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()))));
|
|
55473
56733
|
if (subagentNames.size === 0) continue;
|
|
55474
56734
|
const skillNames = (await new SkillsProcessor({
|
|
55475
|
-
|
|
56735
|
+
inputRoots: config.getInputRoots(),
|
|
55476
56736
|
toolTarget,
|
|
55477
56737
|
global,
|
|
55478
56738
|
logger
|
|
@@ -55521,6 +56781,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
55521
56781
|
*/
|
|
55522
56782
|
async function generate(params) {
|
|
55523
56783
|
const { config, logger } = params;
|
|
56784
|
+
resetRootShadowingWarnings({ logger });
|
|
55524
56785
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
55525
56786
|
toolTarget,
|
|
55526
56787
|
outputRoot
|
|
@@ -55659,7 +56920,7 @@ async function generateRulesCore(params) {
|
|
|
55659
56920
|
toolTarget,
|
|
55660
56921
|
global: config.getGlobal()
|
|
55661
56922
|
}),
|
|
55662
|
-
|
|
56923
|
+
inputRoots: config.getInputRoots(),
|
|
55663
56924
|
toolTarget,
|
|
55664
56925
|
global: config.getGlobal(),
|
|
55665
56926
|
simulateCommands: config.getSimulateCommands(),
|
|
@@ -55709,7 +56970,7 @@ async function generateIgnoreCore(params) {
|
|
|
55709
56970
|
for (const outputRoot of config.getOutputRoots(toolTarget)) try {
|
|
55710
56971
|
const processor = new IgnoreProcessor({
|
|
55711
56972
|
outputRoot,
|
|
55712
|
-
|
|
56973
|
+
inputRoots: config.getInputRoots(),
|
|
55713
56974
|
toolTarget,
|
|
55714
56975
|
global,
|
|
55715
56976
|
dryRun: config.isPreviewMode(),
|
|
@@ -55756,7 +57017,7 @@ async function generateMcpCore(params) {
|
|
|
55756
57017
|
toolTarget,
|
|
55757
57018
|
global: config.getGlobal()
|
|
55758
57019
|
}),
|
|
55759
|
-
|
|
57020
|
+
inputRoots: config.getInputRoots(),
|
|
55760
57021
|
toolTarget,
|
|
55761
57022
|
global: config.getGlobal(),
|
|
55762
57023
|
dryRun: config.isPreviewMode(),
|
|
@@ -55802,7 +57063,7 @@ async function generateCommandsCore(params) {
|
|
|
55802
57063
|
toolTarget,
|
|
55803
57064
|
global: config.getGlobal()
|
|
55804
57065
|
}),
|
|
55805
|
-
|
|
57066
|
+
inputRoots: config.getInputRoots(),
|
|
55806
57067
|
toolTarget,
|
|
55807
57068
|
global: config.getGlobal(),
|
|
55808
57069
|
dryRun: config.isPreviewMode(),
|
|
@@ -55849,7 +57110,7 @@ async function generateSubagentsCore(params) {
|
|
|
55849
57110
|
toolTarget,
|
|
55850
57111
|
global: config.getGlobal()
|
|
55851
57112
|
}),
|
|
55852
|
-
|
|
57113
|
+
inputRoots: config.getInputRoots(),
|
|
55853
57114
|
toolTarget,
|
|
55854
57115
|
global: config.getGlobal(),
|
|
55855
57116
|
dryRun: config.isPreviewMode(),
|
|
@@ -55896,7 +57157,7 @@ async function generateSkillsCore(params) {
|
|
|
55896
57157
|
toolTarget,
|
|
55897
57158
|
global: config.getGlobal()
|
|
55898
57159
|
}),
|
|
55899
|
-
|
|
57160
|
+
inputRoots: config.getInputRoots(),
|
|
55900
57161
|
toolTarget,
|
|
55901
57162
|
global: config.getGlobal(),
|
|
55902
57163
|
dryRun: config.isPreviewMode(),
|
|
@@ -55941,7 +57202,7 @@ async function generateHooksCore(params) {
|
|
|
55941
57202
|
toolTarget,
|
|
55942
57203
|
global: config.getGlobal()
|
|
55943
57204
|
}),
|
|
55944
|
-
|
|
57205
|
+
inputRoots: config.getInputRoots(),
|
|
55945
57206
|
toolTarget,
|
|
55946
57207
|
global: config.getGlobal(),
|
|
55947
57208
|
dryRun: config.isPreviewMode(),
|
|
@@ -55983,7 +57244,7 @@ async function generatePermissionsCore(params) {
|
|
|
55983
57244
|
toolTarget,
|
|
55984
57245
|
global: config.getGlobal()
|
|
55985
57246
|
}),
|
|
55986
|
-
|
|
57247
|
+
inputRoots: config.getInputRoots(),
|
|
55987
57248
|
toolTarget,
|
|
55988
57249
|
global: config.getGlobal(),
|
|
55989
57250
|
dryRun: config.isPreviewMode(),
|
|
@@ -56029,7 +57290,7 @@ async function generateChecksCore(params) {
|
|
|
56029
57290
|
toolTarget,
|
|
56030
57291
|
global: config.getGlobal()
|
|
56031
57292
|
}),
|
|
56032
|
-
|
|
57293
|
+
inputRoots: config.getInputRoots(),
|
|
56033
57294
|
toolTarget,
|
|
56034
57295
|
global: config.getGlobal(),
|
|
56035
57296
|
dryRun: config.isPreviewMode(),
|
|
@@ -56401,6 +57662,6 @@ async function importChecksCore(params) {
|
|
|
56401
57662
|
return writtenCount;
|
|
56402
57663
|
}
|
|
56403
57664
|
//#endregion
|
|
56404
|
-
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 };
|
|
56405
57666
|
|
|
56406
|
-
//# sourceMappingURL=import-
|
|
57667
|
+
//# sourceMappingURL=import-DimKwtQ6.js.map
|