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
|
@@ -134,27 +134,34 @@ const isFeatureValueEnabled = (value) => {
|
|
|
134
134
|
};
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/constants/rulesync-paths.ts
|
|
137
|
-
const { join: join$
|
|
137
|
+
const { join: join$291 } = node_path.posix;
|
|
138
138
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
139
139
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
140
140
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
join$
|
|
150
|
-
const
|
|
151
|
-
const
|
|
141
|
+
const RULES_FEATURE_SUBDIR = "rules";
|
|
142
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$291(RULES_FEATURE_SUBDIR, ".curated");
|
|
143
|
+
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
144
|
+
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
145
|
+
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
146
|
+
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
147
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$291(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
148
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
149
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
150
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
151
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
152
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
153
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
154
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
155
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
156
|
+
join$291(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
157
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
158
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
152
159
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
153
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
160
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
154
161
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
155
162
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
156
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
157
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
163
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
164
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$291(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
158
165
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
159
166
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
160
167
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -1142,6 +1149,7 @@ const ConfigParamsSchema = zod_mini.z.object({
|
|
|
1142
1149
|
dryRun: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
1143
1150
|
check: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
1144
1151
|
inputRoot: (0, zod_mini.optional)(zod_mini.z.string()),
|
|
1152
|
+
inputRoots: (0, zod_mini.optional)(zod_mini.z.array(zod_mini.z.string()).check((0, zod_mini.minLength)(1, "inputRoots must be non-empty"))),
|
|
1145
1153
|
sources: (0, zod_mini.optional)(zod_mini.z.array(SourceEntrySchema))
|
|
1146
1154
|
});
|
|
1147
1155
|
zod_mini.z.partial(ConfigParamsSchema);
|
|
@@ -1154,14 +1162,39 @@ zod_mini.z.required(ConfigParamsSchema);
|
|
|
1154
1162
|
* Normalizes the configuration file location to an absolute path.
|
|
1155
1163
|
*
|
|
1156
1164
|
* `ConfigResolver` always supplies the path it actually loaded; the fallback
|
|
1157
|
-
* only covers direct programmatic construction
|
|
1158
|
-
*
|
|
1165
|
+
* only covers direct programmatic construction. `anchorDir` is the directory
|
|
1166
|
+
* the config file lives next to — for the default `.rulesync/` layout this
|
|
1167
|
+
* is the parent of the primary source tree.
|
|
1159
1168
|
*/
|
|
1160
|
-
function normalizeConfigFilePath({ configFilePath,
|
|
1161
|
-
if (configFilePath === void 0) return (0, node_path.join)(
|
|
1169
|
+
function normalizeConfigFilePath({ configFilePath, anchorDir }) {
|
|
1170
|
+
if (configFilePath === void 0) return (0, node_path.join)(anchorDir, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
|
|
1162
1171
|
return (0, node_path.isAbsolute)(configFilePath) ? configFilePath : (0, node_path.resolve)(configFilePath);
|
|
1163
1172
|
}
|
|
1164
1173
|
/**
|
|
1174
|
+
* Resolves any accepted input-root shape (`inputRoot`, `inputRoots`, or
|
|
1175
|
+
* neither) to the canonical non-empty tuple of absolute paths that
|
|
1176
|
+
* `Config` stores. Relative entries are resolved against the current
|
|
1177
|
+
* working directory at call time.
|
|
1178
|
+
*
|
|
1179
|
+
* Semantics (post-refactor):
|
|
1180
|
+
* - Each entry in `inputRoots` is a rulesync **source tree** (the directory
|
|
1181
|
+
* that directly holds `rules/`, `skills/`, `mcp.jsonc`, etc.). No implicit
|
|
1182
|
+
* `.rulesync/` join is applied.
|
|
1183
|
+
* - The legacy singular `inputRoot` is a shorthand for "parent of the
|
|
1184
|
+
* default `.rulesync/` source tree", and is expanded to
|
|
1185
|
+
* `[join(inputRoot, ".rulesync")]` before hitting any consumer. This is
|
|
1186
|
+
* the ONLY place `.rulesync` is appended by convention.
|
|
1187
|
+
* - The "nothing configured" default expands to `[join(cwd, ".rulesync")]`
|
|
1188
|
+
* so existing projects with a single `.rulesync/` tree keep working
|
|
1189
|
+
* unchanged.
|
|
1190
|
+
*
|
|
1191
|
+
* Callers must have already run `assertInputRootFieldsExclusive`.
|
|
1192
|
+
*/
|
|
1193
|
+
function normalizeInputRoots({ inputRoot, inputRoots }) {
|
|
1194
|
+
const resolved = (inputRoots !== void 0 && inputRoots.length > 0 ? inputRoots : inputRoot !== void 0 ? [(0, node_path.join)(inputRoot, RULESYNC_RELATIVE_DIR_PATH)] : [(0, node_path.join)(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)]).map((entry) => (0, node_path.isAbsolute)(entry) ? entry : (0, node_path.resolve)(entry));
|
|
1195
|
+
return [resolved[0], ...resolved.slice(1)];
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1165
1198
|
* Conflicting target pairs that cannot be used together.
|
|
1166
1199
|
* Exported so `rulesync doctor` can report the same conflicts as diagnostics
|
|
1167
1200
|
* without duplicating the list.
|
|
@@ -1198,6 +1231,28 @@ const assertTargetsFeaturesExclusive = ({ targets, features }) => {
|
|
|
1198
1231
|
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.");
|
|
1199
1232
|
};
|
|
1200
1233
|
/**
|
|
1234
|
+
* Rejects a single user-authored config file (or a single programmatic
|
|
1235
|
+
* construction) that defines both `inputRoot` and `inputRoots` — the two
|
|
1236
|
+
* fields express the same setting at singular vs. list level and cannot
|
|
1237
|
+
* be combined within one file without ambiguity.
|
|
1238
|
+
*
|
|
1239
|
+
* The check is intentionally per-file: base and local config files can each
|
|
1240
|
+
* be valid in isolation and merge into a state where both survive, and the
|
|
1241
|
+
* resolver picks `inputRoots` in that case (see `resolveEffectiveInputRoots`).
|
|
1242
|
+
* Only a single file declaring both is a genuine authoring error.
|
|
1243
|
+
*/
|
|
1244
|
+
const assertInputRootFieldsExclusive = ({ inputRoot, inputRoots }) => {
|
|
1245
|
+
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.");
|
|
1246
|
+
};
|
|
1247
|
+
/**
|
|
1248
|
+
* Rejects an explicitly supplied empty `inputRoots` list. Omitting the field
|
|
1249
|
+
* selects the conventional default, while an empty list has no meaningful
|
|
1250
|
+
* source-tree semantics and must not be treated as an absent override.
|
|
1251
|
+
*/
|
|
1252
|
+
const assertInputRootsNonEmpty = ({ inputRoots }) => {
|
|
1253
|
+
if (inputRoots !== void 0 && inputRoots.length === 0) throw new Error("Invalid config: 'inputRoots' must be non-empty.");
|
|
1254
|
+
};
|
|
1255
|
+
/**
|
|
1201
1256
|
* Normalizes a post-resolution `ConfigParams` input by rejecting the case
|
|
1202
1257
|
* where both `targets` and `features` are undefined — a degenerate state
|
|
1203
1258
|
* that would silently produce a no-op config (no targets, no features).
|
|
@@ -1233,14 +1288,35 @@ var Config = class Config {
|
|
|
1233
1288
|
gitignoreDestination;
|
|
1234
1289
|
dryRun;
|
|
1235
1290
|
check;
|
|
1236
|
-
|
|
1291
|
+
/**
|
|
1292
|
+
* Ordered, absolute-path list of rulesync source trees. Each entry is a
|
|
1293
|
+
* source tree itself — the directory that directly contains `rules/`,
|
|
1294
|
+
* `skills/`, `mcp.jsonc`, etc. No implicit `.rulesync/` join is applied.
|
|
1295
|
+
*
|
|
1296
|
+
* Always non-empty by construction — the constructor either normalizes
|
|
1297
|
+
* an `inputRoot`/`inputRoots` input or falls back to a single-element
|
|
1298
|
+
* list containing `join(<cwd>, ".rulesync")`.
|
|
1299
|
+
*
|
|
1300
|
+
* `inputRoot` (singular) is a deprecated backward-compatibility alias
|
|
1301
|
+
* that expands to `[join(inputRoot, ".rulesync")]`.
|
|
1302
|
+
*
|
|
1303
|
+
* Typed as a non-empty tuple so the one internal caller that legitimately
|
|
1304
|
+
* needs "the primary root" (`normalizeConfigFilePath` fallback) can index
|
|
1305
|
+
* `[0]` without a runtime null-check.
|
|
1306
|
+
*/
|
|
1307
|
+
inputRoots;
|
|
1237
1308
|
configFilePath;
|
|
1238
1309
|
sources;
|
|
1239
|
-
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
|
|
1310
|
+
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
1240
1311
|
assertTargetsFeaturesExclusive({
|
|
1241
1312
|
targets,
|
|
1242
1313
|
features
|
|
1243
1314
|
});
|
|
1315
|
+
assertInputRootFieldsExclusive({
|
|
1316
|
+
inputRoot,
|
|
1317
|
+
inputRoots
|
|
1318
|
+
});
|
|
1319
|
+
assertInputRootsNonEmpty({ inputRoots });
|
|
1244
1320
|
assertTargetsOrFeaturesProvided({
|
|
1245
1321
|
targets,
|
|
1246
1322
|
features
|
|
@@ -1268,10 +1344,13 @@ var Config = class Config {
|
|
|
1268
1344
|
this.gitignoreDestination = gitignoreDestination ?? "gitignore";
|
|
1269
1345
|
this.dryRun = dryRun ?? false;
|
|
1270
1346
|
this.check = check ?? false;
|
|
1271
|
-
this.
|
|
1347
|
+
this.inputRoots = normalizeInputRoots({
|
|
1348
|
+
inputRoot,
|
|
1349
|
+
inputRoots
|
|
1350
|
+
});
|
|
1272
1351
|
this.configFilePath = normalizeConfigFilePath({
|
|
1273
1352
|
configFilePath,
|
|
1274
|
-
|
|
1353
|
+
anchorDir: (0, node_path.dirname)(this.inputRoots[0])
|
|
1275
1354
|
});
|
|
1276
1355
|
this.sources = sources ?? [];
|
|
1277
1356
|
}
|
|
@@ -1455,16 +1534,22 @@ var Config = class Config {
|
|
|
1455
1534
|
return this.check;
|
|
1456
1535
|
}
|
|
1457
1536
|
/**
|
|
1458
|
-
* Returns the
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1537
|
+
* Returns the ordered list of rulesync source trees. Each entry is the
|
|
1538
|
+
* source tree itself — the directory that directly contains `rules/`,
|
|
1539
|
+
* `skills/`, `mcp.jsonc`, etc. Values are absolute paths captured at
|
|
1540
|
+
* config-construction time, so this accessor is pure and never depends on
|
|
1541
|
+
* a live `process.cwd()` read.
|
|
1461
1542
|
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1543
|
+
* The returned tuple is always non-empty: when no `inputRoot`/`inputRoots`
|
|
1544
|
+
* was supplied, `[join(process.cwd(), ".rulesync")]` is snapshotted once
|
|
1545
|
+
* during construction. The first entry is the required base source tree.
|
|
1546
|
+
* Later entries are optional overlays and may be absent; when present, they
|
|
1547
|
+
* take precedence when the same relative source path exists in more than
|
|
1548
|
+
* one root (see per-feature merge policies in the processor
|
|
1549
|
+
* `loadRulesync*` methods).
|
|
1465
1550
|
*/
|
|
1466
|
-
|
|
1467
|
-
return this.
|
|
1551
|
+
getInputRoots() {
|
|
1552
|
+
return this.inputRoots;
|
|
1468
1553
|
}
|
|
1469
1554
|
/**
|
|
1470
1555
|
* Returns the absolute path of the configuration file this config was
|
|
@@ -1505,6 +1590,7 @@ const getDefaults = () => ({
|
|
|
1505
1590
|
dryRun: false,
|
|
1506
1591
|
check: false,
|
|
1507
1592
|
inputRoot: void 0,
|
|
1593
|
+
inputRoots: void 0,
|
|
1508
1594
|
sources: []
|
|
1509
1595
|
});
|
|
1510
1596
|
const loadConfigFromFile = async (filePath) => {
|
|
@@ -1516,8 +1602,23 @@ const loadConfigFromFile = async (filePath) => {
|
|
|
1516
1602
|
targets: configParams.targets,
|
|
1517
1603
|
features: configParams.features
|
|
1518
1604
|
});
|
|
1605
|
+
try {
|
|
1606
|
+
assertInputRootFieldsExclusive({
|
|
1607
|
+
inputRoot: configParams.inputRoot,
|
|
1608
|
+
inputRoots: configParams.inputRoots
|
|
1609
|
+
});
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1612
|
+
throw new Error(`${detail} (in ${JSON.stringify(filePath)})`, { cause: error });
|
|
1613
|
+
}
|
|
1519
1614
|
return configParams;
|
|
1520
1615
|
};
|
|
1616
|
+
function mergeInputRootConfigs({ baseConfig, localConfig }) {
|
|
1617
|
+
return {
|
|
1618
|
+
inputRoot: localConfig.inputRoot ?? baseConfig.inputRoot,
|
|
1619
|
+
inputRoots: localConfig.inputRoots ?? baseConfig.inputRoots
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1521
1622
|
const mergeConfigs = (baseConfig, localConfig) => {
|
|
1522
1623
|
return {
|
|
1523
1624
|
targets: localConfig.targets ?? baseConfig.targets,
|
|
@@ -1535,7 +1636,10 @@ const mergeConfigs = (baseConfig, localConfig) => {
|
|
|
1535
1636
|
gitignoreDestination: localConfig.gitignoreDestination ?? baseConfig.gitignoreDestination,
|
|
1536
1637
|
dryRun: localConfig.dryRun ?? baseConfig.dryRun,
|
|
1537
1638
|
check: localConfig.check ?? baseConfig.check,
|
|
1538
|
-
|
|
1639
|
+
...mergeInputRootConfigs({
|
|
1640
|
+
baseConfig,
|
|
1641
|
+
localConfig
|
|
1642
|
+
}),
|
|
1539
1643
|
sources: localConfig.sources ?? baseConfig.sources
|
|
1540
1644
|
};
|
|
1541
1645
|
};
|
|
@@ -1564,13 +1668,14 @@ function assertMergedTargetsFeaturesExclusive({ configByFile, validatedConfigPat
|
|
|
1564
1668
|
}
|
|
1565
1669
|
}
|
|
1566
1670
|
/**
|
|
1567
|
-
* Resolve the effective `global` flag. When an
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1671
|
+
* Resolve the effective `global` flag. When an input root (singular
|
|
1672
|
+
* `inputRoot` or plural `inputRoots`) is in play the user is decoupling
|
|
1673
|
+
* source from output, so a config-file `global: true` is dropped (unless
|
|
1674
|
+
* the caller also explicitly passes `global`); a warning is emitted in
|
|
1570
1675
|
* that case. Returns the resolved boolean `global`.
|
|
1571
1676
|
*/
|
|
1572
1677
|
function resolveGlobal({ logger, resolvedInputRoot, global, configByFile, validatedConfigPath }) {
|
|
1573
|
-
if (resolvedInputRoot !== void 0 && global === void 0 && configByFile.global === true) warnWithFallback(logger, `Ignoring "global: true" from ${JSON.stringify(validatedConfigPath)} because an
|
|
1678
|
+
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).`);
|
|
1574
1679
|
return pick({
|
|
1575
1680
|
cli: global,
|
|
1576
1681
|
file: resolvedInputRoot !== void 0 ? false : configByFile.global,
|
|
@@ -1594,17 +1699,81 @@ function resolveFeaturesAndTargets({ features, targets, configByFile }) {
|
|
|
1594
1699
|
resolvedTargets: userProvidedTargets ?? getDefaults().targets
|
|
1595
1700
|
};
|
|
1596
1701
|
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Resolve the effective, non-empty, absolute-path list of source-tree roots
|
|
1704
|
+
* by applying CLI > file > default precedence and preferring `inputRoots`
|
|
1705
|
+
* over `inputRoot` when both survive the base+local merge. Duplicates (after
|
|
1706
|
+
* normalization to absolute paths) are removed silently so overlapping
|
|
1707
|
+
* base/local declarations do not double-count the same tree.
|
|
1708
|
+
*
|
|
1709
|
+
* Semantics (post-refactor):
|
|
1710
|
+
* - `inputRoots` entries are the source trees themselves (each holds
|
|
1711
|
+
* `rules/`, `skills/`, `mcp.jsonc`, etc.); they are passed through
|
|
1712
|
+
* unchanged.
|
|
1713
|
+
* - `inputRoot` (legacy singular) is a shorthand for "parent of the
|
|
1714
|
+
* `.rulesync/` source tree" and is expanded to `join(inputRoot,
|
|
1715
|
+
* ".rulesync")` before it hits any consumer.
|
|
1716
|
+
* - The "nothing configured" default expands to `[join(cwd, ".rulesync")]`
|
|
1717
|
+
* so existing projects keep working unchanged.
|
|
1718
|
+
*
|
|
1719
|
+
* When both the merged file config has `inputRoots` and the CLI supplied
|
|
1720
|
+
* `inputRoot` (or vice versa), CLI wins outright — matching how every
|
|
1721
|
+
* other field is resolved. When only the file config supplies both, the
|
|
1722
|
+
* plural wins over the singular and the drop is logged at debug level.
|
|
1723
|
+
*/
|
|
1724
|
+
function resolveEffectiveInputRoots({ cliInputRoot, cliInputRoots, configByFile, cwd, logger }) {
|
|
1725
|
+
let source;
|
|
1726
|
+
let field;
|
|
1727
|
+
if (cliInputRoots !== void 0 && cliInputRoots.length > 0) {
|
|
1728
|
+
source = cliInputRoots;
|
|
1729
|
+
field = "inputRoots";
|
|
1730
|
+
} else if (cliInputRoot !== void 0) {
|
|
1731
|
+
source = [(0, node_path.join)(cliInputRoot, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1732
|
+
field = "inputRoot";
|
|
1733
|
+
} else if (configByFile.inputRoots !== void 0 && configByFile.inputRoots.length > 0) {
|
|
1734
|
+
source = configByFile.inputRoots;
|
|
1735
|
+
field = "inputRoots";
|
|
1736
|
+
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.`);
|
|
1737
|
+
} else if (configByFile.inputRoot !== void 0) {
|
|
1738
|
+
source = [(0, node_path.join)(configByFile.inputRoot, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1739
|
+
field = "inputRoot";
|
|
1740
|
+
} else source = [(0, node_path.join)(cwd, RULESYNC_RELATIVE_DIR_PATH)];
|
|
1741
|
+
const candidates = source.map((entry) => (0, node_path.resolve)(cwd, entry));
|
|
1742
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1743
|
+
const resolved = [];
|
|
1744
|
+
for (const absolute of candidates) {
|
|
1745
|
+
if (seen.has(absolute)) continue;
|
|
1746
|
+
seen.add(absolute);
|
|
1747
|
+
resolved.push(absolute);
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
inputRoots: [resolved[0], ...resolved.slice(1)],
|
|
1751
|
+
candidates,
|
|
1752
|
+
field
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1597
1755
|
var ConfigResolver = class {
|
|
1598
|
-
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot }, { logger } = {}) {
|
|
1756
|
+
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot, inputRoots }, { logger } = {}) {
|
|
1599
1757
|
const cwd = (0, node_path.resolve)(process.cwd());
|
|
1758
|
+
assertInputRootFieldsExclusive({
|
|
1759
|
+
inputRoot,
|
|
1760
|
+
inputRoots
|
|
1761
|
+
});
|
|
1762
|
+
assertInputRootsNonEmpty({ inputRoots });
|
|
1600
1763
|
if (inputRoot !== void 0) validateOutputRoot(inputRoot);
|
|
1601
|
-
|
|
1764
|
+
if (inputRoots !== void 0) for (const entry of inputRoots) validateOutputRoot(entry);
|
|
1765
|
+
const cliConfigAnchor = inputRoot;
|
|
1766
|
+
const hasCliInputRootOverride = inputRoot !== void 0 || inputRoots !== void 0;
|
|
1767
|
+
const validatedConfigPath = resolvePath(configPath, (0, node_path.resolve)(cliConfigAnchor ?? cwd));
|
|
1602
1768
|
const baseConfig = await loadConfigFromFile(validatedConfigPath);
|
|
1603
1769
|
const configDir = (0, node_path.dirname)(validatedConfigPath);
|
|
1604
1770
|
const localConfigPath = (0, node_path.join)(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
|
|
1605
1771
|
const localConfig = await loadConfigFromFile(localConfigPath);
|
|
1606
1772
|
const configByFile = mergeConfigs(baseConfig, localConfig);
|
|
1607
|
-
if (
|
|
1773
|
+
if (!hasCliInputRootOverride) {
|
|
1774
|
+
if (configByFile.inputRoot !== void 0) validateOutputRoot(configByFile.inputRoot);
|
|
1775
|
+
if (configByFile.inputRoots !== void 0) for (const entry of configByFile.inputRoots) validateOutputRoot(entry);
|
|
1776
|
+
}
|
|
1608
1777
|
assertMergedTargetsFeaturesExclusive({
|
|
1609
1778
|
configByFile,
|
|
1610
1779
|
validatedConfigPath,
|
|
@@ -1630,10 +1799,16 @@ var ConfigResolver = class {
|
|
|
1630
1799
|
silent: resolvedSilent
|
|
1631
1800
|
});
|
|
1632
1801
|
}
|
|
1633
|
-
const
|
|
1802
|
+
const resolvedInputRoots = resolveEffectiveInputRoots({
|
|
1803
|
+
cliInputRoot: inputRoot,
|
|
1804
|
+
cliInputRoots: inputRoots,
|
|
1805
|
+
configByFile,
|
|
1806
|
+
cwd,
|
|
1807
|
+
logger
|
|
1808
|
+
}).inputRoots;
|
|
1634
1809
|
const resolvedGlobal = resolveGlobal({
|
|
1635
1810
|
logger,
|
|
1636
|
-
resolvedInputRoot,
|
|
1811
|
+
resolvedInputRoot: inputRoot !== void 0 || inputRoots !== void 0 || configByFile.inputRoot !== void 0 || configByFile.inputRoots !== void 0 ? resolvedInputRoots[0] : void 0,
|
|
1637
1812
|
global,
|
|
1638
1813
|
configByFile,
|
|
1639
1814
|
validatedConfigPath
|
|
@@ -1697,7 +1872,7 @@ var ConfigResolver = class {
|
|
|
1697
1872
|
file: configByFile.check,
|
|
1698
1873
|
fallback: getDefaults().check
|
|
1699
1874
|
}),
|
|
1700
|
-
|
|
1875
|
+
inputRoots: resolvedInputRoots,
|
|
1701
1876
|
configFilePath: validatedConfigPath,
|
|
1702
1877
|
sources: configByFile.sources ?? getDefaults().sources,
|
|
1703
1878
|
flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
|
|
@@ -2097,8 +2272,9 @@ var RulesyncCheck = class RulesyncCheck extends RulesyncFile {
|
|
|
2097
2272
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
2098
2273
|
};
|
|
2099
2274
|
}
|
|
2100
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
2101
|
-
const
|
|
2275
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
2276
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().relativeDirPath;
|
|
2277
|
+
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
2102
2278
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
2103
2279
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
2104
2280
|
const result = RulesyncCheckFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -2106,7 +2282,7 @@ var RulesyncCheck = class RulesyncCheck extends RulesyncFile {
|
|
|
2106
2282
|
const filename = (0, node_path.basename)(relativeFilePath);
|
|
2107
2283
|
return new RulesyncCheck({
|
|
2108
2284
|
outputRoot,
|
|
2109
|
-
relativeDirPath:
|
|
2285
|
+
relativeDirPath: dirPath,
|
|
2110
2286
|
relativeFilePath: filename,
|
|
2111
2287
|
frontmatter: result.data,
|
|
2112
2288
|
body: content.trim()
|
|
@@ -2177,8 +2353,9 @@ var RulesyncCommand = class RulesyncCommand extends RulesyncFile {
|
|
|
2177
2353
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
2178
2354
|
};
|
|
2179
2355
|
}
|
|
2180
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
2181
|
-
const
|
|
2356
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
2357
|
+
const dirPath = relativeDirPath ?? RulesyncCommand.getSettablePaths().relativeDirPath;
|
|
2358
|
+
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
2182
2359
|
const fileContent = await readFileContent(filePath);
|
|
2183
2360
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
|
|
2184
2361
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
@@ -2186,7 +2363,7 @@ var RulesyncCommand = class RulesyncCommand extends RulesyncFile {
|
|
|
2186
2363
|
if (!result.success) throw new Error(`Invalid frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
|
|
2187
2364
|
return new RulesyncCommand({
|
|
2188
2365
|
outputRoot,
|
|
2189
|
-
relativeDirPath:
|
|
2366
|
+
relativeDirPath: dirPath,
|
|
2190
2367
|
relativeFilePath,
|
|
2191
2368
|
frontmatter: result.data,
|
|
2192
2369
|
body: content.trim(),
|
|
@@ -3575,8 +3752,19 @@ const CANONICAL_TO_VIBE_EVENT_NAMES = {
|
|
|
3575
3752
|
};
|
|
3576
3753
|
/**
|
|
3577
3754
|
* Map Mistral Vibe snake_case event names to canonical camelCase.
|
|
3755
|
+
*
|
|
3756
|
+
* The pre-2.21.0 spellings are accepted alongside the current ones. Vibe's
|
|
3757
|
+
* strict `HookType` enum rejects a file that still uses them, so such a file is
|
|
3758
|
+
* already dead on disk; reading it here and emitting the renamed spelling is
|
|
3759
|
+
* what repairs it, whereas leaving the old name unmapped would route the hook
|
|
3760
|
+
* into a tool override block and lose the event.
|
|
3578
3761
|
*/
|
|
3579
|
-
const VIBE_TO_CANONICAL_EVENT_NAMES =
|
|
3762
|
+
const VIBE_TO_CANONICAL_EVENT_NAMES = {
|
|
3763
|
+
...Object.fromEntries(Object.entries(CANONICAL_TO_VIBE_EVENT_NAMES).map(([k, v]) => [v, k])),
|
|
3764
|
+
before_tool: "preToolUse",
|
|
3765
|
+
after_tool: "postToolUse",
|
|
3766
|
+
post_agent_turn: "stop"
|
|
3767
|
+
};
|
|
3580
3768
|
/**
|
|
3581
3769
|
* Map canonical camelCase event names to Qwen Code PascalCase.
|
|
3582
3770
|
*
|
|
@@ -3797,15 +3985,17 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
3797
3985
|
error: null
|
|
3798
3986
|
};
|
|
3799
3987
|
}
|
|
3800
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
3988
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true }) {
|
|
3801
3989
|
const paths = RulesyncHooks.getSettablePaths();
|
|
3990
|
+
const overrideDirPath = relativeDirPath;
|
|
3802
3991
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
3803
|
-
const
|
|
3992
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
3993
|
+
const filePath = (0, node_path.join)(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
3804
3994
|
if (!await fileExists(filePath)) continue;
|
|
3805
3995
|
const fileContent = await readFileContent(filePath);
|
|
3806
3996
|
return new RulesyncHooks({
|
|
3807
3997
|
outputRoot,
|
|
3808
|
-
relativeDirPath:
|
|
3998
|
+
relativeDirPath: candidateDirPath,
|
|
3809
3999
|
relativeFilePath: candidate.relativeFilePath,
|
|
3810
4000
|
fileContent,
|
|
3811
4001
|
validate
|
|
@@ -3832,21 +4022,23 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
|
|
|
3832
4022
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
3833
4023
|
relativeFilePath: RULESYNC_AIIGNORE_FILE_NAME
|
|
3834
4024
|
},
|
|
3835
|
-
legacy: {
|
|
4025
|
+
legacy: [{
|
|
3836
4026
|
relativeDirPath: ".",
|
|
3837
4027
|
relativeFilePath: RULESYNC_IGNORE_RELATIVE_FILE_PATH
|
|
3838
|
-
}
|
|
4028
|
+
}]
|
|
3839
4029
|
};
|
|
3840
4030
|
}
|
|
3841
|
-
static async fromFile({ outputRoot = process.cwd() } = {}) {
|
|
4031
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath } = {}) {
|
|
3842
4032
|
const paths = this.getSettablePaths();
|
|
3843
|
-
const
|
|
3844
|
-
const
|
|
4033
|
+
const recommendedDirPath = relativeDirPath ?? paths.recommended.relativeDirPath;
|
|
4034
|
+
const recommendedPath = (0, node_path.join)(outputRoot, recommendedDirPath, paths.recommended.relativeFilePath);
|
|
4035
|
+
const [legacy] = paths.legacy;
|
|
4036
|
+
const legacyPath = (0, node_path.join)(outputRoot, legacy.relativeDirPath, legacy.relativeFilePath);
|
|
3845
4037
|
if (await fileExists(recommendedPath)) {
|
|
3846
4038
|
const fileContent = await readFileContent(recommendedPath);
|
|
3847
4039
|
return new RulesyncIgnore({
|
|
3848
4040
|
outputRoot,
|
|
3849
|
-
relativeDirPath:
|
|
4041
|
+
relativeDirPath: recommendedDirPath,
|
|
3850
4042
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
3851
4043
|
fileContent
|
|
3852
4044
|
});
|
|
@@ -3855,15 +4047,15 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
|
|
|
3855
4047
|
const fileContent = await readFileContent(legacyPath);
|
|
3856
4048
|
return new RulesyncIgnore({
|
|
3857
4049
|
outputRoot,
|
|
3858
|
-
relativeDirPath:
|
|
3859
|
-
relativeFilePath:
|
|
4050
|
+
relativeDirPath: legacy.relativeDirPath,
|
|
4051
|
+
relativeFilePath: legacy.relativeFilePath,
|
|
3860
4052
|
fileContent
|
|
3861
4053
|
});
|
|
3862
4054
|
}
|
|
3863
4055
|
const fileContent = await readFileContent(recommendedPath);
|
|
3864
4056
|
return new RulesyncIgnore({
|
|
3865
4057
|
outputRoot,
|
|
3866
|
-
relativeDirPath:
|
|
4058
|
+
relativeDirPath: recommendedDirPath,
|
|
3867
4059
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
3868
4060
|
fileContent
|
|
3869
4061
|
});
|
|
@@ -4019,6 +4211,118 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
4019
4211
|
warp: zod_mini.z.optional(toolScopedMcpSchema),
|
|
4020
4212
|
zed: zod_mini.z.optional(toolScopedMcpSchema)
|
|
4021
4213
|
});
|
|
4214
|
+
/**
|
|
4215
|
+
* The tool-scoped block keys that carry a `{toolname}.mcpServers` sub-map.
|
|
4216
|
+
* Derived from `RulesyncMcpFileSchema`'s own shape so this set can never drift
|
|
4217
|
+
* from the schema — every tool-scoped block declared above is treated as a
|
|
4218
|
+
* "merge servers by name" site by `mergeMcpJsonOverlays`, and everything else
|
|
4219
|
+
* (including `$schema` and top-level Kimi Code timeout fields) is replaced
|
|
4220
|
+
* atomically.
|
|
4221
|
+
*/
|
|
4222
|
+
const TOOL_SCOPED_MCP_KEYS = new Set(Object.keys(RulesyncMcpFileSchema.def.shape).filter((key) => key !== "$schema" && key !== "mcpServers"));
|
|
4223
|
+
/**
|
|
4224
|
+
* Return the first candidate path (recommended, then legacy variants) that
|
|
4225
|
+
* exists under `outputRoot`, or `undefined` when none is present. Shared
|
|
4226
|
+
* between `fromFile` (single-root) and `fromRoots` (multi-root) so both
|
|
4227
|
+
* paths honour the same intra-root resolution order.
|
|
4228
|
+
*
|
|
4229
|
+
* When `overrideDirPath` is provided it replaces the candidates'
|
|
4230
|
+
* class-level `relativeDirPath` (which defaults to `.rulesync/`) so the
|
|
4231
|
+
* caller can point at a non-default source tree (e.g. `.rulesync.local/`).
|
|
4232
|
+
* Also returned is the effective `relativeDirPath` for the winning
|
|
4233
|
+
* candidate so the caller can reconstruct a `RulesyncMcp` with matching
|
|
4234
|
+
* anchor fields.
|
|
4235
|
+
*/
|
|
4236
|
+
async function findFirstExistingCandidate({ paths, outputRoot, overrideDirPath }) {
|
|
4237
|
+
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
4238
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
4239
|
+
const filePath = (0, node_path.join)(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
4240
|
+
if (await fileExists(filePath)) return {
|
|
4241
|
+
filePath,
|
|
4242
|
+
candidate: {
|
|
4243
|
+
relativeDirPath: candidateDirPath,
|
|
4244
|
+
relativeFilePath: candidate.relativeFilePath
|
|
4245
|
+
}
|
|
4246
|
+
};
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Merge two parsed MCP JSON objects with the one-level policy from the
|
|
4251
|
+
* inputRoots plan: the top-level `mcpServers` map and each
|
|
4252
|
+
* `<toolname>.mcpServers` sub-map are merged by server name (later wins per
|
|
4253
|
+
* key). Every other value — individual server configs, other top-level keys
|
|
4254
|
+
* — is replaced atomically. This keeps the merge predictable: an overlay can
|
|
4255
|
+
* add or replace whole shared servers, but a partial patch of one server's
|
|
4256
|
+
* `args`/`env` is deliberately not supported.
|
|
4257
|
+
*/
|
|
4258
|
+
function getRecordField({ value, path }) {
|
|
4259
|
+
if (value === void 0) return {};
|
|
4260
|
+
if (!isRecord$1(value)) throw new Error(`Invalid MCP overlay: '${path}' must be an object.`);
|
|
4261
|
+
return value;
|
|
4262
|
+
}
|
|
4263
|
+
/**
|
|
4264
|
+
* Overlay one record onto another, dropping any overlay key that could reach
|
|
4265
|
+
* `Object.prototype`. Every overlay merge goes through this so a `__proto__`
|
|
4266
|
+
* entry cannot enter the merged config from any depth — top-level
|
|
4267
|
+
* `mcpServers`, a tool-scoped block, or that block's own `mcpServers`.
|
|
4268
|
+
*/
|
|
4269
|
+
function mergeRecordsSkippingPollutionKeys({ base, overlay }) {
|
|
4270
|
+
const merged = { ...base };
|
|
4271
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
4272
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
4273
|
+
merged[key] = value;
|
|
4274
|
+
}
|
|
4275
|
+
return merged;
|
|
4276
|
+
}
|
|
4277
|
+
function mergeMcpJsonOverlays({ base, overlay }) {
|
|
4278
|
+
const merged = { ...base };
|
|
4279
|
+
for (const [key, overlayValue] of Object.entries(overlay)) {
|
|
4280
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
4281
|
+
if (key === "mcpServers") {
|
|
4282
|
+
merged.mcpServers = mergeRecordsSkippingPollutionKeys({
|
|
4283
|
+
base: getRecordField({
|
|
4284
|
+
value: base.mcpServers,
|
|
4285
|
+
path: "mcpServers"
|
|
4286
|
+
}),
|
|
4287
|
+
overlay: getRecordField({
|
|
4288
|
+
value: overlayValue,
|
|
4289
|
+
path: "mcpServers"
|
|
4290
|
+
})
|
|
4291
|
+
});
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
if (TOOL_SCOPED_MCP_KEYS.has(key)) {
|
|
4295
|
+
const baseBlock = getRecordField({
|
|
4296
|
+
value: base[key],
|
|
4297
|
+
path: key
|
|
4298
|
+
});
|
|
4299
|
+
const overlayBlock = getRecordField({
|
|
4300
|
+
value: overlayValue,
|
|
4301
|
+
path: key
|
|
4302
|
+
});
|
|
4303
|
+
const mergedBlock = mergeRecordsSkippingPollutionKeys({
|
|
4304
|
+
base: baseBlock,
|
|
4305
|
+
overlay: overlayBlock
|
|
4306
|
+
});
|
|
4307
|
+
const baseServers = getRecordField({
|
|
4308
|
+
value: baseBlock.mcpServers,
|
|
4309
|
+
path: `${key}.mcpServers`
|
|
4310
|
+
});
|
|
4311
|
+
const overlayServers = getRecordField({
|
|
4312
|
+
value: overlayBlock.mcpServers,
|
|
4313
|
+
path: `${key}.mcpServers`
|
|
4314
|
+
});
|
|
4315
|
+
if (Object.keys(baseServers).length > 0 || Object.keys(overlayServers).length > 0) mergedBlock.mcpServers = mergeRecordsSkippingPollutionKeys({
|
|
4316
|
+
base: baseServers,
|
|
4317
|
+
overlay: overlayServers
|
|
4318
|
+
});
|
|
4319
|
+
merged[key] = mergedBlock;
|
|
4320
|
+
continue;
|
|
4321
|
+
}
|
|
4322
|
+
merged[key] = overlayValue;
|
|
4323
|
+
}
|
|
4324
|
+
return merged;
|
|
4325
|
+
}
|
|
4022
4326
|
var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
4023
4327
|
json;
|
|
4024
4328
|
constructor(params) {
|
|
@@ -4055,28 +4359,136 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
4055
4359
|
error: null
|
|
4056
4360
|
};
|
|
4057
4361
|
}
|
|
4058
|
-
|
|
4362
|
+
/**
|
|
4363
|
+
* Load and merge MCP source files across the configured input roots.
|
|
4364
|
+
*
|
|
4365
|
+
* `inputRoots` entries are the source trees themselves (e.g.
|
|
4366
|
+
* `/repo/.rulesync`, `/repo/.rulesync.local`). Per-root behavior mirrors
|
|
4367
|
+
* `fromFile`: each root's own candidate paths (recommended `mcp.jsonc`,
|
|
4368
|
+
* legacy `mcp.json`, deprecated `.mcp.json`) are checked INSIDE that
|
|
4369
|
+
* source tree and the first hit is loaded. Roots that have no candidate
|
|
4370
|
+
* contribute nothing.
|
|
4371
|
+
*
|
|
4372
|
+
* Cross-root behavior: the parsed JSON objects are folded left-to-right
|
|
4373
|
+
* with `mergeMcpJsonOverlays`, so later roots overlay earlier ones by
|
|
4374
|
+
* server name (one level deep) and replace every other value atomically.
|
|
4375
|
+
* With one root, this delegates to `fromFile` so JSONC formatting and the
|
|
4376
|
+
* actual candidate path are preserved. With multiple roots, each source is
|
|
4377
|
+
* parsed and schema-validated before merging so failures name the originating
|
|
4378
|
+
* file. The merged object is necessarily synthetic, serialized JSON anchored
|
|
4379
|
+
* to the first root's recommended path.
|
|
4380
|
+
*
|
|
4381
|
+
* A multi-root configuration where only one root actually supplies a file is
|
|
4382
|
+
* treated as the single-root case: nothing is merged, so the original file
|
|
4383
|
+
* content is kept verbatim (preserving JSONC comments) and the instance is
|
|
4384
|
+
* anchored at the root that supplied it rather than at the primary root's
|
|
4385
|
+
* recommended path.
|
|
4386
|
+
*
|
|
4387
|
+
* When no root supplies any candidate, this falls back to reading the
|
|
4388
|
+
* primary root's recommended path so the underlying file-not-found error
|
|
4389
|
+
* matches the single-root behavior of `fromFile`.
|
|
4390
|
+
*/
|
|
4391
|
+
static async fromRoots({ inputRoots, validate = true, logger }) {
|
|
4392
|
+
if (inputRoots.length === 1) {
|
|
4393
|
+
const [primary] = inputRoots;
|
|
4394
|
+
return this.fromFile({
|
|
4395
|
+
outputRoot: (0, node_path.dirname)(primary),
|
|
4396
|
+
relativeDirPath: (0, node_path.basename)(primary),
|
|
4397
|
+
validate,
|
|
4398
|
+
logger
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4059
4401
|
const paths = this.getSettablePaths();
|
|
4402
|
+
const rootSources = [];
|
|
4403
|
+
for (const root of inputRoots) {
|
|
4404
|
+
const parent = (0, node_path.dirname)(root);
|
|
4405
|
+
const treeName = (0, node_path.basename)(root);
|
|
4406
|
+
const found = await findFirstExistingCandidate({
|
|
4407
|
+
paths,
|
|
4408
|
+
outputRoot: parent,
|
|
4409
|
+
overrideDirPath: treeName
|
|
4410
|
+
});
|
|
4411
|
+
if (found === void 0) continue;
|
|
4412
|
+
const { filePath, candidate } = found;
|
|
4413
|
+
if (filePath.endsWith(".mcp.json")) {
|
|
4414
|
+
const recommendedPath = (0, node_path.join)(parent, treeName, paths.recommended.relativeFilePath);
|
|
4415
|
+
logger?.warn(`⚠️ Using deprecated path "${filePath}". Please migrate to "${recommendedPath}"`);
|
|
4416
|
+
}
|
|
4417
|
+
const fileContent = await readFileContent(filePath);
|
|
4418
|
+
let parsed;
|
|
4419
|
+
try {
|
|
4420
|
+
parsed = parseJsonc$8(fileContent);
|
|
4421
|
+
if (!isRecord$1(parsed)) throw new Error("Expected a JSON object.");
|
|
4422
|
+
if (validate) {
|
|
4423
|
+
const result = RulesyncMcpFileSchema.safeParse(parsed);
|
|
4424
|
+
if (!result.success) throw result.error;
|
|
4425
|
+
}
|
|
4426
|
+
} catch (error) {
|
|
4427
|
+
throw new Error(`Invalid MCP source file '${filePath}': ${formatError(error)}`, { cause: error });
|
|
4428
|
+
}
|
|
4429
|
+
rootSources.push({
|
|
4430
|
+
record: parsed,
|
|
4431
|
+
outputRoot: parent,
|
|
4432
|
+
relativeDirPath: candidate.relativeDirPath,
|
|
4433
|
+
relativeFilePath: candidate.relativeFilePath,
|
|
4434
|
+
fileContent
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
if (rootSources.length === 0) {
|
|
4438
|
+
const primary = inputRoots[0];
|
|
4439
|
+
return this.fromFile({
|
|
4440
|
+
outputRoot: (0, node_path.dirname)(primary),
|
|
4441
|
+
relativeDirPath: (0, node_path.basename)(primary),
|
|
4442
|
+
validate,
|
|
4443
|
+
logger
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
const onlySource = rootSources.length === 1 ? rootSources[0] : void 0;
|
|
4447
|
+
if (onlySource !== void 0) return new RulesyncMcp({
|
|
4448
|
+
outputRoot: onlySource.outputRoot,
|
|
4449
|
+
relativeDirPath: onlySource.relativeDirPath,
|
|
4450
|
+
relativeFilePath: onlySource.relativeFilePath,
|
|
4451
|
+
fileContent: onlySource.fileContent,
|
|
4452
|
+
validate
|
|
4453
|
+
});
|
|
4454
|
+
const merged = rootSources.reduce((acc, next) => mergeMcpJsonOverlays({
|
|
4455
|
+
base: acc,
|
|
4456
|
+
overlay: next.record
|
|
4457
|
+
}), {});
|
|
4458
|
+
const primary = inputRoots[0];
|
|
4459
|
+
return new RulesyncMcp({
|
|
4460
|
+
outputRoot: (0, node_path.dirname)(primary),
|
|
4461
|
+
relativeDirPath: (0, node_path.basename)(primary),
|
|
4462
|
+
relativeFilePath: paths.recommended.relativeFilePath,
|
|
4463
|
+
fileContent: JSON.stringify(merged, null, 2),
|
|
4464
|
+
validate
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true, logger }) {
|
|
4468
|
+
const paths = this.getSettablePaths();
|
|
4469
|
+
const overrideDirPath = relativeDirPath;
|
|
4060
4470
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
4061
|
-
const
|
|
4471
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
4472
|
+
const filePath = (0, node_path.join)(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
4062
4473
|
if (!await fileExists(filePath)) continue;
|
|
4063
4474
|
if (candidate.relativeFilePath === ".mcp.json") {
|
|
4064
|
-
const recommendedPath = (0, node_path.join)(outputRoot,
|
|
4475
|
+
const recommendedPath = (0, node_path.join)(outputRoot, candidateDirPath, paths.recommended.relativeFilePath);
|
|
4065
4476
|
logger?.warn(`⚠️ Using deprecated path "${filePath}". Please migrate to "${recommendedPath}"`);
|
|
4066
4477
|
}
|
|
4067
4478
|
const fileContent = await readFileContent(filePath);
|
|
4068
4479
|
return new RulesyncMcp({
|
|
4069
4480
|
outputRoot,
|
|
4070
|
-
relativeDirPath:
|
|
4481
|
+
relativeDirPath: candidateDirPath,
|
|
4071
4482
|
relativeFilePath: candidate.relativeFilePath,
|
|
4072
4483
|
fileContent,
|
|
4073
4484
|
validate
|
|
4074
4485
|
});
|
|
4075
4486
|
}
|
|
4076
|
-
const
|
|
4487
|
+
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
4488
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath));
|
|
4077
4489
|
return new RulesyncMcp({
|
|
4078
4490
|
outputRoot,
|
|
4079
|
-
relativeDirPath:
|
|
4491
|
+
relativeDirPath: fallbackDirPath,
|
|
4080
4492
|
relativeFilePath: paths.recommended.relativeFilePath,
|
|
4081
4493
|
fileContent,
|
|
4082
4494
|
validate
|
|
@@ -5271,15 +5683,17 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
5271
5683
|
error: null
|
|
5272
5684
|
};
|
|
5273
5685
|
}
|
|
5274
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
5686
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, validate = true }) {
|
|
5275
5687
|
const paths = RulesyncPermissions.getSettablePaths();
|
|
5688
|
+
const overrideDirPath = relativeDirPath;
|
|
5276
5689
|
for (const candidate of getRulesyncSourceCandidates({ paths })) {
|
|
5277
|
-
const
|
|
5690
|
+
const candidateDirPath = overrideDirPath ?? candidate.relativeDirPath;
|
|
5691
|
+
const filePath = (0, node_path.join)(outputRoot, candidateDirPath, candidate.relativeFilePath);
|
|
5278
5692
|
if (!await fileExists(filePath)) continue;
|
|
5279
5693
|
const fileContent = await readFileContent(filePath);
|
|
5280
5694
|
return new RulesyncPermissions({
|
|
5281
5695
|
outputRoot,
|
|
5282
|
-
relativeDirPath:
|
|
5696
|
+
relativeDirPath: candidateDirPath,
|
|
5283
5697
|
relativeFilePath: candidate.relativeFilePath,
|
|
5284
5698
|
fileContent,
|
|
5285
5699
|
validate
|
|
@@ -5443,8 +5857,9 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5443
5857
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5444
5858
|
};
|
|
5445
5859
|
}
|
|
5446
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
5447
|
-
const
|
|
5860
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
5861
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().recommended.relativeDirPath;
|
|
5862
|
+
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
5448
5863
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5449
5864
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5450
5865
|
const result = RulesyncRuleFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5457,7 +5872,7 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
5457
5872
|
};
|
|
5458
5873
|
return new RulesyncRule({
|
|
5459
5874
|
outputRoot,
|
|
5460
|
-
relativeDirPath:
|
|
5875
|
+
relativeDirPath: dirPath,
|
|
5461
5876
|
relativeFilePath,
|
|
5462
5877
|
frontmatter: validatedFrontmatter,
|
|
5463
5878
|
body: content.trim(),
|
|
@@ -5705,7 +6120,11 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
5705
6120
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5706
6121
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5707
6122
|
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
5708
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
6123
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
6124
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
6125
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
6126
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
6127
|
+
version: zod_mini.z.optional(zod_mini.z.unknown())
|
|
5709
6128
|
})),
|
|
5710
6129
|
grokcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5711
6130
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -5881,8 +6300,9 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5881
6300
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
5882
6301
|
};
|
|
5883
6302
|
}
|
|
5884
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath }) {
|
|
5885
|
-
const
|
|
6303
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
6304
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths().relativeDirPath;
|
|
6305
|
+
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
5886
6306
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
5887
6307
|
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}. Rulesync files must begin with a YAML frontmatter block delimited by '---'.`);
|
|
5888
6308
|
const result = RulesyncSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -5890,7 +6310,7 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5890
6310
|
const filename = (0, node_path.basename)(relativeFilePath);
|
|
5891
6311
|
return new RulesyncSubagent({
|
|
5892
6312
|
outputRoot,
|
|
5893
|
-
relativeDirPath:
|
|
6313
|
+
relativeDirPath: dirPath,
|
|
5894
6314
|
relativeFilePath: filename,
|
|
5895
6315
|
frontmatter: result.data,
|
|
5896
6316
|
body: content.trim()
|
|
@@ -5900,16 +6320,18 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
5900
6320
|
//#endregion
|
|
5901
6321
|
//#region src/features/skills/skills-utils.ts
|
|
5902
6322
|
/**
|
|
5903
|
-
* Returns the set of local skill directory names (excluding `.curated`)
|
|
6323
|
+
* Returns the set of local skill directory names (excluding `.curated`)
|
|
6324
|
+
* from a rulesync source tree (e.g. `/repo/.rulesync` or
|
|
6325
|
+
* `/repo/.rulesync.local`).
|
|
5904
6326
|
*/
|
|
5905
|
-
async function getLocalSkillDirNames(
|
|
5906
|
-
const skillsDir = (0, node_path.join)(
|
|
6327
|
+
async function getLocalSkillDirNames(sourceTree) {
|
|
6328
|
+
const skillsDir = (0, node_path.join)(sourceTree, SKILLS_FEATURE_SUBDIR);
|
|
5907
6329
|
const names = /* @__PURE__ */ new Set();
|
|
5908
6330
|
if (!await directoryExists(skillsDir)) return names;
|
|
5909
6331
|
const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDir, "*"), { type: "dir" });
|
|
5910
6332
|
for (const dirPath of dirPaths) {
|
|
5911
6333
|
const name = (0, node_path.basename)(dirPath);
|
|
5912
|
-
if (name === (0, node_path.basename)(
|
|
6334
|
+
if (name === (0, node_path.basename)(CURATED_SKILLS_FEATURE_SUBDIR)) continue;
|
|
5913
6335
|
names.add(name);
|
|
5914
6336
|
}
|
|
5915
6337
|
return names;
|
|
@@ -6222,15 +6644,49 @@ function companionFileContentsEquivalent({ filePath, expected, existing, compose
|
|
|
6222
6644
|
return tryFileContentsEquivalent(filePath, expectedText, existingText) ?? false;
|
|
6223
6645
|
}
|
|
6224
6646
|
//#endregion
|
|
6647
|
+
//#region src/utils/control-characters.ts
|
|
6648
|
+
/**
|
|
6649
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
6650
|
+
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
6651
|
+
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
6652
|
+
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
6653
|
+
* settings file must never reach the terminal with these intact: they let the
|
|
6654
|
+
* text forge log lines, reorder what is printed around them, or inject escape
|
|
6655
|
+
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
6656
|
+
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
6657
|
+
* place to preserve the typography of a right-to-left name.
|
|
6658
|
+
*/
|
|
6659
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
6660
|
+
/**
|
|
6661
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
6662
|
+
* log line or other terminal output.
|
|
6663
|
+
*/
|
|
6664
|
+
function stripControlCharacters(text) {
|
|
6665
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
6666
|
+
}
|
|
6667
|
+
//#endregion
|
|
6225
6668
|
//#region src/types/feature-processor.ts
|
|
6226
6669
|
var FeatureProcessor = class {
|
|
6227
6670
|
outputRoot;
|
|
6228
|
-
|
|
6671
|
+
/**
|
|
6672
|
+
* Ordered, non-empty list of rulesync source-tree directories. Each entry
|
|
6673
|
+
* is a source tree itself — the directory that directly contains feature
|
|
6674
|
+
* subdirectories (`rules/`, `commands/`, …) and single-file features
|
|
6675
|
+
* (`mcp.jsonc`, `hooks.jsonc`, …). Later entries take precedence in
|
|
6676
|
+
* per-feature merges. Defaults to `[join(process.cwd(), ".rulesync")]`.
|
|
6677
|
+
*
|
|
6678
|
+
* The singular user-facing alias (`inputRoot` in `rulesync.jsonc` / the
|
|
6679
|
+
* `--input-root` CLI flag / `GenerateOptions.inputRoot`) is deprecated
|
|
6680
|
+
* and collapsed into `[join(inputRoot, ".rulesync")]` before it ever
|
|
6681
|
+
* reaches a processor — every internal consumer only ever sees the
|
|
6682
|
+
* plural form, with the source tree already resolved.
|
|
6683
|
+
*/
|
|
6684
|
+
inputRoots;
|
|
6229
6685
|
dryRun;
|
|
6230
6686
|
logger;
|
|
6231
|
-
constructor({ outputRoot = process.cwd(),
|
|
6687
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, logger }) {
|
|
6232
6688
|
this.outputRoot = outputRoot;
|
|
6233
|
-
this.
|
|
6689
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [(0, node_path.join)(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
6234
6690
|
this.dryRun = dryRun;
|
|
6235
6691
|
this.logger = logger;
|
|
6236
6692
|
}
|
|
@@ -6297,6 +6753,198 @@ var FeatureProcessor = class {
|
|
|
6297
6753
|
return orphanFiles.length;
|
|
6298
6754
|
}
|
|
6299
6755
|
};
|
|
6756
|
+
/**
|
|
6757
|
+
* Messages already reported for a given logger.
|
|
6758
|
+
*
|
|
6759
|
+
* One `generate` run constructs a single-file processor per tool target and
|
|
6760
|
+
* per output root — more than twenty times for `--targets "*"` — and each one
|
|
6761
|
+
* re-resolves the same roots. Keying on the logger — created once per run, and
|
|
6762
|
+
* once per test — keeps the shadowing warning to a single line instead of
|
|
6763
|
+
* repeating it for every target. `--watch` reuses one logger across runs, so
|
|
6764
|
+
* {@link resetRootShadowingWarnings} clears the set at the start of each one.
|
|
6765
|
+
*/
|
|
6766
|
+
const warnedRootShadowingByLogger = /* @__PURE__ */ new WeakMap();
|
|
6767
|
+
/**
|
|
6768
|
+
* Forget which shadowing warnings have already been reported.
|
|
6769
|
+
*
|
|
6770
|
+
* `generate` calls this once per run. Without it, `--watch` reuses one logger
|
|
6771
|
+
* for the whole session, so the warning would be printed on the first
|
|
6772
|
+
* generation and never again — the opposite of why it is a warning, since
|
|
6773
|
+
* `--watch` is exactly when an overlay is most likely to be added or edited.
|
|
6774
|
+
*/
|
|
6775
|
+
function resetRootShadowingWarnings({ logger }) {
|
|
6776
|
+
warnedRootShadowingByLogger.delete(logger);
|
|
6777
|
+
}
|
|
6778
|
+
/**
|
|
6779
|
+
* Return the last input root that contains any of the given `relativePaths`,
|
|
6780
|
+
* or `undefined` when none of the roots has any of them. Used by single-file
|
|
6781
|
+
* features (hooks, permissions, ignore) to implement the "later root wins
|
|
6782
|
+
* the whole file" merge policy without materializing the file contents.
|
|
6783
|
+
*
|
|
6784
|
+
* `relativePaths` accepts a small list so features that historically read
|
|
6785
|
+
* either a recommended path or a legacy alias (e.g. `.rulesync/mcp.jsonc`
|
|
6786
|
+
* plus `.rulesync/mcp.json`) can preserve that resolution order per root.
|
|
6787
|
+
* A root counts as "having" the file as long as at least one candidate path
|
|
6788
|
+
* is present.
|
|
6789
|
+
*/
|
|
6790
|
+
async function pickLastRootWithFile({ inputRoots, relativePaths, logger, artifactName }) {
|
|
6791
|
+
let winner;
|
|
6792
|
+
const rootsWithFile = [];
|
|
6793
|
+
for (const root of inputRoots) for (const relativePath of relativePaths) if (await fileExists((0, node_path.join)(root, relativePath))) {
|
|
6794
|
+
winner = root;
|
|
6795
|
+
rootsWithFile.push(root);
|
|
6796
|
+
break;
|
|
6797
|
+
}
|
|
6798
|
+
if (rootsWithFile.length > 1 && winner !== void 0) {
|
|
6799
|
+
const shadowed = rootsWithFile.slice(0, -1);
|
|
6800
|
+
const message = `${artifactName} is provided by more than one input root; '${stripControlCharacters(winner)}' replaces the whole file from ${shadowed.map((root) => `'${stripControlCharacters(root)}'`).join(", ")}.`;
|
|
6801
|
+
let warnedMessages = warnedRootShadowingByLogger.get(logger);
|
|
6802
|
+
if (warnedMessages === void 0) {
|
|
6803
|
+
warnedMessages = /* @__PURE__ */ new Set();
|
|
6804
|
+
warnedRootShadowingByLogger.set(logger, warnedMessages);
|
|
6805
|
+
}
|
|
6806
|
+
if (!warnedMessages.has(message)) {
|
|
6807
|
+
warnedMessages.add(message);
|
|
6808
|
+
logger.warn(message);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
return winner;
|
|
6812
|
+
}
|
|
6813
|
+
/**
|
|
6814
|
+
* Merge per-root result lists into a single ordered list, keeping the
|
|
6815
|
+
* later root's entry when two roots produced an item with the same
|
|
6816
|
+
* identity. Identity is intentionally provided by the caller so
|
|
6817
|
+
* per-feature nuances (case-insensitive filesystems, directory names for
|
|
6818
|
+
* skills, server names for MCP) live next to the feature that owns them.
|
|
6819
|
+
*
|
|
6820
|
+
* The returned list preserves the FIRST appearance order of each identity —
|
|
6821
|
+
* items in the earliest root keep their position, but their content is
|
|
6822
|
+
* replaced by the last root that provided the same identity. This matches
|
|
6823
|
+
* the "overlay" mental model: an overlay changes content, not order.
|
|
6824
|
+
*/
|
|
6825
|
+
function mergeByIdentity({ perRoot, identity }) {
|
|
6826
|
+
const order = [];
|
|
6827
|
+
const winnerByKey = /* @__PURE__ */ new Map();
|
|
6828
|
+
for (const rootItems of perRoot) for (const item of rootItems) {
|
|
6829
|
+
const key = identity(item);
|
|
6830
|
+
if (!winnerByKey.has(key)) order.push(key);
|
|
6831
|
+
winnerByKey.set(key, item);
|
|
6832
|
+
}
|
|
6833
|
+
return order.map((key) => winnerByKey.get(key));
|
|
6834
|
+
}
|
|
6835
|
+
/**
|
|
6836
|
+
* The key two spellings share when a case-insensitive filesystem would give
|
|
6837
|
+
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
6838
|
+
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
6839
|
+
* locale), and the NFC pass folds the composed and decomposed spellings of an
|
|
6840
|
+
* accented name — which macOS also resolves to a single directory —
|
|
6841
|
+
* onto each other.
|
|
6842
|
+
*
|
|
6843
|
+
* This is simple lowercasing rather than full Unicode case folding, so it is
|
|
6844
|
+
* deliberately narrower than what a filesystem considers one file: a Greek
|
|
6845
|
+
* final sigma, a Turkish `ı` under NTFS's upcasing, and a Win32 name whose
|
|
6846
|
+
* trailing dot is stripped all still produce distinct keys. Those pairs keep
|
|
6847
|
+
* the pre-existing behavior (both are imported, and the later one wins on the
|
|
6848
|
+
* filesystem); folding them here would instead drop names that a
|
|
6849
|
+
* case-sensitive filesystem keeps genuinely apart.
|
|
6850
|
+
*/
|
|
6851
|
+
function caseFoldIdentity(identity) {
|
|
6852
|
+
return identity.normalize("NFC").toLowerCase();
|
|
6853
|
+
}
|
|
6854
|
+
/**
|
|
6855
|
+
* Group spellings by their case-folded identity, keeping every original
|
|
6856
|
+
* spelling. On a case-sensitive filesystem one identity can cover several
|
|
6857
|
+
* spellings at once, and the caller needs them all to describe a collision
|
|
6858
|
+
* accurately.
|
|
6859
|
+
*/
|
|
6860
|
+
function groupSpellingsByCaseFoldedIdentity(spellings) {
|
|
6861
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6862
|
+
for (const spelling of spellings) {
|
|
6863
|
+
const identity = caseFoldIdentity(spelling);
|
|
6864
|
+
const existing = grouped.get(identity);
|
|
6865
|
+
if (existing === void 0) grouped.set(identity, [spelling]);
|
|
6866
|
+
else existing.push(spelling);
|
|
6867
|
+
}
|
|
6868
|
+
return grouped;
|
|
6869
|
+
}
|
|
6870
|
+
/**
|
|
6871
|
+
* Build the warning emitted when a `.curated/` entry and a local entry in the
|
|
6872
|
+
* same tree differ only in case.
|
|
6873
|
+
*
|
|
6874
|
+
* `.curated/` is expanded from a declarative source (an external Git repository
|
|
6875
|
+
* or npm package), so its names are untrusted input; both sides are stripped of
|
|
6876
|
+
* control characters before they reach the terminal.
|
|
6877
|
+
*
|
|
6878
|
+
* The winning local spelling is the LAST one, matching the precedence
|
|
6879
|
+
* {@link mergeByCaseInsensitiveIdentity} applies afterwards; any other spelling
|
|
6880
|
+
* that folds onto the same identity is listed too, so the message never names a
|
|
6881
|
+
* spelling that loses.
|
|
6882
|
+
*/
|
|
6883
|
+
function formatCuratedCaseCollisionWarning({ artifactKind, entryNoun, treeDirPath, curatedSpelling, localSpellings }) {
|
|
6884
|
+
const winner = localSpellings[localSpellings.length - 1] ?? "";
|
|
6885
|
+
const shadowed = localSpellings.slice(0, -1);
|
|
6886
|
+
const shadowedSuffix = shadowed.length === 0 ? "" : ` Other local spellings that fold onto the same identity: ${shadowed.map((spelling) => `'${stripControlCharacters(spelling)}'`).join(", ")}.`;
|
|
6887
|
+
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;
|
|
6888
|
+
}
|
|
6889
|
+
/**
|
|
6890
|
+
* Merge artifacts whose filenames are case-insensitive identities, warning
|
|
6891
|
+
* when distinct spellings collapse to the same key. Exact-name overlays are
|
|
6892
|
+
* intentional and remain quiet; only case-only ambiguity is diagnosed.
|
|
6893
|
+
*
|
|
6894
|
+
* Precedence here is the opposite of {@link ClaimedIdentities}: this merges
|
|
6895
|
+
* overlay roots, where the LAST entry wins, while the tool-side loaders keep
|
|
6896
|
+
* the FIRST root to claim a name.
|
|
6897
|
+
*/
|
|
6898
|
+
function mergeByCaseInsensitiveIdentity({ perRoot, identity, artifactName, logger }) {
|
|
6899
|
+
const spellingByKey = /* @__PURE__ */ new Map();
|
|
6900
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
6901
|
+
return mergeByIdentity({
|
|
6902
|
+
perRoot,
|
|
6903
|
+
identity: (item) => {
|
|
6904
|
+
const spelling = identity(item);
|
|
6905
|
+
const key = caseFoldIdentity(spelling);
|
|
6906
|
+
const previousSpelling = spellingByKey.get(key);
|
|
6907
|
+
if (previousSpelling !== void 0 && previousSpelling !== spelling && !warnedKeys.has(key)) {
|
|
6908
|
+
logger.warn(`Case-insensitive ${artifactName} collision: '${stripControlCharacters(previousSpelling)}' and '${stripControlCharacters(spelling)}' resolve to the same identity. The later entry wins.`);
|
|
6909
|
+
warnedKeys.add(key);
|
|
6910
|
+
}
|
|
6911
|
+
if (previousSpelling === void 0) spellingByKey.set(key, spelling);
|
|
6912
|
+
return key;
|
|
6913
|
+
}
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
6916
|
+
/**
|
|
6917
|
+
* Tracks the import identities already claimed while scanning, folding case
|
|
6918
|
+
* through {@link caseFoldIdentity}.
|
|
6919
|
+
*
|
|
6920
|
+
* The tool-side loaders scan several roots in precedence order and keep the
|
|
6921
|
+
* first spelling of each identity. Comparing those identities exactly lets
|
|
6922
|
+
* `.junie/skills/dup-skill` and `.agents/skills/Dup-Skill` both through, and
|
|
6923
|
+
* since macOS and Windows resolve the two written-back directories to a
|
|
6924
|
+
* single one, the shared Agent Skills copy lands last and overwrites the
|
|
6925
|
+
* tool-specific one — inverting the precedence the roots were ordered by.
|
|
6926
|
+
*
|
|
6927
|
+
* The FIRST claimer wins, which is the opposite of the overlay precedence in
|
|
6928
|
+
* {@link mergeByCaseInsensitiveIdentity}: roots are passed in precedence
|
|
6929
|
+
* order, so the earliest one to name a skill is the one that should keep it.
|
|
6930
|
+
*/
|
|
6931
|
+
var ClaimedIdentities = class {
|
|
6932
|
+
claimByKey = /* @__PURE__ */ new Map();
|
|
6933
|
+
/**
|
|
6934
|
+
* Claim `identity` on behalf of `source`. Returns `null` when nothing held
|
|
6935
|
+
* it yet, or the standing claim when something did.
|
|
6936
|
+
*/
|
|
6937
|
+
claim({ identity, source }) {
|
|
6938
|
+
const key = caseFoldIdentity(identity);
|
|
6939
|
+
const claimed = this.claimByKey.get(key);
|
|
6940
|
+
if (claimed !== void 0) return claimed;
|
|
6941
|
+
this.claimByKey.set(key, {
|
|
6942
|
+
spelling: identity,
|
|
6943
|
+
source
|
|
6944
|
+
});
|
|
6945
|
+
return null;
|
|
6946
|
+
}
|
|
6947
|
+
};
|
|
6300
6948
|
//#endregion
|
|
6301
6949
|
//#region src/constants/amp-paths.ts
|
|
6302
6950
|
const AMP_DIR = ".amp";
|
|
@@ -8619,10 +9267,10 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8619
9267
|
toolTarget;
|
|
8620
9268
|
global;
|
|
8621
9269
|
getFactory;
|
|
8622
|
-
constructor({ outputRoot = process.cwd(),
|
|
9270
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$6, dryRun = false, logger }) {
|
|
8623
9271
|
super({
|
|
8624
9272
|
outputRoot,
|
|
8625
|
-
|
|
9273
|
+
inputRoots,
|
|
8626
9274
|
dryRun,
|
|
8627
9275
|
logger
|
|
8628
9276
|
});
|
|
@@ -8659,11 +9307,15 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8659
9307
|
return toolFiles.filter((file) => file instanceof ToolCheck).flatMap((toolCheck) => toolCheck.toRulesyncChecks());
|
|
8660
9308
|
}
|
|
8661
9309
|
/**
|
|
8662
|
-
*
|
|
8663
|
-
*
|
|
9310
|
+
* Load check files from a single source-tree's `checks/` subtree.
|
|
9311
|
+
* `sourceTree` is the source tree itself (e.g. `/repo/.rulesync` or
|
|
9312
|
+
* `/repo/.rulesync.local`).
|
|
8664
9313
|
*/
|
|
8665
|
-
async
|
|
8666
|
-
const
|
|
9314
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
9315
|
+
const treeParent = (0, node_path.dirname)(sourceTree);
|
|
9316
|
+
const treeName = (0, node_path.basename)(sourceTree);
|
|
9317
|
+
const treeChecksDirPath = (0, node_path.join)(treeName, CHECKS_FEATURE_SUBDIR);
|
|
9318
|
+
const checksDir = (0, node_path.join)(sourceTree, CHECKS_FEATURE_SUBDIR);
|
|
8667
9319
|
if (!await directoryExists(checksDir)) {
|
|
8668
9320
|
this.logger.debug(`Rulesync checks directory not found: ${checksDir}`);
|
|
8669
9321
|
return [];
|
|
@@ -8679,7 +9331,8 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8679
9331
|
const filepath = (0, node_path.join)(checksDir, mdFile);
|
|
8680
9332
|
try {
|
|
8681
9333
|
const rulesyncCheck = await RulesyncCheck.fromFile({
|
|
8682
|
-
outputRoot:
|
|
9334
|
+
outputRoot: treeParent,
|
|
9335
|
+
relativeDirPath: treeChecksDirPath,
|
|
8683
9336
|
relativeFilePath: mdFile,
|
|
8684
9337
|
validate: true
|
|
8685
9338
|
});
|
|
@@ -8690,6 +9343,21 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
8690
9343
|
continue;
|
|
8691
9344
|
}
|
|
8692
9345
|
}
|
|
9346
|
+
return rulesyncChecks;
|
|
9347
|
+
}
|
|
9348
|
+
/**
|
|
9349
|
+
* Implementation of abstract method from Processor
|
|
9350
|
+
* Load and parse rulesync check files from every configured input root's
|
|
9351
|
+
* `.rulesync/checks/` directory, merging by relative file path so a check
|
|
9352
|
+
* from a later root replaces the earlier root's copy.
|
|
9353
|
+
*/
|
|
9354
|
+
async loadRulesyncFiles() {
|
|
9355
|
+
const rulesyncChecks = mergeByCaseInsensitiveIdentity({
|
|
9356
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
9357
|
+
identity: (check) => check.getRelativeFilePath(),
|
|
9358
|
+
artifactName: "check",
|
|
9359
|
+
logger: this.logger
|
|
9360
|
+
});
|
|
8693
9361
|
this.logger.debug(`Successfully loaded ${rulesyncChecks.length} rulesync checks`);
|
|
8694
9362
|
return rulesyncChecks;
|
|
8695
9363
|
}
|
|
@@ -10168,18 +10836,23 @@ function commandSlug(relativeFilePath) {
|
|
|
10168
10836
|
return (0, node_path.basename)(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
10169
10837
|
}
|
|
10170
10838
|
/**
|
|
10171
|
-
* Whether a rulesync command exists whose slug matches `dirName
|
|
10839
|
+
* Whether a rulesync command exists whose slug matches `dirName` in any of
|
|
10840
|
+
* the configured input roots.
|
|
10841
|
+
*
|
|
10842
|
+
* `inputRoots[i]` is a source tree itself (e.g. `/repo/.rulesync` or
|
|
10843
|
+
* `/repo/.rulesync.local`), so commands live directly under
|
|
10844
|
+
* `<sourceTree>/commands/`.
|
|
10172
10845
|
*
|
|
10173
10846
|
* Used by the skills-surface `isDirOwned` hooks of tools whose commands are
|
|
10174
10847
|
* emitted as `<slug>/SKILL.md` into the skills tree: a directory matching a
|
|
10175
10848
|
* current command slug is owned by the commands feature, so the skills
|
|
10176
10849
|
* feature must neither import it as a skill nor delete it as an orphan
|
|
10177
|
-
* skill. Once the command is removed from
|
|
10178
|
-
* directory stops matching and the skills feature cleans
|
|
10179
|
-
* orphan.
|
|
10850
|
+
* skill. Once the command is removed from every source tree's `commands/`
|
|
10851
|
+
* directory, the directory stops matching and the skills feature cleans
|
|
10852
|
+
* it up as a regular orphan.
|
|
10180
10853
|
*/
|
|
10181
|
-
async function rulesyncCommandSlugExists({
|
|
10182
|
-
return (await findFilesByGlobs((0, node_path.join)(
|
|
10854
|
+
async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
|
|
10855
|
+
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs((0, node_path.join)(root, COMMANDS_FEATURE_SUBDIR, "**", "*.md"))))).flat().some((filePath) => commandSlug((0, node_path.basename)(filePath)) === dirName);
|
|
10183
10856
|
}
|
|
10184
10857
|
//#endregion
|
|
10185
10858
|
//#region src/features/commands/devin-command.ts
|
|
@@ -10869,11 +11542,10 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
10869
11542
|
* output is lost — so this warns rather than failing the run the way the
|
|
10870
11543
|
* Hermes check does, where the two surfaces really do write the same path.
|
|
10871
11544
|
*/
|
|
10872
|
-
static async validateRulesyncCommands({
|
|
11545
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands, logger }) {
|
|
10873
11546
|
const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
|
|
10874
11547
|
if (commandNames.size === 0) return;
|
|
10875
|
-
const
|
|
10876
|
-
const shadowed = (await findFilesByGlobs((0, node_path.join)(skillsRoot, "**", "SKILL.md"))).map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
|
|
11548
|
+
const shadowed = (await Promise.all(inputRoots.map((root) => findFilesByGlobs((0, node_path.join)(root, SKILLS_FEATURE_SUBDIR, "**", "SKILL.md"))))).flat().map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
|
|
10877
11549
|
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.`);
|
|
10878
11550
|
}
|
|
10879
11551
|
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
@@ -11645,7 +12317,7 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11645
12317
|
static getExtraSharedWritePaths() {
|
|
11646
12318
|
return getHermesagentSharedConfigWritePaths();
|
|
11647
12319
|
}
|
|
11648
|
-
static async validateRulesyncCommands({
|
|
12320
|
+
static async validateRulesyncCommands({ inputRoots, rulesyncCommands }) {
|
|
11649
12321
|
const commandSlugs = /* @__PURE__ */ new Set();
|
|
11650
12322
|
const commandOrigins = /* @__PURE__ */ new Map();
|
|
11651
12323
|
for (const command of rulesyncCommands.filter((candidate) => this.isTargetedByRulesyncCommand(candidate))) {
|
|
@@ -11657,13 +12329,25 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
|
|
|
11657
12329
|
commandOrigins.set(slug, origin);
|
|
11658
12330
|
commandSlugs.add(slug);
|
|
11659
12331
|
}
|
|
11660
|
-
const
|
|
11661
|
-
const
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
|
|
11666
|
-
|
|
12332
|
+
const skillsByName = /* @__PURE__ */ new Map();
|
|
12333
|
+
for (const rootPath of inputRoots) {
|
|
12334
|
+
const skillsRoot = (0, node_path.join)(rootPath, SKILLS_FEATURE_SUBDIR);
|
|
12335
|
+
const skillFiles = await findFilesByGlobs((0, node_path.join)(skillsRoot, "**", "SKILL.md"));
|
|
12336
|
+
const loaded = await Promise.all(skillFiles.map(async (filePath) => {
|
|
12337
|
+
const dirName = toPosixPath((0, node_path.relative)(skillsRoot, (0, node_path.dirname)(filePath)));
|
|
12338
|
+
return {
|
|
12339
|
+
rulesyncSkill: await RulesyncSkill.fromDir({
|
|
12340
|
+
outputRoot: rootPath,
|
|
12341
|
+
relativeDirPath: SKILLS_FEATURE_SUBDIR,
|
|
12342
|
+
dirName
|
|
12343
|
+
}),
|
|
12344
|
+
dirName,
|
|
12345
|
+
rootPath
|
|
12346
|
+
};
|
|
12347
|
+
}));
|
|
12348
|
+
for (const entry of loaded) skillsByName.set(caseFoldIdentity(entry.dirName), entry);
|
|
12349
|
+
}
|
|
12350
|
+
const collisions = [...skillsByName.values()].map(({ rulesyncSkill }) => rulesyncSkill).filter((skill) => HermesagentSkill.isTargetedByRulesyncSkill(skill)).map((skill) => hermesSlashName(skill.getFrontmatter().name)).filter((slug) => commandSlugs.has(slug));
|
|
11667
12351
|
if (collisions.length > 0) throw new Error(`Hermes command and skill slash-name collision: ${[...new Set(collisions)].toSorted().join(", ")}`);
|
|
11668
12352
|
}
|
|
11669
12353
|
static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
|
|
@@ -11796,6 +12480,8 @@ const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
|
|
|
11796
12480
|
const JUNIE_IGNORE_FILE_NAME = ".aiignore";
|
|
11797
12481
|
const JUNIE_RULE_FILE_NAME = "AGENTS.md";
|
|
11798
12482
|
const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
|
|
12483
|
+
const JUNIE_RULES_DIR_NAME = "rules";
|
|
12484
|
+
const JUNIE_PLAYBOOK_FILE_NAME = "playbook.md";
|
|
11799
12485
|
//#endregion
|
|
11800
12486
|
//#region src/features/commands/junie-command.ts
|
|
11801
12487
|
const JunieCommandFrontmatterSchema = zod_mini.z.looseObject({ description: zod_mini.z.optional(zod_mini.z.string()) });
|
|
@@ -13756,10 +14442,10 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13756
14442
|
global;
|
|
13757
14443
|
getFactory;
|
|
13758
14444
|
flattenedCommandNaming;
|
|
13759
|
-
constructor({ outputRoot = process.cwd(),
|
|
14445
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$5, dryRun = false, flattenedCommandNaming = "basename", logger }) {
|
|
13760
14446
|
super({
|
|
13761
14447
|
outputRoot,
|
|
13762
|
-
|
|
14448
|
+
inputRoots,
|
|
13763
14449
|
dryRun,
|
|
13764
14450
|
logger
|
|
13765
14451
|
});
|
|
@@ -13774,7 +14460,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13774
14460
|
const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
|
|
13775
14461
|
const factory = this.getFactory(this.toolTarget);
|
|
13776
14462
|
await factory.class.validateRulesyncCommands?.({
|
|
13777
|
-
|
|
14463
|
+
inputRoots: this.inputRoots,
|
|
13778
14464
|
rulesyncCommands,
|
|
13779
14465
|
logger: this.logger
|
|
13780
14466
|
});
|
|
@@ -13826,16 +14512,36 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
13826
14512
|
return rel;
|
|
13827
14513
|
}
|
|
13828
14514
|
/**
|
|
13829
|
-
*
|
|
13830
|
-
*
|
|
14515
|
+
* Load rulesync command files from a single source-tree's `commands/`
|
|
14516
|
+
* subtree. `sourceTree` is the source tree itself (e.g.
|
|
14517
|
+
* `/repo/.rulesync` or `/repo/.rulesync.local`).
|
|
13831
14518
|
*/
|
|
13832
|
-
async
|
|
13833
|
-
const
|
|
14519
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
14520
|
+
const treeParent = (0, node_path.dirname)(sourceTree);
|
|
14521
|
+
const treeName = (0, node_path.basename)(sourceTree);
|
|
14522
|
+
const treeCommandsDirPath = (0, node_path.join)(treeName, COMMANDS_FEATURE_SUBDIR);
|
|
14523
|
+
const basePath = (0, node_path.join)(sourceTree, COMMANDS_FEATURE_SUBDIR);
|
|
13834
14524
|
const rulesyncCommandPaths = await findFilesByGlobs((0, node_path.join)(basePath, "**", "*.md"));
|
|
13835
|
-
|
|
13836
|
-
outputRoot:
|
|
14525
|
+
return await Promise.all(rulesyncCommandPaths.map((path) => RulesyncCommand.fromFile({
|
|
14526
|
+
outputRoot: treeParent,
|
|
14527
|
+
relativeDirPath: treeCommandsDirPath,
|
|
13837
14528
|
relativeFilePath: this.safeRelativePath(basePath, path)
|
|
13838
14529
|
})));
|
|
14530
|
+
}
|
|
14531
|
+
/**
|
|
14532
|
+
* Implementation of abstract method from FeatureProcessor
|
|
14533
|
+
* Load and parse rulesync command files from every configured input root's
|
|
14534
|
+
* `.rulesync/commands/` directory, merging by relative path so a command
|
|
14535
|
+
* with the same target path from a later root replaces the earlier root's
|
|
14536
|
+
* copy.
|
|
14537
|
+
*/
|
|
14538
|
+
async loadRulesyncFiles() {
|
|
14539
|
+
const rulesyncCommands = mergeByCaseInsensitiveIdentity({
|
|
14540
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
14541
|
+
identity: (command) => command.getRelativeFilePath(),
|
|
14542
|
+
artifactName: "command",
|
|
14543
|
+
logger: this.logger
|
|
14544
|
+
});
|
|
13839
14545
|
this.logger.debug(`Successfully loaded ${rulesyncCommands.length} rulesync commands`);
|
|
13840
14546
|
return rulesyncCommands;
|
|
13841
14547
|
}
|
|
@@ -19475,6 +20181,7 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
19475
20181
|
const entry = raw;
|
|
19476
20182
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
19477
20183
|
if (vibeEvent === void 0) return null;
|
|
20184
|
+
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
19478
20185
|
const canonicalEvent = VIBE_TO_CANONICAL_EVENT_NAMES[vibeEvent] ?? vibeEvent;
|
|
19479
20186
|
const def = { type: "command" };
|
|
19480
20187
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
@@ -20023,10 +20730,10 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
20023
20730
|
var HooksProcessor = class extends FeatureProcessor {
|
|
20024
20731
|
toolTarget;
|
|
20025
20732
|
global;
|
|
20026
|
-
constructor({ outputRoot = process.cwd(),
|
|
20733
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
20027
20734
|
super({
|
|
20028
20735
|
outputRoot,
|
|
20029
|
-
|
|
20736
|
+
inputRoots,
|
|
20030
20737
|
dryRun,
|
|
20031
20738
|
logger
|
|
20032
20739
|
});
|
|
@@ -20036,9 +20743,17 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
20036
20743
|
this.global = global;
|
|
20037
20744
|
}
|
|
20038
20745
|
async loadRulesyncFiles() {
|
|
20746
|
+
const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncHooks.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
|
|
20747
|
+
const sourceTree = await pickLastRootWithFile({
|
|
20748
|
+
inputRoots: this.inputRoots,
|
|
20749
|
+
relativePaths,
|
|
20750
|
+
logger: this.logger,
|
|
20751
|
+
artifactName: "The hooks file"
|
|
20752
|
+
}) ?? this.inputRoots[0];
|
|
20039
20753
|
try {
|
|
20040
20754
|
return [await RulesyncHooks.fromFile({
|
|
20041
|
-
outputRoot:
|
|
20755
|
+
outputRoot: (0, node_path.dirname)(sourceTree),
|
|
20756
|
+
relativeDirPath: (0, node_path.basename)(sourceTree),
|
|
20042
20757
|
validate: true
|
|
20043
20758
|
})];
|
|
20044
20759
|
} catch (error) {
|
|
@@ -21580,10 +22295,10 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21580
22295
|
getFactory;
|
|
21581
22296
|
featureOptions;
|
|
21582
22297
|
global;
|
|
21583
|
-
constructor({ outputRoot = process.cwd(),
|
|
22298
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$4, global = false, dryRun = false, logger, featureOptions }) {
|
|
21584
22299
|
super({
|
|
21585
22300
|
outputRoot,
|
|
21586
|
-
|
|
22301
|
+
inputRoots,
|
|
21587
22302
|
dryRun,
|
|
21588
22303
|
logger
|
|
21589
22304
|
});
|
|
@@ -21600,11 +22315,35 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
21600
22315
|
}
|
|
21601
22316
|
/**
|
|
21602
22317
|
* Implementation of abstract method from FeatureProcessor
|
|
21603
|
-
*
|
|
22318
|
+
*
|
|
22319
|
+
* Load and parse the rulesync ignore file. `inputRoots[i]` is a source
|
|
22320
|
+
* tree itself (e.g. `/repo/.rulesync.local`); the recommended `.aiignore`
|
|
22321
|
+
* lives directly inside it. The legacy `.rulesyncignore` is shared at the
|
|
22322
|
+
* project root, so it is intentionally not considered when choosing which
|
|
22323
|
+
* source tree wins; `RulesyncIgnore.fromFile` still uses it as a fallback
|
|
22324
|
+
* for the chosen tree.
|
|
22325
|
+
*
|
|
22326
|
+
* When multiple input roots are configured, the last root that provides
|
|
22327
|
+
* an ignore file wins entirely (whole-file replacement — no line-level
|
|
22328
|
+
* merge in this slice; see the "Deliberately out of scope" section of
|
|
22329
|
+
* the inputRoots plan for context). If no root has the file, fall back
|
|
22330
|
+
* to the primary root's path so the underlying `RulesyncIgnore.fromFile`
|
|
22331
|
+
* surfaces the same missing-file error it would in the single-root case.
|
|
21604
22332
|
*/
|
|
21605
22333
|
async loadRulesyncFiles() {
|
|
22334
|
+
const paths = RulesyncIgnore.getSettablePaths();
|
|
22335
|
+
const relativePaths = getRulesyncSourceCandidates({ paths }).filter((candidate) => candidate.relativeDirPath === paths.recommended.relativeDirPath).map((candidate) => candidate.relativeFilePath);
|
|
22336
|
+
const sourceTree = await pickLastRootWithFile({
|
|
22337
|
+
inputRoots: this.inputRoots,
|
|
22338
|
+
relativePaths,
|
|
22339
|
+
logger: this.logger,
|
|
22340
|
+
artifactName: "The ignore file (.aiignore)"
|
|
22341
|
+
}) ?? this.inputRoots[0];
|
|
21606
22342
|
try {
|
|
21607
|
-
return [await RulesyncIgnore.fromFile({
|
|
22343
|
+
return [await RulesyncIgnore.fromFile({
|
|
22344
|
+
outputRoot: (0, node_path.dirname)(sourceTree),
|
|
22345
|
+
relativeDirPath: (0, node_path.basename)(sourceTree)
|
|
22346
|
+
})];
|
|
21608
22347
|
} catch (error) {
|
|
21609
22348
|
this.logger.error(`Failed to load rulesync ignore file (${RULESYNC_AIIGNORE_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
21610
22349
|
return [];
|
|
@@ -26639,11 +27378,86 @@ function disabledNamesOf(config) {
|
|
|
26639
27378
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
26640
27379
|
}
|
|
26641
27380
|
/**
|
|
27381
|
+
* The value `mcp.mcpConfigPath` needs so Rovo Dev reads the project-scope
|
|
27382
|
+
* `.rovodev/mcp.json`. A config-file value, not a filesystem path, so it is
|
|
27383
|
+
* always POSIX-separated.
|
|
27384
|
+
*/
|
|
27385
|
+
const ROVODEV_PROJECT_MCP_CONFIG_POINTER = node_path.posix.join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME);
|
|
27386
|
+
/**
|
|
27387
|
+
* The keys that make an entry in `mcp.json` something Rovo Dev can start:
|
|
27388
|
+
* a local process, or a remote endpoint under either spelling the canonical
|
|
27389
|
+
* config accepts.
|
|
27390
|
+
*/
|
|
27391
|
+
const MCP_SERVER_ENDPOINT_KEYS = [
|
|
27392
|
+
"command",
|
|
27393
|
+
"url",
|
|
27394
|
+
"httpUrl"
|
|
27395
|
+
];
|
|
27396
|
+
function normalizeMcpConfigPathValue(value) {
|
|
27397
|
+
return toPosixPath(value).replace(/^\.\//, "");
|
|
27398
|
+
}
|
|
27399
|
+
/**
|
|
27400
|
+
* Point `mcp.mcpConfigPath` at the project-scope `mcp.json` rulesync writes,
|
|
27401
|
+
* and report whether the block gained a value it did not already carry.
|
|
27402
|
+
*
|
|
27403
|
+
* Rovo Dev's `mcpConfigPath` defaults to a file under the user's home
|
|
27404
|
+
* directory, so a repo-committed `.rovodev/mcp.json` is inert until the active
|
|
27405
|
+
* config points at it — the Bitbucket Agentic Pipelines guide documents
|
|
27406
|
+
* registering the server and setting the pointer as two required steps. Global
|
|
27407
|
+
* scope is left alone: there the default already resolves to the file rulesync
|
|
27408
|
+
* writes.
|
|
27409
|
+
*
|
|
27410
|
+
* The pointer names one config rather than merging with the default, so it is
|
|
27411
|
+
* written only when this project actually has a Rovo Dev server to run — a
|
|
27412
|
+
* server that targets `rovodev` and is not disabled. Otherwise `mcp.json` is
|
|
27413
|
+
* generated empty, and pointing at it would take away the user's global
|
|
27414
|
+
* servers for this repository in exchange for nothing.
|
|
27415
|
+
*
|
|
27416
|
+
* That condition can also stop holding after the fact, once the last server is
|
|
27417
|
+
* removed from the canonical config or switched off. Rulesync does not take
|
|
27418
|
+
* the pointer back out — it cannot tell its own past value from a user who
|
|
27419
|
+
* typed the same string — but it says so, since the file is now a live setting
|
|
27420
|
+
* that resolves to nothing.
|
|
27421
|
+
*
|
|
27422
|
+
* A pointer the user aimed somewhere else is theirs, not ours: overwriting it
|
|
27423
|
+
* would silently redirect Rovo Dev away from a file they chose. It is named in
|
|
27424
|
+
* a warning instead, because the generated `mcp.json` is unread while it
|
|
27425
|
+
* stands.
|
|
27426
|
+
*
|
|
27427
|
+
* Whatever the outcome, it is logged: writing the pointer turns servers that
|
|
27428
|
+
* were generated-but-never-read into servers Rovo Dev actually spawns, and
|
|
27429
|
+
* points it away from the global MCP config, so it is not something to do
|
|
27430
|
+
* quietly.
|
|
27431
|
+
*
|
|
27432
|
+
* @see https://support.atlassian.com/bitbucket-cloud/docs/rovo-dev-advanced-agentic-configuration/
|
|
27433
|
+
* @see https://support.atlassian.com/rovo/docs/manage-rovo-dev-cli-settings/
|
|
27434
|
+
*/
|
|
27435
|
+
function applyProjectMcpConfigPointer({ existingMcp, global, hasLiveServers, logger }) {
|
|
27436
|
+
if (global) return false;
|
|
27437
|
+
const existing = existingMcp.mcpConfigPath;
|
|
27438
|
+
const pointsAtGeneratedFile = typeof existing === "string" && normalizeMcpConfigPathValue(existing) === ROVODEV_PROJECT_MCP_CONFIG_POINTER;
|
|
27439
|
+
if (!hasLiveServers) {
|
|
27440
|
+
if (pointsAtGeneratedFile) logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} points at ${(0, node_path.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.`);
|
|
27441
|
+
return false;
|
|
27442
|
+
}
|
|
27443
|
+
if (existing === void 0) {
|
|
27444
|
+
existingMcp.mcpConfigPath = ROVODEV_PROJECT_MCP_CONFIG_POINTER;
|
|
27445
|
+
logger?.info(`Rovo Dev MCP: setting mcp.mcpConfigPath to "${ROVODEV_PROJECT_MCP_CONFIG_POINTER}" in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Rovo Dev will now launch the servers in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} for this project instead of the ones in the default global MCP config.`);
|
|
27446
|
+
return true;
|
|
27447
|
+
}
|
|
27448
|
+
if (pointsAtGeneratedFile) return false;
|
|
27449
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${JSON.stringify(existing)} in ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Rovo Dev reads MCP servers from that path, so the generated ${(0, node_path.join)(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} is unused until it is set to "${ROVODEV_PROJECT_MCP_CONFIG_POINTER}".`);
|
|
27450
|
+
return false;
|
|
27451
|
+
}
|
|
27452
|
+
/**
|
|
26642
27453
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
26643
27454
|
* `~/.rovodev/config.yml` (global). Carries `disabledMcpServers` — the key
|
|
26644
|
-
* Rovo Dev actually consults to switch a server off —
|
|
26645
|
-
*
|
|
26646
|
-
*
|
|
27455
|
+
* Rovo Dev actually consults to switch a server off — plus `mcpConfigPath`,
|
|
27456
|
+
* which rulesync authors in project scope when the key is absent and this
|
|
27457
|
+
* project has a server to run (see `applyProjectMcpConfigPointer`). The block
|
|
27458
|
+
* is recomputed from the existing one, so user keys (`allowedMcpServers`,
|
|
27459
|
+
* ...), a `mcpConfigPath` the user aimed elsewhere, and disabled names for
|
|
27460
|
+
* servers rulesync does not manage all survive.
|
|
26647
27461
|
*/
|
|
26648
27462
|
var RovodevMcpConfigYaml = class extends ToolFile {
|
|
26649
27463
|
isDeletable() {
|
|
@@ -26776,7 +27590,17 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
26776
27590
|
const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
|
|
26777
27591
|
if (mergedDisabled.length > 0) existingMcp.disabledMcpServers = mergedDisabled;
|
|
26778
27592
|
else delete existingMcp.disabledMcpServers;
|
|
26779
|
-
|
|
27593
|
+
const wrotePointer = applyProjectMcpConfigPointer({
|
|
27594
|
+
existingMcp,
|
|
27595
|
+
global,
|
|
27596
|
+
hasLiveServers: managedNames.filter((name) => {
|
|
27597
|
+
if (disabledNames.includes(name)) return false;
|
|
27598
|
+
const server = servers[name];
|
|
27599
|
+
return isRecord$1(server) && MCP_SERVER_ENDPOINT_KEYS.some((endpointKey) => server[endpointKey] !== void 0);
|
|
27600
|
+
}).length > 0,
|
|
27601
|
+
logger
|
|
27602
|
+
});
|
|
27603
|
+
if (mergedDisabled.length === 0 && !wrotePointer && existingContent.trim() === "") return [];
|
|
26780
27604
|
const fileContent = applySharedConfigPatch({
|
|
26781
27605
|
fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
|
|
26782
27606
|
feature: "mcp",
|
|
@@ -27869,10 +28693,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27869
28693
|
toolTarget;
|
|
27870
28694
|
global;
|
|
27871
28695
|
getFactory;
|
|
27872
|
-
constructor({ outputRoot = process.cwd(),
|
|
28696
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$3, dryRun = false, logger }) {
|
|
27873
28697
|
super({
|
|
27874
28698
|
outputRoot,
|
|
27875
|
-
|
|
28699
|
+
inputRoots,
|
|
27876
28700
|
dryRun,
|
|
27877
28701
|
logger
|
|
27878
28702
|
});
|
|
@@ -27888,7 +28712,10 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27888
28712
|
*/
|
|
27889
28713
|
async loadRulesyncFiles() {
|
|
27890
28714
|
try {
|
|
27891
|
-
return [await RulesyncMcp.
|
|
28715
|
+
return [await RulesyncMcp.fromRoots({
|
|
28716
|
+
inputRoots: this.inputRoots,
|
|
28717
|
+
logger: this.logger
|
|
28718
|
+
})];
|
|
27892
28719
|
} catch (error) {
|
|
27893
28720
|
this.logger.error(`Failed to load a Rulesync MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`);
|
|
27894
28721
|
return [];
|
|
@@ -29430,27 +30257,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
29430
30257
|
return { permission };
|
|
29431
30258
|
}
|
|
29432
30259
|
//#endregion
|
|
29433
|
-
//#region src/utils/control-characters.ts
|
|
29434
|
-
/**
|
|
29435
|
-
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
29436
|
-
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
29437
|
-
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
29438
|
-
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
29439
|
-
* settings file must never reach the terminal with these intact: they let the
|
|
29440
|
-
* text forge log lines, reorder what is printed around them, or inject escape
|
|
29441
|
-
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
29442
|
-
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
29443
|
-
* place to preserve the typography of a right-to-left name.
|
|
29444
|
-
*/
|
|
29445
|
-
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
29446
|
-
/**
|
|
29447
|
-
* Removes every control character from `text` so it is safe to splice into a
|
|
29448
|
-
* log line or other terminal output.
|
|
29449
|
-
*/
|
|
29450
|
-
function stripControlCharacters(text) {
|
|
29451
|
-
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
29452
|
-
}
|
|
29453
|
-
//#endregion
|
|
29454
30260
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
29455
30261
|
/**
|
|
29456
30262
|
* Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
|
|
@@ -29536,8 +30342,8 @@ function deepMergeRecords(base, patch) {
|
|
|
29536
30342
|
*
|
|
29537
30343
|
* Deliberately NOT listed:
|
|
29538
30344
|
* - `ripgrep` / `bwrapPath` / `socatPath`: each names an executable, so
|
|
29539
|
-
* `
|
|
29540
|
-
* emitting them under `--global`.
|
|
30345
|
+
* `CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL` refuses them in both scopes
|
|
30346
|
+
* rather than emitting them under `--global`.
|
|
29541
30347
|
* - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
|
|
29542
30348
|
* unit is the individual entry's mode, not the settings key, and the same
|
|
29543
30349
|
* lists carry `deny` entries that project settings *do* honor — dropping a
|
|
@@ -29556,6 +30362,30 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
|
29556
30362
|
["allowAppleEvents"]
|
|
29557
30363
|
];
|
|
29558
30364
|
/**
|
|
30365
|
+
* `sandbox.*` paths documented with a `Managed` scope: Claude Code reads them
|
|
30366
|
+
* only from the settings file an organization deploys. Neither file rulesync
|
|
30367
|
+
* writes is that file, so they are dropped in **both** scopes — the `sandbox`
|
|
30368
|
+
* counterpart of {@link CLAUDECODE_UNHONORED_KEY_SOURCES}, which does the same
|
|
30369
|
+
* for top-level `Managed` keys.
|
|
30370
|
+
*
|
|
30371
|
+
* Both only ever *narrow* the policy — they stop a lower-scoped file from
|
|
30372
|
+
* re-opening what managed settings blocked — so neither is trust-widening. They
|
|
30373
|
+
* are dropped rather than written for the opposite reason: written into a
|
|
30374
|
+
* project or user file they do nothing at all, and a `sandbox` block that reads
|
|
30375
|
+
* as though it locked the policy to managed values while Claude Code ignores it
|
|
30376
|
+
* is the more dangerous of the two failure modes.
|
|
30377
|
+
*
|
|
30378
|
+
* Import keeps them, unlike the command-executing paths: the value in an
|
|
30379
|
+
* existing `settings.json` was hand-written to be honored somewhere, and
|
|
30380
|
+
* round-tripping it preserves the author's intent for the day it moves into a
|
|
30381
|
+
* managed file. The cost is a warning on every generate until it is removed,
|
|
30382
|
+
* which the refusal message points at.
|
|
30383
|
+
*
|
|
30384
|
+
* @see https://code.claude.com/docs/en/settings-reference#sandbox-filesystem-allowmanagedreadpathsonly
|
|
30385
|
+
* — "Scope: `Managed`"; the `network` entry says the same.
|
|
30386
|
+
*/
|
|
30387
|
+
const CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS = [["filesystem", "allowManagedReadPathsOnly"], ["network", "allowManagedDomainsOnly"]];
|
|
30388
|
+
/**
|
|
29559
30389
|
* Walks `segments` from `root`, returning the record they name or `undefined` if
|
|
29560
30390
|
* any step is missing or not a record. Shared by everything below that addresses
|
|
29561
30391
|
* a `sandbox` path, so a nested path added to one of the tables is actually
|
|
@@ -29602,6 +30432,18 @@ function deleteSandboxPath({ target, path }) {
|
|
|
29602
30432
|
return true;
|
|
29603
30433
|
}
|
|
29604
30434
|
/**
|
|
30435
|
+
* The one warning that names every trust-affecting setting this generate wrote
|
|
30436
|
+
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
30437
|
+
* matter, but the "review this as you would a hook" framing only needs saying
|
|
30438
|
+
* once, and repeating it per key buries the reasons in boilerplate.
|
|
30439
|
+
*/
|
|
30440
|
+
function warnOnTrustAffectingEntries({ entries, relativeFilePath, logger }) {
|
|
30441
|
+
if (entries.length === 0) return;
|
|
30442
|
+
const one = entries.length === 1;
|
|
30443
|
+
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
30444
|
+
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}.`);
|
|
30445
|
+
}
|
|
30446
|
+
/**
|
|
29605
30447
|
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
29606
30448
|
* than the default. `plan` and `default` are absent because they do not widen
|
|
29607
30449
|
* anything.
|
|
@@ -29614,14 +30456,22 @@ const CLAUDECODE_WIDENING_DEFAULT_MODES = {
|
|
|
29614
30456
|
/**
|
|
29615
30457
|
* The `permissions` fields that widen rather than restrict: a `defaultMode` that
|
|
29616
30458
|
* removes prompts, and `additionalDirectories`, which moves the
|
|
29617
|
-
* working-directory boundary.
|
|
29618
|
-
* shareable permissions file should not loosen the permission system quietly.
|
|
30459
|
+
* working-directory boundary. Reported for the same reason `disableAllHooks` is:
|
|
30460
|
+
* a shareable permissions file should not loosen the permission system quietly.
|
|
29619
30461
|
*/
|
|
29620
|
-
function
|
|
30462
|
+
function collectWideningPermissionFields({ fields }) {
|
|
30463
|
+
const entries = [];
|
|
29621
30464
|
const defaultMode = fields.defaultMode;
|
|
29622
|
-
if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, defaultMode))
|
|
30465
|
+
if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, defaultMode)) entries.push({
|
|
30466
|
+
label: `permissions.defaultMode: "${defaultMode}"`,
|
|
30467
|
+
reason: CLAUDECODE_WIDENING_DEFAULT_MODES[defaultMode]
|
|
30468
|
+
});
|
|
29623
30469
|
const additionalDirectories = fields.additionalDirectories;
|
|
29624
|
-
if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0))
|
|
30470
|
+
if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0)) entries.push({
|
|
30471
|
+
label: "permissions.additionalDirectories",
|
|
30472
|
+
reason: "moves the boundary of what Claude Code may read and edit outside the project"
|
|
30473
|
+
});
|
|
30474
|
+
return entries;
|
|
29625
30475
|
}
|
|
29626
30476
|
/**
|
|
29627
30477
|
* `sandbox` paths whose value names a binary Claude Code runs. `sandbox` has its
|
|
@@ -29638,6 +30488,18 @@ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
|
29638
30488
|
["socatPath"]
|
|
29639
30489
|
];
|
|
29640
30490
|
/**
|
|
30491
|
+
* The predicates the "which value actually widens?" tables are built from.
|
|
30492
|
+
* Each names the value that does *not* widen and reports everything else, never
|
|
30493
|
+
* the reverse: the override is authored JSONC, so a key can carry any value at
|
|
30494
|
+
* all, and one Claude Code coerces is still honored. Reporting an off-type value
|
|
30495
|
+
* keeps the warning fail-safe — silence has to mean "this cannot loosen
|
|
30496
|
+
* anything", not "this is not the type the table expected".
|
|
30497
|
+
*/
|
|
30498
|
+
const isNotFalse = (value) => value !== false;
|
|
30499
|
+
const isNotTrue = (value) => value !== true;
|
|
30500
|
+
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
30501
|
+
const isNonEmptyMap = (value) => !isPlainRecord(value) || Object.keys(value).length > 0;
|
|
30502
|
+
/**
|
|
29641
30503
|
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
29642
30504
|
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
29643
30505
|
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
@@ -29657,77 +30519,77 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
29657
30519
|
{
|
|
29658
30520
|
path: ["allowAppleEvents"],
|
|
29659
30521
|
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
29660
|
-
widens:
|
|
30522
|
+
widens: isNotFalse
|
|
29661
30523
|
},
|
|
29662
30524
|
{
|
|
29663
30525
|
path: ["allowUnsandboxedCommands"],
|
|
29664
30526
|
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
29665
|
-
widens:
|
|
30527
|
+
widens: isNotFalse
|
|
29666
30528
|
},
|
|
29667
30529
|
{
|
|
29668
30530
|
path: ["autoAllowBashIfSandboxed"],
|
|
29669
30531
|
reason: "controls whether every Bash command the sandbox accepts runs without a prompt",
|
|
29670
|
-
widens:
|
|
30532
|
+
widens: isNotFalse
|
|
29671
30533
|
},
|
|
29672
30534
|
{
|
|
29673
30535
|
path: ["enableWeakerNestedSandbox"],
|
|
29674
30536
|
reason: "runs the Linux sandbox inside an unprivileged container, which weakens it",
|
|
29675
|
-
widens:
|
|
30537
|
+
widens: isNotFalse
|
|
29676
30538
|
},
|
|
29677
30539
|
{
|
|
29678
30540
|
path: ["enableWeakerNetworkIsolation"],
|
|
29679
30541
|
reason: "weakens the sandbox's network isolation on macOS",
|
|
29680
|
-
widens:
|
|
30542
|
+
widens: isNotFalse
|
|
29681
30543
|
},
|
|
29682
30544
|
{
|
|
29683
30545
|
path: ["enabled"],
|
|
29684
30546
|
reason: "turns the sandbox on, and sandboxed Bash commands then run without a permission prompt unless `autoAllowBashIfSandboxed` is false",
|
|
29685
|
-
widens:
|
|
30547
|
+
widens: isNotFalse
|
|
29686
30548
|
},
|
|
29687
30549
|
{
|
|
29688
30550
|
path: ["excludedCommands"],
|
|
29689
30551
|
reason: "names commands that always run outside the sandbox, with no sandbox policy applied",
|
|
29690
|
-
widens:
|
|
30552
|
+
widens: isNonEmptyList
|
|
29691
30553
|
},
|
|
29692
30554
|
{
|
|
29693
30555
|
path: ["filesystem", "allowRead"],
|
|
29694
30556
|
reason: "re-opens reading inside a region the sandbox's `denyRead` blocks",
|
|
29695
|
-
widens:
|
|
30557
|
+
widens: isNonEmptyList
|
|
29696
30558
|
},
|
|
29697
30559
|
{
|
|
29698
30560
|
path: ["filesystem", "allowWrite"],
|
|
29699
30561
|
reason: "adds paths sandboxed commands may write to, outside the working directory",
|
|
29700
|
-
widens:
|
|
30562
|
+
widens: isNonEmptyList
|
|
29701
30563
|
},
|
|
29702
30564
|
{
|
|
29703
30565
|
path: ["ignoreViolations"],
|
|
29704
30566
|
reason: "hides the sandbox violations it names, so a blocked access stops being reported",
|
|
29705
|
-
widens: (value) =>
|
|
30567
|
+
widens: (value) => isNonEmptyMap(value) && isNotFalse(value)
|
|
29706
30568
|
},
|
|
29707
30569
|
{
|
|
29708
30570
|
path: ["network", "allowAllUnixSockets"],
|
|
29709
30571
|
reason: "lets sandboxed commands connect to every Unix socket",
|
|
29710
|
-
widens:
|
|
30572
|
+
widens: isNotFalse
|
|
29711
30573
|
},
|
|
29712
30574
|
{
|
|
29713
30575
|
path: ["network", "allowedDomains"],
|
|
29714
30576
|
reason: "pre-allows domains sandboxed commands may reach without a prompt",
|
|
29715
|
-
widens:
|
|
30577
|
+
widens: isNonEmptyList
|
|
29716
30578
|
},
|
|
29717
30579
|
{
|
|
29718
30580
|
path: ["network", "allowLocalBinding"],
|
|
29719
30581
|
reason: "lets sandboxed commands bind local ports",
|
|
29720
|
-
widens:
|
|
30582
|
+
widens: isNotFalse
|
|
29721
30583
|
},
|
|
29722
30584
|
{
|
|
29723
30585
|
path: ["network", "allowMachLookup"],
|
|
29724
30586
|
reason: "names the macOS services sandboxed commands may reach, and `*` means every service",
|
|
29725
|
-
widens:
|
|
30587
|
+
widens: isNonEmptyList
|
|
29726
30588
|
},
|
|
29727
30589
|
{
|
|
29728
30590
|
path: ["network", "allowUnixSockets"],
|
|
29729
30591
|
reason: "names Unix sockets sandboxed commands may reach, and one such as `/var/run/docker.sock` is host access",
|
|
29730
|
-
widens:
|
|
30592
|
+
widens: isNonEmptyList
|
|
29731
30593
|
},
|
|
29732
30594
|
{
|
|
29733
30595
|
path: ["network", "httpProxyPort"],
|
|
@@ -29741,11 +30603,12 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
29741
30603
|
}
|
|
29742
30604
|
];
|
|
29743
30605
|
/**
|
|
29744
|
-
*
|
|
29745
|
-
*
|
|
29746
|
-
*
|
|
30606
|
+
* Every authored `sandbox` path that loosens the sandbox. Nothing is removed —
|
|
30607
|
+
* the values are written, just not silently. Called on the filtered `sandbox`
|
|
30608
|
+
* so it never claims to be writing a path the scope filters dropped.
|
|
29747
30609
|
*/
|
|
29748
|
-
function
|
|
30610
|
+
function collectTrustAffectingSandboxPaths({ sandbox }) {
|
|
30611
|
+
const entries = [];
|
|
29749
30612
|
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
29750
30613
|
const leaf = path.at(-1);
|
|
29751
30614
|
if (leaf === void 0) continue;
|
|
@@ -29756,38 +30619,49 @@ function warnOnTrustAffectingSandboxPaths({ sandbox, relativeFilePath, logger })
|
|
|
29756
30619
|
if (parent === void 0) continue;
|
|
29757
30620
|
const value = parent[leaf];
|
|
29758
30621
|
if (value === void 0 || !widens(value)) continue;
|
|
29759
|
-
|
|
30622
|
+
entries.push({
|
|
30623
|
+
label: `sandbox.${path.join(".")}`,
|
|
30624
|
+
reason
|
|
30625
|
+
});
|
|
29760
30626
|
}
|
|
30627
|
+
return entries;
|
|
29761
30628
|
}
|
|
30629
|
+
/** Paths that name an executable Claude Code runs. Refused in both scopes. */
|
|
30630
|
+
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL = {
|
|
30631
|
+
paths: CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS,
|
|
30632
|
+
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.`
|
|
30633
|
+
};
|
|
29762
30634
|
/**
|
|
29763
|
-
*
|
|
29764
|
-
*
|
|
30635
|
+
* Paths Claude Code honors only from managed settings. Refused in both scopes,
|
|
30636
|
+
* like the command-executing paths, because managed settings are not a file
|
|
30637
|
+
* rulesync writes in either of them.
|
|
29765
30638
|
*/
|
|
29766
|
-
|
|
29767
|
-
|
|
29768
|
-
|
|
29769
|
-
|
|
29770
|
-
|
|
29771
|
-
|
|
29772
|
-
|
|
29773
|
-
|
|
29774
|
-
|
|
29775
|
-
return filtered;
|
|
29776
|
-
}
|
|
30639
|
+
const CLAUDECODE_MANAGED_ONLY_SANDBOX_REFUSAL = {
|
|
30640
|
+
paths: CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS,
|
|
30641
|
+
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.`
|
|
30642
|
+
};
|
|
30643
|
+
/** Paths Claude Code honors only above project scope. Refused at project scope. */
|
|
30644
|
+
const CLAUDECODE_GLOBAL_ONLY_SANDBOX_REFUSAL = {
|
|
30645
|
+
paths: CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS,
|
|
30646
|
+
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.`
|
|
30647
|
+
};
|
|
29777
30648
|
/**
|
|
29778
|
-
* Copy of the authored `sandbox` override with
|
|
29779
|
-
* removed, warning once per dropped path. Only the override copy is
|
|
29780
|
-
* a value already hand-written in the target file is left untouched,
|
|
29781
|
-
* the `qwencode` `security.allowPrivateNetworkHooks` precedent.
|
|
30649
|
+
* Copy of the authored `sandbox` override with every path of every passed
|
|
30650
|
+
* refusal removed, warning once per dropped path. Only the override copy is
|
|
30651
|
+
* filtered — a value already hand-written in the target file is left untouched,
|
|
30652
|
+
* matching the `qwencode` `security.allowPrivateNetworkHooks` precedent.
|
|
29782
30653
|
*/
|
|
29783
|
-
function
|
|
30654
|
+
function stripSandboxPaths({ sandbox, refusals, relativeFilePath, logger }) {
|
|
29784
30655
|
const filtered = structuredClone(sandbox);
|
|
29785
|
-
for (const path of
|
|
30656
|
+
for (const { paths, warn } of refusals) for (const path of paths) {
|
|
29786
30657
|
if (!deleteSandboxPath({
|
|
29787
30658
|
target: filtered,
|
|
29788
30659
|
path
|
|
29789
30660
|
})) continue;
|
|
29790
|
-
logger?.warn(
|
|
30661
|
+
logger?.warn(warn({
|
|
30662
|
+
label: `sandbox.${path.join(".")}`,
|
|
30663
|
+
relativeFilePath
|
|
30664
|
+
}));
|
|
29791
30665
|
}
|
|
29792
30666
|
return filtered;
|
|
29793
30667
|
}
|
|
@@ -29811,7 +30685,7 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
|
|
|
29811
30685
|
* such entries to `deny`), so the "reads as masked but isn't" state this guards
|
|
29812
30686
|
* against cannot slip through a differently-spelled value.
|
|
29813
30687
|
*
|
|
29814
|
-
* Like `
|
|
30688
|
+
* Like `stripSandboxPaths`, only the override copy is filtered — a
|
|
29815
30689
|
* value already in the target file is left untouched, which is why the warning
|
|
29816
30690
|
* points at it.
|
|
29817
30691
|
*
|
|
@@ -29978,6 +30852,9 @@ const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
|
29978
30852
|
allowedHttpHookUrls: "limits which URLs an HTTP hook may target, and an empty list means every URL",
|
|
29979
30853
|
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",
|
|
29980
30854
|
autoMode: "auto-approves shell commands with a classifier rather than with a prompt",
|
|
30855
|
+
claudeMdExcludes: "skips the CLAUDE.md files its patterns match, so the instructions a repository relies on can be dropped from every session",
|
|
30856
|
+
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",
|
|
30857
|
+
crossSessionInbound: "decides what a session does with messages arriving from your other Claude Code sessions, and `accept` delivers them straight to Claude",
|
|
29981
30858
|
disableAllHooks: "controls whether hooks run at all",
|
|
29982
30859
|
disableSkillShellExecution: "re-opens the inline shell commands in a skill or custom command that a user setting had turned off",
|
|
29983
30860
|
enableAllProjectMcpServers: "auto-approves every server in the project `.mcp.json`",
|
|
@@ -29986,17 +30863,58 @@ const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
|
29986
30863
|
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",
|
|
29987
30864
|
extraKnownMarketplaces: "registers plugin marketplace sources",
|
|
29988
30865
|
httpHookAllowedEnvVars: "controls which environment variables an HTTP hook may put in a request header, credentials included",
|
|
30866
|
+
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",
|
|
29989
30867
|
outputStyle: "replaces the system prompt every session runs with",
|
|
30868
|
+
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",
|
|
30869
|
+
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",
|
|
29990
30870
|
skipAutoPermissionPrompt: "removes the confirmation shown before auto-approval mode starts",
|
|
29991
|
-
skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check starts"
|
|
30871
|
+
skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check starts",
|
|
30872
|
+
skipWebFetchPreflight: "turns off the WebFetch domain safety check, so WebFetch retrieves any URL without consulting Anthropic's blocklist"
|
|
29992
30873
|
};
|
|
29993
30874
|
/**
|
|
29994
30875
|
* The keys from the table above that only widen at one particular value.
|
|
29995
30876
|
* `disableSkillShellExecution: true` turns inline shell execution off, which
|
|
29996
|
-
* restricts;
|
|
29997
|
-
* use to undo a user setting, so only
|
|
29998
|
-
|
|
29999
|
-
|
|
30877
|
+
* restricts; anything else re-opens it, and that is what a fetched override
|
|
30878
|
+
* could use to undo a user setting. The rest default to off, so only a value
|
|
30879
|
+
* other than the one that leaves them off is worth a line — and for the
|
|
30880
|
+
* list-valued and map-valued keys, only a non-empty one, since an empty list or
|
|
30881
|
+
* map excludes, announces and overrides nothing.
|
|
30882
|
+
*/
|
|
30883
|
+
const CLAUDECODE_TRUST_KEY_WIDENING_VALUES = {
|
|
30884
|
+
claudeMdExcludes: isNonEmptyList,
|
|
30885
|
+
companyAnnouncements: isNonEmptyList,
|
|
30886
|
+
crossSessionInbound: (value) => value !== "hold" && value !== "refuse",
|
|
30887
|
+
disableSkillShellExecution: isNotTrue,
|
|
30888
|
+
modelOverrides: isNonEmptyMap,
|
|
30889
|
+
remoteControlAtStartup: isNotFalse,
|
|
30890
|
+
skipWebFetchPreflight: isNotFalse
|
|
30891
|
+
};
|
|
30892
|
+
/**
|
|
30893
|
+
* Top-level keys a project-scoped `.claude/settings.json` honors at one value
|
|
30894
|
+
* but ignores at another — the value-level counterpart of
|
|
30895
|
+
* {@link CLAUDECODE_USER_SCOPE_ONLY_KEYS}, which is scoped per key. The ignored
|
|
30896
|
+
* value is dropped at project scope for the same reason a wholly unhonored key
|
|
30897
|
+
* is: committing it would read as a policy that never applies.
|
|
30898
|
+
*
|
|
30899
|
+
* `remoteControlAtStartup` is the only entry whose honored value can be decided
|
|
30900
|
+
* from the value alone. Claude Code honors a `false` from project or local
|
|
30901
|
+
* settings — a repository may turn auto-connect off for its own checkout — but
|
|
30902
|
+
* ignores a `true`, so that a checked-in file cannot turn Remote Control on for
|
|
30903
|
+
* everyone who opens the repository.
|
|
30904
|
+
*
|
|
30905
|
+
* `crossSessionInbound` is on the same documented list but deliberately absent
|
|
30906
|
+
* here: it is a ladder (`accept` < `hold` < `refuse`) whose project value is
|
|
30907
|
+
* honored only when it is stricter than the one above it, which no per-value
|
|
30908
|
+
* predicate can decide without reading the user's own settings. It is warned
|
|
30909
|
+
* about through {@link CLAUDECODE_TRUST_AFFECTING_KEYS} instead, since under
|
|
30910
|
+
* `--global` its loosening value is honored outright.
|
|
30911
|
+
*
|
|
30912
|
+
* @see https://code.claude.com/docs/en/settings#security-keys-where-the-stricter-value-applies
|
|
30913
|
+
*/
|
|
30914
|
+
const CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES = { remoteControlAtStartup: {
|
|
30915
|
+
ignored: isNotFalse,
|
|
30916
|
+
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"
|
|
30917
|
+
} };
|
|
30000
30918
|
/**
|
|
30001
30919
|
* A key name is authored data that ends up in a log line, so strip the control
|
|
30002
30920
|
* characters that would let it forge a line or hide the warnings beside it, and
|
|
@@ -30024,12 +30942,13 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
|
|
|
30024
30942
|
/**
|
|
30025
30943
|
* Copy of the authored top-level passthrough with the keys the target file
|
|
30026
30944
|
* cannot honor removed, warning once per dropped key. Like
|
|
30027
|
-
* `
|
|
30945
|
+
* `stripSandboxPaths`, only the override copy is filtered — a value
|
|
30028
30946
|
* already hand-written in the target file is left untouched, which is why the
|
|
30029
30947
|
* warning points at it.
|
|
30030
30948
|
*/
|
|
30031
30949
|
function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logger }) {
|
|
30032
30950
|
const filtered = {};
|
|
30951
|
+
const trustAffecting = [];
|
|
30033
30952
|
for (const [key, value] of Object.entries(overrides)) {
|
|
30034
30953
|
const shown = displayKey(key);
|
|
30035
30954
|
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
@@ -30045,11 +30964,22 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
|
|
|
30045
30964
|
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.`);
|
|
30046
30965
|
continue;
|
|
30047
30966
|
}
|
|
30048
|
-
const
|
|
30049
|
-
if (
|
|
30967
|
+
const projectIgnored = Object.hasOwn(CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES, canonicalKey) ? CLAUDECODE_PROJECT_SCOPE_IGNORED_VALUES[canonicalKey] : void 0;
|
|
30968
|
+
if (!global && projectIgnored !== void 0 && projectIgnored.ignored(value)) {
|
|
30969
|
+
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.`);
|
|
30970
|
+
continue;
|
|
30971
|
+
}
|
|
30972
|
+
const widensAtValue = Object.hasOwn(CLAUDECODE_TRUST_KEY_WIDENING_VALUES, canonicalKey) ? CLAUDECODE_TRUST_KEY_WIDENING_VALUES[canonicalKey] : void 0;
|
|
30973
|
+
if (Object.hasOwn(CLAUDECODE_TRUST_AFFECTING_KEYS, canonicalKey) && (widensAtValue === void 0 || widensAtValue(value))) trustAffecting.push({
|
|
30974
|
+
label: shown,
|
|
30975
|
+
reason: CLAUDECODE_TRUST_AFFECTING_KEYS[canonicalKey]
|
|
30976
|
+
});
|
|
30050
30977
|
filtered[key] = value;
|
|
30051
30978
|
}
|
|
30052
|
-
return
|
|
30979
|
+
return {
|
|
30980
|
+
filtered,
|
|
30981
|
+
trustAffecting
|
|
30982
|
+
};
|
|
30053
30983
|
}
|
|
30054
30984
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
30055
30985
|
Write: "Edit",
|
|
@@ -30119,15 +31049,12 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30119
31049
|
config,
|
|
30120
31050
|
logger
|
|
30121
31051
|
});
|
|
31052
|
+
const trustAffecting = [];
|
|
30122
31053
|
const overridePermissions = config.claudecode?.permissions;
|
|
30123
31054
|
if (overridePermissions && typeof overridePermissions === "object") {
|
|
30124
31055
|
const { allow: _a, ask: _k, deny: _d, ...rest } = overridePermissions;
|
|
30125
31056
|
const nonListFields = Object.fromEntries(Object.entries(rest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
30126
|
-
|
|
30127
|
-
fields: nonListFields,
|
|
30128
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30129
|
-
logger
|
|
30130
|
-
});
|
|
31057
|
+
trustAffecting.push(...collectWideningPermissionFields({ fields: nonListFields }));
|
|
30131
31058
|
settings.permissions = {
|
|
30132
31059
|
...settings.permissions,
|
|
30133
31060
|
...nonListFields
|
|
@@ -30135,25 +31062,22 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30135
31062
|
}
|
|
30136
31063
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
30137
31064
|
if (isPlainRecord(overrideSandbox)) {
|
|
30138
|
-
const
|
|
31065
|
+
const honorableSandbox = stripSandboxPaths({
|
|
30139
31066
|
sandbox: overrideSandbox,
|
|
31067
|
+
refusals: [
|
|
31068
|
+
CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL,
|
|
31069
|
+
CLAUDECODE_MANAGED_ONLY_SANDBOX_REFUSAL,
|
|
31070
|
+
...global ? [] : [CLAUDECODE_GLOBAL_ONLY_SANDBOX_REFUSAL]
|
|
31071
|
+
],
|
|
30140
31072
|
relativeFilePath: paths.relativeFilePath,
|
|
30141
31073
|
logger
|
|
30142
31074
|
});
|
|
30143
|
-
const scopedSandbox = global ?
|
|
30144
|
-
sandbox:
|
|
30145
|
-
sandbox: executableFreeSandbox,
|
|
30146
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30147
|
-
logger
|
|
30148
|
-
}),
|
|
30149
|
-
relativeFilePath: paths.relativeFilePath,
|
|
30150
|
-
logger
|
|
30151
|
-
});
|
|
30152
|
-
warnOnTrustAffectingSandboxPaths({
|
|
30153
|
-
sandbox: scopedSandbox,
|
|
31075
|
+
const scopedSandbox = global ? honorableSandbox : stripProjectIgnoredMaskEntries({
|
|
31076
|
+
sandbox: honorableSandbox,
|
|
30154
31077
|
relativeFilePath: paths.relativeFilePath,
|
|
30155
31078
|
logger
|
|
30156
31079
|
});
|
|
31080
|
+
trustAffecting.push(...collectTrustAffectingSandboxPaths({ sandbox: scopedSandbox }));
|
|
30157
31081
|
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
30158
31082
|
}
|
|
30159
31083
|
const overrideTopLevel = {};
|
|
@@ -30163,13 +31087,19 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
30163
31087
|
if (value === void 0) continue;
|
|
30164
31088
|
overrideTopLevel[key] = value;
|
|
30165
31089
|
}
|
|
30166
|
-
const scopedTopLevel = stripUnhonoredTopLevelKeys({
|
|
31090
|
+
const { filtered: scopedTopLevel, trustAffecting: trustAffectingTopLevel } = stripUnhonoredTopLevelKeys({
|
|
30167
31091
|
overrides: overrideTopLevel,
|
|
30168
31092
|
global,
|
|
30169
31093
|
relativeFilePath: paths.relativeFilePath,
|
|
30170
31094
|
logger
|
|
30171
31095
|
});
|
|
31096
|
+
trustAffecting.push(...trustAffectingTopLevel);
|
|
30172
31097
|
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
31098
|
+
warnOnTrustAffectingEntries({
|
|
31099
|
+
entries: trustAffecting,
|
|
31100
|
+
relativeFilePath: paths.relativeFilePath,
|
|
31101
|
+
logger
|
|
31102
|
+
});
|
|
30173
31103
|
const managedToolNames = managedClaudeToolNames(config);
|
|
30174
31104
|
const merged = applyPermissions({
|
|
30175
31105
|
settings,
|
|
@@ -35147,11 +36077,8 @@ const TOOL_KEY_TO_CATEGORY = {
|
|
|
35147
36077
|
updateConfluencePage: "edit"
|
|
35148
36078
|
};
|
|
35149
36079
|
const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
|
|
35150
|
-
const OWNED_TOOL_PERMISSION_KEYS = [
|
|
35151
|
-
|
|
35152
|
-
"allowedExternalPaths",
|
|
35153
|
-
"default"
|
|
35154
|
-
];
|
|
36080
|
+
const OWNED_TOOL_PERMISSION_KEYS = ["allowedExternalPaths", "default"];
|
|
36081
|
+
const MANAGED_BASH_KEYS = ["default", "commands"];
|
|
35155
36082
|
/**
|
|
35156
36083
|
* Permissions adapter for Rovo Dev CLI.
|
|
35157
36084
|
*
|
|
@@ -35287,6 +36214,22 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
35287
36214
|
}
|
|
35288
36215
|
};
|
|
35289
36216
|
/**
|
|
36217
|
+
* Report a `toolPermissions.bash.runInSandbox: false` that this generate is
|
|
36218
|
+
* about to carry through.
|
|
36219
|
+
*
|
|
36220
|
+
* The unmanaged `bash` siblings survive untouched, which for this one means a
|
|
36221
|
+
* generate is not the reset a user might read it as: every command the agent
|
|
36222
|
+
* runs stays outside the sandbox. Rulesync never authors the key and will not
|
|
36223
|
+
* start owning it, but a setting that persists on a committed file and loosens
|
|
36224
|
+
* containment should not do so without saying anything — least of all on the
|
|
36225
|
+
* path where the user just tightened `.rulesync/permissions.*`.
|
|
36226
|
+
*/
|
|
36227
|
+
function warnAboutPreservedSandboxOptOut({ existingToolPermissions, filePath, logger }) {
|
|
36228
|
+
const existingBash = existingToolPermissions.bash;
|
|
36229
|
+
if (!isRecord$1(existingBash) || existingBash.runInSandbox !== false) return;
|
|
36230
|
+
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.`);
|
|
36231
|
+
}
|
|
36232
|
+
/**
|
|
35290
36233
|
* Resolve the `toolPermissions` block to write, merging the generated levels
|
|
35291
36234
|
* over the existing file. Every other top-level key of `config.yml` is the
|
|
35292
36235
|
* caller's to preserve; inside this block, keys rulesync manages are owned and
|
|
@@ -35294,6 +36237,11 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
|
|
|
35294
36237
|
*/
|
|
35295
36238
|
function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, filePath, logger }) {
|
|
35296
36239
|
const existingToolPermissions = isRecord$1(existing) ? { ...existing } : {};
|
|
36240
|
+
warnAboutPreservedSandboxOptOut({
|
|
36241
|
+
existingToolPermissions,
|
|
36242
|
+
filePath,
|
|
36243
|
+
logger
|
|
36244
|
+
});
|
|
35297
36245
|
if (Object.keys(generated).length === 0 && sourceStatesRules) {
|
|
35298
36246
|
if (!isRecord$1(existing)) return;
|
|
35299
36247
|
const strippedKeys = stripPermissiveOwnedValues(existingToolPermissions);
|
|
@@ -35302,9 +36250,12 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35302
36250
|
}
|
|
35303
36251
|
const hasExistingToolsRecord = isRecord$1(existingToolPermissions.tools);
|
|
35304
36252
|
const existingTools = hasExistingToolsRecord ? { ...existingToolPermissions.tools } : {};
|
|
36253
|
+
const hasExistingBashRecord = isRecord$1(existingToolPermissions.bash);
|
|
36254
|
+
const existingBash = hasExistingBashRecord ? { ...existingToolPermissions.bash } : {};
|
|
35305
36255
|
warnAboutDroppedOwnedKeys({
|
|
35306
36256
|
existingToolPermissions,
|
|
35307
36257
|
existingTools,
|
|
36258
|
+
existingBash,
|
|
35308
36259
|
generated,
|
|
35309
36260
|
filePath,
|
|
35310
36261
|
logger
|
|
@@ -35314,15 +36265,22 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35314
36265
|
delete existingToolPermissions[toolKey];
|
|
35315
36266
|
delete existingTools[toolKey];
|
|
35316
36267
|
}
|
|
36268
|
+
for (const bashKey of MANAGED_BASH_KEYS) delete existingBash[bashKey];
|
|
35317
36269
|
const tools = {
|
|
35318
36270
|
...existingTools,
|
|
35319
36271
|
...generated.tools
|
|
35320
36272
|
};
|
|
36273
|
+
const bash = {
|
|
36274
|
+
...existingBash,
|
|
36275
|
+
...generated.bash
|
|
36276
|
+
};
|
|
35321
36277
|
if (hasExistingToolsRecord) delete existingToolPermissions.tools;
|
|
36278
|
+
if (hasExistingBashRecord) delete existingToolPermissions.bash;
|
|
35322
36279
|
return {
|
|
35323
36280
|
...existingToolPermissions,
|
|
35324
36281
|
...generated,
|
|
35325
|
-
...Object.keys(tools).length > 0 ? { tools } : {}
|
|
36282
|
+
...Object.keys(tools).length > 0 ? { tools } : {},
|
|
36283
|
+
...Object.keys(bash).length > 0 ? { bash } : {}
|
|
35326
36284
|
};
|
|
35327
36285
|
}
|
|
35328
36286
|
/**
|
|
@@ -35331,9 +36289,14 @@ function resolveToolPermissionsBlock({ existing, generated, sourceStatesRules, f
|
|
|
35331
36289
|
* `/directories` and by an "always allow" answer to a prompt — so their removal
|
|
35332
36290
|
* must not be silent.
|
|
35333
36291
|
*/
|
|
35334
|
-
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, generated, filePath, logger }) {
|
|
36292
|
+
function warnAboutDroppedOwnedKeys({ existingToolPermissions, existingTools, existingBash, generated, filePath, logger }) {
|
|
35335
36293
|
const newTools = generated.tools ?? {};
|
|
35336
|
-
const
|
|
36294
|
+
const newBash = generated.bash ?? {};
|
|
36295
|
+
const droppedKeys = [
|
|
36296
|
+
...OWNED_TOOL_PERMISSION_KEYS.filter((ownedKey) => existingToolPermissions[ownedKey] !== void 0 && generated[ownedKey] === void 0),
|
|
36297
|
+
...MANAGED_TOOL_KEYS.filter((toolKey) => existingTools[toolKey] !== void 0 && newTools[toolKey] === void 0).map((toolKey) => `tools.${toolKey}`),
|
|
36298
|
+
...MANAGED_BASH_KEYS.filter((bashKey) => existingBash[bashKey] !== void 0 && newBash[bashKey] === void 0).map((bashKey) => `bash.${bashKey}`)
|
|
36299
|
+
];
|
|
35337
36300
|
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.`);
|
|
35338
36301
|
}
|
|
35339
36302
|
/**
|
|
@@ -37441,10 +38404,10 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
37441
38404
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
37442
38405
|
toolTarget;
|
|
37443
38406
|
global;
|
|
37444
|
-
constructor({ outputRoot = process.cwd(),
|
|
38407
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
37445
38408
|
super({
|
|
37446
38409
|
outputRoot,
|
|
37447
|
-
|
|
38410
|
+
inputRoots,
|
|
37448
38411
|
dryRun,
|
|
37449
38412
|
logger
|
|
37450
38413
|
});
|
|
@@ -37454,9 +38417,17 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
37454
38417
|
this.global = global;
|
|
37455
38418
|
}
|
|
37456
38419
|
async loadRulesyncFiles() {
|
|
38420
|
+
const relativePaths = getRulesyncSourceCandidates({ paths: RulesyncPermissions.getSettablePaths() }).map((candidate) => candidate.relativeFilePath);
|
|
38421
|
+
const sourceTree = await pickLastRootWithFile({
|
|
38422
|
+
inputRoots: this.inputRoots,
|
|
38423
|
+
relativePaths,
|
|
38424
|
+
logger: this.logger,
|
|
38425
|
+
artifactName: "The permissions file"
|
|
38426
|
+
}) ?? this.inputRoots[0];
|
|
37457
38427
|
try {
|
|
37458
38428
|
return [await RulesyncPermissions.fromFile({
|
|
37459
|
-
outputRoot:
|
|
38429
|
+
outputRoot: (0, node_path.dirname)(sourceTree),
|
|
38430
|
+
relativeDirPath: (0, node_path.basename)(sourceTree),
|
|
37460
38431
|
validate: true
|
|
37461
38432
|
})];
|
|
37462
38433
|
} catch (error) {
|
|
@@ -37880,13 +38851,26 @@ var RovodevSkill = class RovodevSkill extends ToolSkill {
|
|
|
37880
38851
|
//#region src/types/dir-feature-processor.ts
|
|
37881
38852
|
var DirFeatureProcessor = class {
|
|
37882
38853
|
outputRoot;
|
|
37883
|
-
|
|
38854
|
+
/**
|
|
38855
|
+
* Ordered, non-empty list of rulesync source-tree directories. Each entry
|
|
38856
|
+
* is a source tree itself — the directory that directly contains
|
|
38857
|
+
* feature subdirectories (`rules/`, `skills/`, …) and single-file
|
|
38858
|
+
* features (`mcp.jsonc`, `hooks.jsonc`, …). Later entries take precedence
|
|
38859
|
+
* when two trees supply the same relative path. Defaults to
|
|
38860
|
+
* `[join(process.cwd(), ".rulesync")]`.
|
|
38861
|
+
*
|
|
38862
|
+
* The singular user-facing alias (`inputRoot` in `rulesync.jsonc` / the
|
|
38863
|
+
* `--input-root` CLI flag / `GenerateOptions.inputRoot`) is deprecated
|
|
38864
|
+
* and collapsed into `[join(inputRoot, ".rulesync")]` before it ever
|
|
38865
|
+
* reaches a processor.
|
|
38866
|
+
*/
|
|
38867
|
+
inputRoots;
|
|
37884
38868
|
dryRun;
|
|
37885
38869
|
avoidBlockScalars;
|
|
37886
38870
|
logger;
|
|
37887
|
-
constructor({ outputRoot = process.cwd(),
|
|
38871
|
+
constructor({ outputRoot = process.cwd(), inputRoots, dryRun = false, avoidBlockScalars = false, logger }) {
|
|
37888
38872
|
this.outputRoot = outputRoot;
|
|
37889
|
-
this.
|
|
38873
|
+
this.inputRoots = inputRoots !== void 0 && inputRoots.length > 0 ? [inputRoots[0], ...inputRoots.slice(1)] : [(0, node_path.join)(process.cwd(), RULESYNC_RELATIVE_DIR_PATH)];
|
|
37890
38874
|
this.dryRun = dryRun;
|
|
37891
38875
|
this.avoidBlockScalars = avoidBlockScalars;
|
|
37892
38876
|
this.logger = logger;
|
|
@@ -40145,9 +41129,9 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
40145
41129
|
* slug is owned by the commands feature: it must not be imported as a
|
|
40146
41130
|
* skill nor deleted as an orphan skill.
|
|
40147
41131
|
*/
|
|
40148
|
-
static async isDirOwned({ dirName,
|
|
41132
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
40149
41133
|
return !await rulesyncCommandSlugExists({
|
|
40150
|
-
|
|
41134
|
+
inputRoots,
|
|
40151
41135
|
dirName
|
|
40152
41136
|
});
|
|
40153
41137
|
}
|
|
@@ -40197,7 +41181,11 @@ const FactorydroidSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
40197
41181
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
40198
41182
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
40199
41183
|
enabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
40200
|
-
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
41184
|
+
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
41185
|
+
license: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
41186
|
+
compatibility: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
41187
|
+
metadata: zod_mini.z.optional(zod_mini.z.unknown()),
|
|
41188
|
+
version: zod_mini.z.optional(zod_mini.z.unknown())
|
|
40201
41189
|
});
|
|
40202
41190
|
/**
|
|
40203
41191
|
* Represents a Factory Droid skill directory.
|
|
@@ -40250,16 +41238,10 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
40250
41238
|
};
|
|
40251
41239
|
}
|
|
40252
41240
|
toRulesyncSkill() {
|
|
40253
|
-
const
|
|
40254
|
-
const factorydroidBlock = {
|
|
40255
|
-
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
40256
|
-
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
40257
|
-
...frontmatter.enabled !== void 0 && { enabled: frontmatter.enabled },
|
|
40258
|
-
...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
|
|
40259
|
-
};
|
|
41241
|
+
const { name, description, ...factorydroidBlock } = this.getFrontmatter();
|
|
40260
41242
|
const rulesyncFrontmatter = {
|
|
40261
|
-
name
|
|
40262
|
-
description
|
|
41243
|
+
name,
|
|
41244
|
+
description,
|
|
40263
41245
|
targets: ["*"],
|
|
40264
41246
|
...Object.keys(factorydroidBlock).length > 0 && { factorydroid: factorydroidBlock }
|
|
40265
41247
|
};
|
|
@@ -40286,13 +41268,13 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
40286
41268
|
rootFrontmatter: rulesyncFrontmatter,
|
|
40287
41269
|
section: factorydroidSection
|
|
40288
41270
|
});
|
|
41271
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = factorydroidSection ?? {};
|
|
40289
41272
|
const factorydroidFrontmatter = {
|
|
40290
41273
|
name: rulesyncFrontmatter.name,
|
|
40291
41274
|
description: rulesyncFrontmatter.description,
|
|
41275
|
+
...section,
|
|
40292
41276
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
40293
|
-
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
40294
|
-
...factorydroidSection?.enabled !== void 0 && { enabled: factorydroidSection.enabled },
|
|
40295
|
-
...factorydroidSection?.["allowed-tools"] !== void 0 && { "allowed-tools": factorydroidSection["allowed-tools"] }
|
|
41277
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
40296
41278
|
};
|
|
40297
41279
|
return new FactorydroidSkill({
|
|
40298
41280
|
outputRoot,
|
|
@@ -40704,7 +41686,10 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
40704
41686
|
}
|
|
40705
41687
|
}
|
|
40706
41688
|
static getSettablePaths(_options) {
|
|
40707
|
-
return {
|
|
41689
|
+
return {
|
|
41690
|
+
relativeDirPath: JUNIE_SKILLS_DIR_PATH,
|
|
41691
|
+
importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH]
|
|
41692
|
+
};
|
|
40708
41693
|
}
|
|
40709
41694
|
getFrontmatter() {
|
|
40710
41695
|
return JunieSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
@@ -42866,9 +43851,9 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
42866
43851
|
* slug is owned by the commands feature: it must not be imported as a
|
|
42867
43852
|
* skill nor deleted as an orphan skill.
|
|
42868
43853
|
*/
|
|
42869
|
-
static async isDirOwned({ dirName,
|
|
43854
|
+
static async isDirOwned({ dirName, inputRoots }) {
|
|
42870
43855
|
return !await rulesyncCommandSlugExists({
|
|
42871
|
-
|
|
43856
|
+
inputRoots,
|
|
42872
43857
|
dirName
|
|
42873
43858
|
});
|
|
42874
43859
|
}
|
|
@@ -43426,10 +44411,10 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43426
44411
|
toolTarget;
|
|
43427
44412
|
global;
|
|
43428
44413
|
getFactory;
|
|
43429
|
-
constructor({ outputRoot = process.cwd(),
|
|
44414
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
43430
44415
|
super({
|
|
43431
44416
|
outputRoot,
|
|
43432
|
-
|
|
44417
|
+
inputRoots,
|
|
43433
44418
|
dryRun,
|
|
43434
44419
|
avoidBlockScalars: toolTarget === "cursor",
|
|
43435
44420
|
logger
|
|
@@ -43467,39 +44452,67 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43467
44452
|
return rulesyncSkills;
|
|
43468
44453
|
}
|
|
43469
44454
|
/**
|
|
43470
|
-
*
|
|
43471
|
-
*
|
|
43472
|
-
*
|
|
43473
|
-
*
|
|
44455
|
+
* Load rulesync skill directories from a single source-tree's `skills/`
|
|
44456
|
+
* (and `skills/.curated/`) subtree. `sourceTree` is the source tree
|
|
44457
|
+
* itself (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`). Intra-tree:
|
|
44458
|
+
* local skills take precedence over curated skills with the same name.
|
|
43474
44459
|
*/
|
|
43475
|
-
async
|
|
43476
|
-
const
|
|
44460
|
+
async loadRulesyncDirsForRoot(sourceTree) {
|
|
44461
|
+
const treeParent = (0, node_path.dirname)(sourceTree);
|
|
44462
|
+
const treeName = (0, node_path.basename)(sourceTree);
|
|
44463
|
+
const treeSkillsDirPath = (0, node_path.join)(treeName, SKILLS_FEATURE_SUBDIR);
|
|
44464
|
+
const treeCuratedSkillsDirPath = (0, node_path.join)(treeName, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
44465
|
+
const localDirNames = [...await getLocalSkillDirNames(sourceTree)];
|
|
43477
44466
|
const localSkills = await Promise.all(localDirNames.map((dirName) => RulesyncSkill.fromDir({
|
|
43478
|
-
outputRoot:
|
|
44467
|
+
outputRoot: treeParent,
|
|
44468
|
+
relativeDirPath: treeSkillsDirPath,
|
|
43479
44469
|
dirName,
|
|
43480
44470
|
global: this.global
|
|
43481
44471
|
})));
|
|
43482
|
-
const
|
|
43483
|
-
const curatedDirPath = (0, node_path.join)(
|
|
44472
|
+
const localSkillNamesByIdentity = groupSpellingsByCaseFoldedIdentity(localDirNames);
|
|
44473
|
+
const curatedDirPath = (0, node_path.join)(sourceTree, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
43484
44474
|
let curatedSkills = [];
|
|
43485
44475
|
if (await directoryExists(curatedDirPath)) {
|
|
43486
44476
|
const nonConflicting = (await findFilesByGlobs((0, node_path.join)(curatedDirPath, "*"), { type: "dir" })).map((path) => (0, node_path.basename)(path)).filter((name) => {
|
|
43487
|
-
|
|
43488
|
-
|
|
43489
|
-
|
|
43490
|
-
|
|
43491
|
-
|
|
44477
|
+
const spellings = localSkillNamesByIdentity.get(caseFoldIdentity(name));
|
|
44478
|
+
if (spellings === void 0) return true;
|
|
44479
|
+
if (spellings.includes(name)) this.logger.debug(`Skipping curated skill "${name}": local skill takes precedence.`);
|
|
44480
|
+
else this.logger.warn(formatCuratedCaseCollisionWarning({
|
|
44481
|
+
artifactKind: "skill",
|
|
44482
|
+
entryNoun: "skill",
|
|
44483
|
+
treeDirPath: treeSkillsDirPath,
|
|
44484
|
+
curatedSpelling: name,
|
|
44485
|
+
localSpellings: spellings
|
|
44486
|
+
}));
|
|
44487
|
+
return false;
|
|
43492
44488
|
});
|
|
43493
|
-
const curatedRelativeDirPath = RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH;
|
|
43494
44489
|
curatedSkills = await Promise.all(nonConflicting.map((dirName) => RulesyncSkill.fromDir({
|
|
43495
|
-
outputRoot:
|
|
43496
|
-
relativeDirPath:
|
|
44490
|
+
outputRoot: treeParent,
|
|
44491
|
+
relativeDirPath: treeCuratedSkillsDirPath,
|
|
43497
44492
|
dirName,
|
|
43498
44493
|
global: this.global
|
|
43499
44494
|
})));
|
|
43500
44495
|
}
|
|
43501
|
-
|
|
43502
|
-
|
|
44496
|
+
return [...localSkills, ...curatedSkills];
|
|
44497
|
+
}
|
|
44498
|
+
/**
|
|
44499
|
+
* Implementation of abstract method from DirFeatureProcessor.
|
|
44500
|
+
*
|
|
44501
|
+
* Load and parse rulesync skill directories from every configured input
|
|
44502
|
+
* root's `.rulesync/skills/` tree (each root also honours its own
|
|
44503
|
+
* `.curated/` subdirectory). When two roots supply a skill with the same
|
|
44504
|
+
* directory name, the later root's skill replaces the earlier root's copy
|
|
44505
|
+
* atomically (companion files included) — an overlay always ships a whole
|
|
44506
|
+
* skill directory, never a partial patch.
|
|
44507
|
+
*/
|
|
44508
|
+
async loadRulesyncDirs() {
|
|
44509
|
+
const allSkills = mergeByCaseInsensitiveIdentity({
|
|
44510
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncDirsForRoot(root))),
|
|
44511
|
+
identity: (skill) => skill.getDirName(),
|
|
44512
|
+
artifactName: "skill",
|
|
44513
|
+
logger: this.logger
|
|
44514
|
+
});
|
|
44515
|
+
this.logger.debug(`Successfully loaded ${allSkills.length} rulesync skills`);
|
|
43503
44516
|
return allSkills;
|
|
43504
44517
|
}
|
|
43505
44518
|
/**
|
|
@@ -43515,7 +44528,17 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43515
44528
|
}) : [];
|
|
43516
44529
|
const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
|
|
43517
44530
|
const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
|
|
43518
|
-
const
|
|
44531
|
+
const claimedSkillNames = new ClaimedIdentities();
|
|
44532
|
+
const claimSkillName = ({ skill, relativeDirPath, sourcePath }) => {
|
|
44533
|
+
const skillName = skill.getImportIdentity();
|
|
44534
|
+
const claimed = claimedSkillNames.claim({
|
|
44535
|
+
identity: skillName,
|
|
44536
|
+
source: relativeDirPath
|
|
44537
|
+
});
|
|
44538
|
+
if (claimed === null) return true;
|
|
44539
|
+
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.`);
|
|
44540
|
+
return false;
|
|
44541
|
+
};
|
|
43519
44542
|
const toolSkills = [];
|
|
43520
44543
|
for (const root of roots) {
|
|
43521
44544
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
@@ -43531,54 +44554,60 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43531
44554
|
outputRoot: rootOutputRoot,
|
|
43532
44555
|
relativeDirPath,
|
|
43533
44556
|
dirName,
|
|
43534
|
-
|
|
44557
|
+
inputRoots: this.inputRoots
|
|
43535
44558
|
})) continue;
|
|
43536
44559
|
ownedDirNames.push(dirName);
|
|
43537
44560
|
}
|
|
43538
44561
|
const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
|
|
44562
|
+
const sourcePath = (0, node_path.join)(relativeDirPath, dirName);
|
|
43539
44563
|
try {
|
|
43540
|
-
return
|
|
43541
|
-
|
|
43542
|
-
|
|
43543
|
-
|
|
43544
|
-
|
|
43545
|
-
|
|
44564
|
+
return {
|
|
44565
|
+
skill: await factory.class.fromDir({
|
|
44566
|
+
outputRoot: rootOutputRoot,
|
|
44567
|
+
relativeDirPath,
|
|
44568
|
+
dirName,
|
|
44569
|
+
global: this.global
|
|
44570
|
+
}),
|
|
44571
|
+
sourcePath
|
|
44572
|
+
};
|
|
43546
44573
|
} catch (error) {
|
|
43547
44574
|
if (!isLenientRoot) throw error;
|
|
43548
|
-
this.logger.warn(`Skipping ${
|
|
44575
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43549
44576
|
return null;
|
|
43550
44577
|
}
|
|
43551
|
-
}))).filter((
|
|
43552
|
-
for (const skill of directorySkills) {
|
|
43553
|
-
|
|
43554
|
-
|
|
43555
|
-
|
|
43556
|
-
|
|
43557
|
-
}
|
|
44578
|
+
}))).filter((loaded) => loaded !== null);
|
|
44579
|
+
for (const { skill, sourcePath } of directorySkills) if (claimSkillName({
|
|
44580
|
+
skill,
|
|
44581
|
+
relativeDirPath,
|
|
44582
|
+
sourcePath
|
|
44583
|
+
})) toolSkills.push(skill);
|
|
43558
44584
|
if (!factory.class.fromFlatFile) continue;
|
|
43559
44585
|
const fromFlatFile = factory.class.fromFlatFile;
|
|
43560
44586
|
const directoryStems = new Set(ownedDirNames);
|
|
43561
44587
|
const flatFilePaths = (await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has((0, node_path.basename)(filePath, ".md")));
|
|
43562
44588
|
const flatSkills = (await Promise.all(flatFilePaths.map(async (filePath) => {
|
|
44589
|
+
const sourcePath = (0, node_path.join)(relativeDirPath, (0, node_path.basename)(filePath));
|
|
43563
44590
|
try {
|
|
43564
|
-
return
|
|
43565
|
-
|
|
43566
|
-
|
|
43567
|
-
|
|
43568
|
-
|
|
43569
|
-
|
|
44591
|
+
return {
|
|
44592
|
+
skill: await fromFlatFile({
|
|
44593
|
+
outputRoot: rootOutputRoot,
|
|
44594
|
+
relativeDirPath,
|
|
44595
|
+
relativeFilePath: (0, node_path.basename)(filePath),
|
|
44596
|
+
global: this.global
|
|
44597
|
+
}),
|
|
44598
|
+
sourcePath
|
|
44599
|
+
};
|
|
43570
44600
|
} catch (error) {
|
|
43571
44601
|
if (!isLenientRoot) throw error;
|
|
43572
|
-
this.logger.warn(`Skipping ${
|
|
44602
|
+
this.logger.warn(`Skipping ${sourcePath}: ${formatError(error)}`);
|
|
43573
44603
|
return null;
|
|
43574
44604
|
}
|
|
43575
|
-
}))).filter((
|
|
43576
|
-
for (const skill of flatSkills) {
|
|
43577
|
-
|
|
43578
|
-
|
|
43579
|
-
|
|
43580
|
-
|
|
43581
|
-
}
|
|
44605
|
+
}))).filter((loaded) => loaded !== null);
|
|
44606
|
+
for (const { skill, sourcePath } of flatSkills) if (claimSkillName({
|
|
44607
|
+
skill,
|
|
44608
|
+
relativeDirPath,
|
|
44609
|
+
sourcePath
|
|
44610
|
+
})) toolSkills.push(skill);
|
|
43582
44611
|
}
|
|
43583
44612
|
this.logger.debug(`Successfully loaded ${toolSkills.length} skills from ${roots.length} root(s)`);
|
|
43584
44613
|
return toolSkills;
|
|
@@ -43608,7 +44637,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
43608
44637
|
outputRoot: this.outputRoot,
|
|
43609
44638
|
relativeDirPath: root,
|
|
43610
44639
|
dirName,
|
|
43611
|
-
|
|
44640
|
+
inputRoots: this.inputRoots
|
|
43612
44641
|
})) continue;
|
|
43613
44642
|
const toolSkill = factory.class.forDeletion({
|
|
43614
44643
|
outputRoot: this.outputRoot,
|
|
@@ -46366,6 +47395,7 @@ const JunieSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
46366
47395
|
disallowedTools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
46367
47396
|
mcpServers: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
46368
47397
|
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
47398
|
+
permissionMode: zod_mini.z.optional(zod_mini.z.string()),
|
|
46369
47399
|
reasoningLevel: zod_mini.z.optional(zod_mini.z.string()),
|
|
46370
47400
|
maxTurns: zod_mini.z.optional(zod_mini.z.number()),
|
|
46371
47401
|
skills: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
@@ -48346,14 +49376,25 @@ const subagentsProcessorToolTargetsSimulated = allToolTargetKeys$1.filter((targe
|
|
|
48346
49376
|
const subagentsProcessorToolTargetsGlobal = allToolTargetKeys$1.filter((target) => {
|
|
48347
49377
|
return toolSubagentFactories.get(target)?.meta.supportsGlobal ?? false;
|
|
48348
49378
|
});
|
|
49379
|
+
/**
|
|
49380
|
+
* Stands in for a discovery root when a subagent came from a tool's own config
|
|
49381
|
+
* file rather than a directory (see `loadAdditionalImportFiles`). The angle
|
|
49382
|
+
* brackets keep it from ever matching a real relative directory path.
|
|
49383
|
+
*/
|
|
49384
|
+
const INLINE_SOURCE = "<inline>";
|
|
49385
|
+
/**
|
|
49386
|
+
* The single "root" of the post-conversion output guard, which de-duplicates
|
|
49387
|
+
* `.rulesync/subagents/` paths rather than discovery roots.
|
|
49388
|
+
*/
|
|
49389
|
+
const OUTPUT_SOURCE = "<output>";
|
|
48349
49390
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
48350
49391
|
toolTarget;
|
|
48351
49392
|
global;
|
|
48352
49393
|
getFactory;
|
|
48353
|
-
constructor({ outputRoot = process.cwd(),
|
|
49394
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
48354
49395
|
super({
|
|
48355
49396
|
outputRoot,
|
|
48356
|
-
|
|
49397
|
+
inputRoots,
|
|
48357
49398
|
dryRun,
|
|
48358
49399
|
logger
|
|
48359
49400
|
});
|
|
@@ -48399,24 +49440,31 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48399
49440
|
rulesyncSubagents.push(toolSubagent.toRulesyncSubagent());
|
|
48400
49441
|
}
|
|
48401
49442
|
const uniqueRulesyncSubagents = [];
|
|
48402
|
-
const
|
|
49443
|
+
const claimedOutputPaths = new ClaimedIdentities();
|
|
48403
49444
|
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
48404
49445
|
const outputPath = (0, node_path.join)(rulesyncSubagent.getRelativeDirPath(), rulesyncSubagent.getRelativeFilePath());
|
|
48405
|
-
|
|
48406
|
-
|
|
49446
|
+
const claimed = claimedOutputPaths.claim({
|
|
49447
|
+
identity: outputPath,
|
|
49448
|
+
source: OUTPUT_SOURCE
|
|
49449
|
+
});
|
|
49450
|
+
if (claimed !== null) {
|
|
49451
|
+
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.`);
|
|
48407
49452
|
continue;
|
|
48408
49453
|
}
|
|
48409
|
-
seenOutputPaths.add(outputPath);
|
|
48410
49454
|
uniqueRulesyncSubagents.push(rulesyncSubagent);
|
|
48411
49455
|
}
|
|
48412
49456
|
return uniqueRulesyncSubagents;
|
|
48413
49457
|
}
|
|
48414
49458
|
/**
|
|
48415
|
-
*
|
|
48416
|
-
*
|
|
49459
|
+
* Load subagent files from a single source-tree's `subagents/` subtree.
|
|
49460
|
+
* `sourceTree` is the source tree itself (e.g. `/repo/.rulesync` or
|
|
49461
|
+
* `/repo/.rulesync.local`).
|
|
48417
49462
|
*/
|
|
48418
|
-
async
|
|
48419
|
-
const
|
|
49463
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
49464
|
+
const treeParent = (0, node_path.dirname)(sourceTree);
|
|
49465
|
+
const treeName = (0, node_path.basename)(sourceTree);
|
|
49466
|
+
const treeSubagentsDirPath = (0, node_path.join)(treeName, SUBAGENTS_FEATURE_SUBDIR);
|
|
49467
|
+
const subagentsDir = (0, node_path.join)(sourceTree, SUBAGENTS_FEATURE_SUBDIR);
|
|
48420
49468
|
if (!await directoryExists(subagentsDir)) {
|
|
48421
49469
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
48422
49470
|
return [];
|
|
@@ -48432,7 +49480,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48432
49480
|
const filepath = (0, node_path.join)(subagentsDir, mdFile);
|
|
48433
49481
|
try {
|
|
48434
49482
|
const rulesyncSubagent = await RulesyncSubagent.fromFile({
|
|
48435
|
-
outputRoot:
|
|
49483
|
+
outputRoot: treeParent,
|
|
49484
|
+
relativeDirPath: treeSubagentsDirPath,
|
|
48436
49485
|
relativeFilePath: mdFile,
|
|
48437
49486
|
validate: true
|
|
48438
49487
|
});
|
|
@@ -48443,8 +49492,24 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48443
49492
|
continue;
|
|
48444
49493
|
}
|
|
48445
49494
|
}
|
|
49495
|
+
return rulesyncSubagents;
|
|
49496
|
+
}
|
|
49497
|
+
/**
|
|
49498
|
+
* Implementation of abstract method from Processor
|
|
49499
|
+
* Load and parse rulesync subagent files from every configured input root's
|
|
49500
|
+
* `.rulesync/subagents/` directory, merging by relative file path so a
|
|
49501
|
+
* subagent with the same target path from a later root replaces the
|
|
49502
|
+
* earlier root's copy.
|
|
49503
|
+
*/
|
|
49504
|
+
async loadRulesyncFiles() {
|
|
49505
|
+
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
49506
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
49507
|
+
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
49508
|
+
artifactName: "subagent",
|
|
49509
|
+
logger: this.logger
|
|
49510
|
+
});
|
|
48446
49511
|
if (rulesyncSubagents.length === 0) {
|
|
48447
|
-
this.logger.debug(`No valid subagents found
|
|
49512
|
+
this.logger.debug(`No valid subagents found`);
|
|
48448
49513
|
return [];
|
|
48449
49514
|
}
|
|
48450
49515
|
this.logger.debug(`Successfully loaded ${rulesyncSubagents.length} rulesync subagents`);
|
|
@@ -48459,7 +49524,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48459
49524
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
48460
49525
|
const roots = forDeletion ? [paths.relativeDirPath] : [paths.relativeDirPath, ...paths.importDirPaths ?? []];
|
|
48461
49526
|
const toolSubagents = [];
|
|
48462
|
-
const
|
|
49527
|
+
const claimedRelativeFilePaths = new ClaimedIdentities();
|
|
48463
49528
|
for (const root of roots) {
|
|
48464
49529
|
const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
|
|
48465
49530
|
const dirPath = typeof root === "string" ? root : root.relativeDirPath;
|
|
@@ -48498,37 +49563,77 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
48498
49563
|
relativeFilePath: toRelativeFilePath(path),
|
|
48499
49564
|
global: this.global
|
|
48500
49565
|
})));
|
|
48501
|
-
|
|
48502
|
-
|
|
48503
|
-
|
|
48504
|
-
|
|
48505
|
-
|
|
48506
|
-
continue;
|
|
48507
|
-
}
|
|
48508
|
-
seenRelativeFilePaths.add(key);
|
|
48509
|
-
deduped.push(subagent);
|
|
48510
|
-
}
|
|
48511
|
-
toolSubagents.push(...deduped);
|
|
49566
|
+
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
49567
|
+
loaded,
|
|
49568
|
+
dirPath,
|
|
49569
|
+
claimedRelativeFilePaths
|
|
49570
|
+
}));
|
|
48512
49571
|
}
|
|
48513
49572
|
if (!forDeletion && factory.class.loadAdditionalImportFiles) {
|
|
48514
49573
|
const additionalSubagents = await factory.class.loadAdditionalImportFiles({
|
|
48515
49574
|
outputRoot: this.outputRoot,
|
|
48516
49575
|
global: this.global
|
|
48517
49576
|
});
|
|
48518
|
-
|
|
48519
|
-
|
|
48520
|
-
|
|
48521
|
-
|
|
48522
|
-
continue;
|
|
48523
|
-
}
|
|
48524
|
-
seenRelativeFilePaths.add(key);
|
|
48525
|
-
toolSubagents.push(subagent);
|
|
48526
|
-
}
|
|
49577
|
+
toolSubagents.push(...this.claimInlineSubagents({
|
|
49578
|
+
additionalSubagents,
|
|
49579
|
+
claimedRelativeFilePaths
|
|
49580
|
+
}));
|
|
48527
49581
|
}
|
|
48528
49582
|
this.logger.debug(`Successfully loaded ${toolSubagents.length} ${this.toolTarget} subagents from ${roots.length} root(s)`);
|
|
48529
49583
|
return toolSubagents;
|
|
48530
49584
|
}
|
|
48531
49585
|
/**
|
|
49586
|
+
* Keeps the subagents from one discovery root whose import identity is still
|
|
49587
|
+
* unclaimed, warning about each copy that loses. Split out of
|
|
49588
|
+
* `loadToolFiles` so the two de-duplication passes stay readable side by
|
|
49589
|
+
* side (and so that method stays within the linter's complexity budget).
|
|
49590
|
+
*
|
|
49591
|
+
* When more than one discovery root is scanned (e.g. Junie's `.junie/agents/`
|
|
49592
|
+
* plus `.agents/`), two roots can hold a subagent with the same relative
|
|
49593
|
+
* path. Downstream conversion keys by that path, so a later one would
|
|
49594
|
+
* silently overwrite an earlier one. Warn instead of failing, keeping the
|
|
49595
|
+
* earlier (higher-precedence) root's file.
|
|
49596
|
+
*/
|
|
49597
|
+
claimStandaloneSubagents({ loaded, dirPath, claimedRelativeFilePaths }) {
|
|
49598
|
+
const deduped = [];
|
|
49599
|
+
for (const subagent of loaded) {
|
|
49600
|
+
const key = subagent.getImportIdentity();
|
|
49601
|
+
const claimed = claimedRelativeFilePaths.claim({
|
|
49602
|
+
identity: key,
|
|
49603
|
+
source: dirPath
|
|
49604
|
+
});
|
|
49605
|
+
if (claimed === null) {
|
|
49606
|
+
deduped.push(subagent);
|
|
49607
|
+
continue;
|
|
49608
|
+
}
|
|
49609
|
+
const keptFrom = claimed.source === dirPath ? `the earlier one in ${dirPath}` : `the one from the higher-precedence ${claimed.source}`;
|
|
49610
|
+
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.`);
|
|
49611
|
+
}
|
|
49612
|
+
return deduped;
|
|
49613
|
+
}
|
|
49614
|
+
/**
|
|
49615
|
+
* The same claim-or-warn pass for subagents defined inline in a tool's own
|
|
49616
|
+
* config file (see `loadAdditionalImportFiles`), which are scanned after
|
|
49617
|
+
* every standalone file so a Markdown file of the same name wins.
|
|
49618
|
+
*/
|
|
49619
|
+
claimInlineSubagents({ additionalSubagents, claimedRelativeFilePaths }) {
|
|
49620
|
+
const deduped = [];
|
|
49621
|
+
for (const subagent of additionalSubagents) {
|
|
49622
|
+
const key = subagent.getImportIdentity();
|
|
49623
|
+
const claimed = claimedRelativeFilePaths.claim({
|
|
49624
|
+
identity: key,
|
|
49625
|
+
source: INLINE_SOURCE
|
|
49626
|
+
});
|
|
49627
|
+
if (claimed === null) {
|
|
49628
|
+
deduped.push(subagent);
|
|
49629
|
+
continue;
|
|
49630
|
+
}
|
|
49631
|
+
const kept = claimed.source === INLINE_SOURCE ? "the earlier inline definition" : `the standalone file in ${claimed.source}`;
|
|
49632
|
+
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.`);
|
|
49633
|
+
}
|
|
49634
|
+
return deduped;
|
|
49635
|
+
}
|
|
49636
|
+
/**
|
|
48532
49637
|
* Implementation of abstract method from FeatureProcessor
|
|
48533
49638
|
* Return the tool targets that this processor supports
|
|
48534
49639
|
*/
|
|
@@ -51458,17 +52563,32 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
|
|
|
51458
52563
|
* Rule generator for JetBrains Junie AI coding agent
|
|
51459
52564
|
*
|
|
51460
52565
|
* Generates `.junie/AGENTS.md` files based on rulesync rule content. Junie CLI
|
|
51461
|
-
* resolves project guidelines
|
|
51462
|
-
* `AGENTS.md`
|
|
51463
|
-
*
|
|
51464
|
-
*
|
|
51465
|
-
*
|
|
51466
|
-
*
|
|
51467
|
-
*
|
|
52566
|
+
* resolves project guidelines in this order: `.junie/AGENTS.md` → root
|
|
52567
|
+
* `AGENTS.md` **combined with `.junie/playbook.md` and every
|
|
52568
|
+
* `.junie/rules/*.md`** → legacy `.junie/guidelines.md` / `.junie/guidelines/`.
|
|
52569
|
+
* The multi-file branch exists, but it is unreachable while `.junie/AGENTS.md`
|
|
52570
|
+
* is present: that file "is used exclusively and no other guidelines files are
|
|
52571
|
+
* combined with it". So emitting `.junie/rules/*.md` next to the root file
|
|
52572
|
+
* rulesync writes would produce files Junie never reads, and moving the root
|
|
52573
|
+
* output to project-root `AGENTS.md` would both change every existing output
|
|
52574
|
+
* path and collide with the `agentsmd` target. Non-root rules therefore stay
|
|
52575
|
+
* folded into the single root `.junie/AGENTS.md` by the RulesProcessor
|
|
52576
|
+
* (`nonRoot` is `undefined`, mirroring the warp / deepagents targets) — the
|
|
52577
|
+
* fold is lossless, since Junie loads that one file in full. The original
|
|
52578
|
+
* rationale for this shape was recorded in issue #2211 and re-confirmed
|
|
52579
|
+
* against the 2026-08-21 docs revision in issue #2728. The legacy
|
|
51468
52580
|
* `.junie/guidelines.md` is still accepted as an import fallback, but
|
|
51469
52581
|
* generation always targets `.junie/AGENTS.md`. Junie uses plain markdown
|
|
51470
52582
|
* without frontmatter requirements.
|
|
51471
52583
|
*
|
|
52584
|
+
* The multi-file branch is still read on **import**, though: a repo that
|
|
52585
|
+
* authors `.junie/rules/*.md` or `.junie/playbook.md` by hand — the live
|
|
52586
|
+
* layout whenever `.junie/AGENTS.md` is absent, which is exactly the state a
|
|
52587
|
+
* first `rulesync import` finds — declares those paths as `importOnlyRoots`
|
|
52588
|
+
* and imports them as non-root rules. They are never written back there:
|
|
52589
|
+
* generating `.junie/AGENTS.md` moves Junie onto the first branch, where the
|
|
52590
|
+
* folded root file carries the same content.
|
|
52591
|
+
*
|
|
51472
52592
|
* Global (user) scope writes a single `~/.junie/AGENTS.md` file. Junie merges
|
|
51473
52593
|
* these user-scope guidelines with the project guidelines (both are included
|
|
51474
52594
|
* and marked clearly).
|
|
@@ -51490,6 +52610,14 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51490
52610
|
alternativeRoots: [{
|
|
51491
52611
|
relativeDirPath: buildToolPath(JUNIE_DIR, ".", excludeToolDir),
|
|
51492
52612
|
relativeFilePath: JUNIE_LEGACY_RULE_FILE_NAME
|
|
52613
|
+
}],
|
|
52614
|
+
importOnlyRoots: [{
|
|
52615
|
+
relativeDirPath: buildToolPath(JUNIE_DIR, JUNIE_RULES_DIR_NAME, excludeToolDir),
|
|
52616
|
+
onlyWhenRootAbsent: true
|
|
52617
|
+
}, {
|
|
52618
|
+
relativeDirPath: buildToolPath(JUNIE_DIR, ".", excludeToolDir),
|
|
52619
|
+
relativeFilePath: JUNIE_PLAYBOOK_FILE_NAME,
|
|
52620
|
+
onlyWhenRootAbsent: true
|
|
51493
52621
|
}]
|
|
51494
52622
|
};
|
|
51495
52623
|
}
|
|
@@ -51500,7 +52628,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51500
52628
|
static isRootRelativeFilePath(relativeFilePath) {
|
|
51501
52629
|
return relativeFilePath === "AGENTS.md" || relativeFilePath === "guidelines.md";
|
|
51502
52630
|
}
|
|
51503
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
52631
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath: relativeDirPathParam, relativeFilePath, validate = true, global = false }) {
|
|
51504
52632
|
if (global) {
|
|
51505
52633
|
const paths = this.getSettablePaths({ global: true });
|
|
51506
52634
|
if (!("root" in paths) || !paths.root) throw new Error("JunieRule global settable paths must include a root path");
|
|
@@ -51514,7 +52642,8 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51514
52642
|
root: true
|
|
51515
52643
|
});
|
|
51516
52644
|
}
|
|
51517
|
-
const
|
|
52645
|
+
const settablePaths = this.getSettablePaths();
|
|
52646
|
+
const relativeDirPath = relativeDirPathParam ?? settablePaths.root.relativeDirPath;
|
|
51518
52647
|
const relativePath = (0, node_path.join)(relativeDirPath, relativeFilePath);
|
|
51519
52648
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
51520
52649
|
return new JunieRule({
|
|
@@ -51523,7 +52652,7 @@ var JunieRule = class JunieRule extends ToolRule {
|
|
|
51523
52652
|
relativeFilePath,
|
|
51524
52653
|
fileContent,
|
|
51525
52654
|
validate,
|
|
51526
|
-
root: JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
52655
|
+
root: relativeDirPath === settablePaths.root.relativeDirPath && JunieRule.isRootRelativeFilePath(relativeFilePath)
|
|
51527
52656
|
});
|
|
51528
52657
|
}
|
|
51529
52658
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -53875,10 +55004,23 @@ const defaultGetFactory = (target) => {
|
|
|
53875
55004
|
if (!factory) throw new Error(`Unsupported tool target: ${target}`);
|
|
53876
55005
|
return factory;
|
|
53877
55006
|
};
|
|
53878
|
-
|
|
53879
|
-
|
|
55007
|
+
/**
|
|
55008
|
+
* How many skipped import-only paths a single warning names before it
|
|
55009
|
+
* summarizes the rest. Keeps one line readable when a rules directory holds
|
|
55010
|
+
* dozens of files.
|
|
55011
|
+
*/
|
|
55012
|
+
const MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS = 10;
|
|
55013
|
+
/**
|
|
55014
|
+
* Fall back to a tool's legacy roots when its primary root file is absent.
|
|
55015
|
+
*
|
|
55016
|
+
* The primary hits are passed in rather than globbed here, so that callers
|
|
55017
|
+
* which need "the root file Rulesync generates" — rather than "whatever root
|
|
55018
|
+
* the tool will read" — can keep the two apart. A legacy root is a file
|
|
55019
|
+
* Rulesync reads but never writes, and the difference matters to them.
|
|
55020
|
+
*/
|
|
55021
|
+
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob) => {
|
|
53880
55022
|
if (primaryFilePaths.length > 0) return primaryFilePaths;
|
|
53881
|
-
if (alternativeRoots) return findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
55023
|
+
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
53882
55024
|
return [];
|
|
53883
55025
|
};
|
|
53884
55026
|
var RulesProcessor = class extends FeatureProcessor {
|
|
@@ -53890,10 +55032,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
53890
55032
|
getFactory;
|
|
53891
55033
|
skills;
|
|
53892
55034
|
featureOptions;
|
|
53893
|
-
constructor({ outputRoot = process.cwd(),
|
|
55035
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
53894
55036
|
super({
|
|
53895
55037
|
outputRoot,
|
|
53896
|
-
|
|
55038
|
+
inputRoots,
|
|
53897
55039
|
dryRun,
|
|
53898
55040
|
logger
|
|
53899
55041
|
});
|
|
@@ -54268,25 +55410,45 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54268
55410
|
claimedBy.set(target.toLowerCase(), source);
|
|
54269
55411
|
continue;
|
|
54270
55412
|
}
|
|
54271
|
-
this.logger.warn(`Both ${previous} and ${source} import to ${(0, node_path.join)(RULESYNC_RULES_RELATIVE_DIR_PATH, target)} (compared case-insensitively, as on macOS and Windows); the last one wins wherever they collide.`);
|
|
55413
|
+
this.logger.warn(`Both ${stripControlCharacters(previous)} and ${stripControlCharacters(source)} import to ${stripControlCharacters((0, node_path.join)(RULESYNC_RULES_RELATIVE_DIR_PATH, target))} (compared case-insensitively, as on macOS and Windows); the last one wins wherever they collide.`);
|
|
54272
55414
|
}
|
|
54273
55415
|
return rulesyncRules;
|
|
54274
55416
|
}
|
|
54275
55417
|
/**
|
|
54276
|
-
*
|
|
54277
|
-
*
|
|
55418
|
+
* Load rulesync rule files from a single source-tree's `rules/` (and
|
|
55419
|
+
* `rules/.curated/`) subtree. `sourceTree` is the source tree itself
|
|
55420
|
+
* (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`), NOT its parent.
|
|
55421
|
+
*
|
|
55422
|
+
* Intra-tree behavior — the local-wins-over-curated rule and the
|
|
55423
|
+
* case-insensitive collision handling — is preserved from the previous
|
|
55424
|
+
* single-root implementation. See `loadRulesyncFiles` for how the
|
|
55425
|
+
* per-root results are combined into the effective set.
|
|
54278
55426
|
*/
|
|
54279
|
-
async
|
|
54280
|
-
const
|
|
54281
|
-
const
|
|
55427
|
+
async loadRulesyncFilesForRoot(sourceTree) {
|
|
55428
|
+
const treeParent = (0, node_path.dirname)(sourceTree);
|
|
55429
|
+
const treeName = (0, node_path.basename)(sourceTree);
|
|
55430
|
+
const treeRulesDirPath = (0, node_path.join)(treeName, RULES_FEATURE_SUBDIR);
|
|
55431
|
+
const rulesyncOutputRoot = (0, node_path.join)(sourceTree, RULES_FEATURE_SUBDIR);
|
|
55432
|
+
const curatedOutputRoot = (0, node_path.join)(sourceTree, CURATED_RULES_FEATURE_SUBDIR);
|
|
54282
55433
|
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([findFilesByGlobs((0, node_path.join)(rulesyncOutputRoot, "**", "*.md")), findFilesByGlobs((0, node_path.join)(curatedOutputRoot, "**", "*.md"))]);
|
|
54283
55434
|
const files = [.../* @__PURE__ */ new Set([...discoveredFiles, ...discoveredCuratedFiles])];
|
|
54284
55435
|
const localFiles = files.filter((file) => !(0, node_path.relative)(rulesyncOutputRoot, file).startsWith(`.curated${node_path.sep}`));
|
|
54285
|
-
const
|
|
55436
|
+
const localRelativePathsByIdentity = groupSpellingsByCaseFoldedIdentity(localFiles.map((file) => (0, node_path.relative)(rulesyncOutputRoot, file)));
|
|
54286
55437
|
const curatedFiles = files.filter((file) => (0, node_path.relative)(rulesyncOutputRoot, file).startsWith(`.curated${node_path.sep}`)).map((file) => ({
|
|
54287
55438
|
file,
|
|
54288
55439
|
relativeFilePath: (0, node_path.relative)(curatedOutputRoot, file)
|
|
54289
|
-
})).filter(({ relativeFilePath }) =>
|
|
55440
|
+
})).filter(({ relativeFilePath }) => {
|
|
55441
|
+
const spellings = localRelativePathsByIdentity.get(caseFoldIdentity(relativeFilePath));
|
|
55442
|
+
if (spellings === void 0) return true;
|
|
55443
|
+
if (!spellings.includes(relativeFilePath)) this.logger.warn(formatCuratedCaseCollisionWarning({
|
|
55444
|
+
artifactKind: "rule",
|
|
55445
|
+
entryNoun: "file",
|
|
55446
|
+
treeDirPath: treeRulesDirPath,
|
|
55447
|
+
curatedSpelling: (0, node_path.join)(".curated", relativeFilePath),
|
|
55448
|
+
localSpellings: spellings
|
|
55449
|
+
}));
|
|
55450
|
+
return false;
|
|
55451
|
+
});
|
|
54290
55452
|
const selectedFiles = [...localFiles.map((file) => ({
|
|
54291
55453
|
file,
|
|
54292
55454
|
sourceRelativeFilePath: (0, node_path.relative)(rulesyncOutputRoot, file),
|
|
@@ -54296,25 +55458,41 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54296
55458
|
sourceRelativeFilePath: (0, node_path.join)(".curated", relativeFilePath),
|
|
54297
55459
|
relativeFilePath
|
|
54298
55460
|
}))];
|
|
54299
|
-
this.logger.debug(`Found ${selectedFiles.length} rulesync files`);
|
|
54300
|
-
|
|
55461
|
+
this.logger.debug(`Found ${selectedFiles.length} rulesync files under ${rulesyncOutputRoot}`);
|
|
55462
|
+
return await Promise.all(selectedFiles.map(async ({ sourceRelativeFilePath, relativeFilePath }) => {
|
|
54301
55463
|
checkPathTraversal({
|
|
54302
55464
|
relativePath: sourceRelativeFilePath,
|
|
54303
55465
|
intendedRootDir: rulesyncOutputRoot
|
|
54304
55466
|
});
|
|
54305
55467
|
const rule = await RulesyncRule.fromFile({
|
|
54306
|
-
outputRoot:
|
|
55468
|
+
outputRoot: treeParent,
|
|
55469
|
+
relativeDirPath: treeRulesDirPath,
|
|
54307
55470
|
relativeFilePath: sourceRelativeFilePath
|
|
54308
55471
|
});
|
|
54309
55472
|
if (sourceRelativeFilePath === relativeFilePath) return rule;
|
|
54310
55473
|
return new RulesyncRule({
|
|
54311
|
-
outputRoot:
|
|
54312
|
-
relativeDirPath:
|
|
55474
|
+
outputRoot: treeParent,
|
|
55475
|
+
relativeDirPath: treeRulesDirPath,
|
|
54313
55476
|
relativeFilePath,
|
|
54314
55477
|
frontmatter: rule.getFrontmatter(),
|
|
54315
55478
|
body: rule.getBody()
|
|
54316
55479
|
});
|
|
54317
55480
|
}));
|
|
55481
|
+
}
|
|
55482
|
+
/**
|
|
55483
|
+
* Implementation of abstract method from FeatureProcessor
|
|
55484
|
+
* Load and parse rulesync rule files from every configured input root's
|
|
55485
|
+
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
55486
|
+
* with the same target path from a later root replaces the earlier root's
|
|
55487
|
+
* copy (case-insensitive, matching the intra-root collision handling).
|
|
55488
|
+
*/
|
|
55489
|
+
async loadRulesyncFiles() {
|
|
55490
|
+
const rulesyncRules = mergeByCaseInsensitiveIdentity({
|
|
55491
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
55492
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
55493
|
+
artifactName: "rule",
|
|
55494
|
+
logger: this.logger
|
|
55495
|
+
});
|
|
54318
55496
|
const factory = this.getFactory(this.toolTarget);
|
|
54319
55497
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
54320
55498
|
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}.`);
|
|
@@ -54408,23 +55586,42 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54408
55586
|
});
|
|
54409
55587
|
}).filter((rule) => rule.isDeletable());
|
|
54410
55588
|
};
|
|
55589
|
+
/**
|
|
55590
|
+
* Import counterpart of {@link buildDeletionRulesFromPaths} for the
|
|
55591
|
+
* root-shaped scans (root, legacy roots, and read-only roots), whose
|
|
55592
|
+
* paths all sit at a directory Rulesync resolves from the file itself.
|
|
55593
|
+
*/
|
|
55594
|
+
const buildImportRulesFromPaths = (filePaths) => Promise.all(filePaths.map((filePath) => {
|
|
55595
|
+
const relativeDirPath = resolveRelativeDirPath(filePath);
|
|
55596
|
+
checkPathTraversal({
|
|
55597
|
+
relativePath: relativeDirPath,
|
|
55598
|
+
intendedRootDir: this.outputRoot
|
|
55599
|
+
});
|
|
55600
|
+
return factory.class.fromFile({
|
|
55601
|
+
outputRoot: this.outputRoot,
|
|
55602
|
+
relativeDirPath,
|
|
55603
|
+
relativeFilePath: (0, node_path.basename)(filePath),
|
|
55604
|
+
global: this.global
|
|
55605
|
+
});
|
|
55606
|
+
}));
|
|
55607
|
+
/**
|
|
55608
|
+
* The tool's own root file, as opposed to whichever root
|
|
55609
|
+
* `rootToolRules` ends up resolving. A legacy root reached through
|
|
55610
|
+
* `alternativeRoots` is deliberately not counted here: it is a
|
|
55611
|
+
* hand-authored file Rulesync never writes, so it has folded nothing in,
|
|
55612
|
+
* and in Junie's resolution order it ranks *below* the multi-file layout
|
|
55613
|
+
* that `importOnlyRoots` describes. Gating those roots on it would drop
|
|
55614
|
+
* exactly the files the tool is really reading.
|
|
55615
|
+
*
|
|
55616
|
+
* Resolved once, up front, so the two blocks that need it do not depend
|
|
55617
|
+
* on each other's evaluation order.
|
|
55618
|
+
*/
|
|
55619
|
+
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", settablePaths.root.relativeFilePath)) : [];
|
|
54411
55620
|
const rootToolRules = await (async () => {
|
|
54412
55621
|
if (!settablePaths.root) return [];
|
|
54413
|
-
const uniqueRootFilePaths = await findFilesWithFallback(
|
|
55622
|
+
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, alt.relativeFilePath));
|
|
54414
55623
|
if (forDeletion) return buildDeletionRulesFromPaths(uniqueRootFilePaths);
|
|
54415
|
-
return await
|
|
54416
|
-
const relativeDirPath = resolveRelativeDirPath(filePath);
|
|
54417
|
-
checkPathTraversal({
|
|
54418
|
-
relativePath: relativeDirPath,
|
|
54419
|
-
intendedRootDir: this.outputRoot
|
|
54420
|
-
});
|
|
54421
|
-
return factory.class.fromFile({
|
|
54422
|
-
outputRoot: this.outputRoot,
|
|
54423
|
-
relativeFilePath: (0, node_path.basename)(filePath),
|
|
54424
|
-
relativeDirPath,
|
|
54425
|
-
global: this.global
|
|
54426
|
-
});
|
|
54427
|
-
}));
|
|
55624
|
+
return await buildImportRulesFromPaths(uniqueRootFilePaths);
|
|
54428
55625
|
})();
|
|
54429
55626
|
this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
|
|
54430
55627
|
const localRootToolRules = await (async () => {
|
|
@@ -54436,7 +55633,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54436
55633
|
fileName
|
|
54437
55634
|
}));
|
|
54438
55635
|
if (!settablePaths.root) return [];
|
|
54439
|
-
return await findFilesWithFallback((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName));
|
|
55636
|
+
return await findFilesWithFallback(await findFilesByGlobs((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName)), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName));
|
|
54440
55637
|
})();
|
|
54441
55638
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
54442
55639
|
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
@@ -54512,6 +55709,29 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54512
55709
|
}));
|
|
54513
55710
|
})();
|
|
54514
55711
|
this.logger.debug(`Found ${nestedToolRules.length} nested tool rule files`);
|
|
55712
|
+
const importOnlyToolRules = await (async () => {
|
|
55713
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
55714
|
+
if (forDeletion || !importOnlyRoots || importOnlyRoots.length === 0) return [];
|
|
55715
|
+
const rootFilePath = primaryRootFilePaths[0];
|
|
55716
|
+
const scannedPaths = [];
|
|
55717
|
+
const skippedPaths = [];
|
|
55718
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
55719
|
+
const matchedPaths = await findFilesByGlobs((0, node_path.join)(this.outputRoot, importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), { type: "file" });
|
|
55720
|
+
if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
|
|
55721
|
+
skippedPaths.push(...matchedPaths);
|
|
55722
|
+
continue;
|
|
55723
|
+
}
|
|
55724
|
+
scannedPaths.push(...matchedPaths);
|
|
55725
|
+
}
|
|
55726
|
+
if (skippedPaths.length > 0 && rootFilePath !== void 0) {
|
|
55727
|
+
const skippedNames = skippedPaths.map((filePath) => stripControlCharacters((0, node_path.relative)(this.outputRoot, filePath)));
|
|
55728
|
+
const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
55729
|
+
const remainingCount = skippedNames.length - listedNames.length;
|
|
55730
|
+
this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters((0, node_path.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.`);
|
|
55731
|
+
}
|
|
55732
|
+
return await buildImportRulesFromPaths(scannedPaths);
|
|
55733
|
+
})();
|
|
55734
|
+
this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);
|
|
54515
55735
|
const nonRootToolRules = await (async () => {
|
|
54516
55736
|
if (!settablePaths.nonRoot) return [];
|
|
54517
55737
|
const nonRootOutputRoot = (0, node_path.join)(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
|
|
@@ -54544,6 +55764,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
54544
55764
|
})();
|
|
54545
55765
|
this.logger.debug(`Found ${nonRootToolRules.length} non-root tool rule files`);
|
|
54546
55766
|
return [
|
|
55767
|
+
...importOnlyToolRules,
|
|
54547
55768
|
...rootToolRules,
|
|
54548
55769
|
...localRootToolRules,
|
|
54549
55770
|
...rootMirrorDeletionRules,
|
|
@@ -55351,14 +56572,53 @@ function warnUnsupportedTargets(params) {
|
|
|
55351
56572
|
}
|
|
55352
56573
|
}
|
|
55353
56574
|
/**
|
|
55354
|
-
*
|
|
55355
|
-
*
|
|
55356
|
-
*
|
|
55357
|
-
*
|
|
55358
|
-
*
|
|
56575
|
+
* Inspect every configured input-root path. The first entry is the required
|
|
56576
|
+
* base source tree; later entries are optional overlays and may be absent.
|
|
56577
|
+
* Each existing entry is a rulesync source tree itself (the directory that
|
|
56578
|
+
* directly holds `rules/`, `skills/`, `mcp.jsonc`, etc.). Existing empty
|
|
56579
|
+
* directories are valid because delete and check workflows still need to
|
|
56580
|
+
* inspect generated outputs.
|
|
55359
56581
|
*/
|
|
55360
|
-
async function
|
|
55361
|
-
|
|
56582
|
+
async function inspectInputRoots(inputRoots) {
|
|
56583
|
+
const existing = [];
|
|
56584
|
+
const missing = [];
|
|
56585
|
+
const invalidOverlays = [];
|
|
56586
|
+
const nonDirectories = /* @__PURE__ */ new Set();
|
|
56587
|
+
for (const [index, root] of inputRoots.entries()) if (await directoryExists(root)) existing.push(root);
|
|
56588
|
+
else {
|
|
56589
|
+
missing.push(root);
|
|
56590
|
+
if (await fileExists(root)) {
|
|
56591
|
+
nonDirectories.add(root);
|
|
56592
|
+
if (index > 0) invalidOverlays.push(root);
|
|
56593
|
+
}
|
|
56594
|
+
}
|
|
56595
|
+
const primaryRoot = inputRoots[0];
|
|
56596
|
+
const displayPrimaryRoot = stripControlCharacters(primaryRoot ?? "");
|
|
56597
|
+
if (primaryRoot === void 0 || existing.includes(primaryRoot)) {
|
|
56598
|
+
const invalidOverlay = invalidOverlays[0];
|
|
56599
|
+
return {
|
|
56600
|
+
existing,
|
|
56601
|
+
missing,
|
|
56602
|
+
message: invalidOverlay === void 0 ? void 0 : `Configured optional input root '${stripControlCharacters(invalidOverlay)}' exists but is not a directory.`
|
|
56603
|
+
};
|
|
56604
|
+
}
|
|
56605
|
+
const defaultRoot = (0, node_path.join)(process.cwd(), RULESYNC_RELATIVE_DIR_PATH);
|
|
56606
|
+
if (primaryRoot === defaultRoot && !nonDirectories.has(primaryRoot)) return {
|
|
56607
|
+
existing,
|
|
56608
|
+
missing,
|
|
56609
|
+
message: `Rulesync source directory '${defaultRoot}' does not exist. Run 'rulesync init' first.`
|
|
56610
|
+
};
|
|
56611
|
+
const settingHint = `your input root setting ('inputRoots', or the deprecated 'inputRoot')`;
|
|
56612
|
+
if (nonDirectories.has(primaryRoot)) return {
|
|
56613
|
+
existing,
|
|
56614
|
+
missing,
|
|
56615
|
+
message: `Configured primary input root '${displayPrimaryRoot}' exists but is not a directory. Point ${settingHint} at a directory.`
|
|
56616
|
+
};
|
|
56617
|
+
return {
|
|
56618
|
+
existing,
|
|
56619
|
+
missing,
|
|
56620
|
+
message: `Configured primary input root '${displayPrimaryRoot}' does not exist. Create the directory or update ${settingHint}.`
|
|
56621
|
+
};
|
|
55362
56622
|
}
|
|
55363
56623
|
function dependsOnReachable(byId, from, target) {
|
|
55364
56624
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -55489,7 +56749,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
55489
56749
|
const subagentsDirPath = subagentFactory.class.getSettablePaths({ global }).relativeDirPath;
|
|
55490
56750
|
if (subagentsDirPath !== skillFactory.class.getSettablePaths({ global }).relativeDirPath) continue;
|
|
55491
56751
|
const subagentsProcessor = new SubagentsProcessor({
|
|
55492
|
-
|
|
56752
|
+
inputRoots: config.getInputRoots(),
|
|
55493
56753
|
toolTarget,
|
|
55494
56754
|
global,
|
|
55495
56755
|
logger
|
|
@@ -55497,7 +56757,7 @@ async function warnSkillSubagentNameCollisions(params) {
|
|
|
55497
56757
|
const subagentNames = new Set((await subagentsProcessor.loadRulesyncFiles()).filter((file) => file instanceof RulesyncSubagent).filter((file) => subagentFactory.class.isTargetedByRulesyncSubagent(file)).map((file) => (0, node_path.basename)(file.getRelativeFilePath(), (0, node_path.extname)(file.getRelativeFilePath()))));
|
|
55498
56758
|
if (subagentNames.size === 0) continue;
|
|
55499
56759
|
const skillNames = (await new SkillsProcessor({
|
|
55500
|
-
|
|
56760
|
+
inputRoots: config.getInputRoots(),
|
|
55501
56761
|
toolTarget,
|
|
55502
56762
|
global,
|
|
55503
56763
|
logger
|
|
@@ -55546,6 +56806,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
55546
56806
|
*/
|
|
55547
56807
|
async function generate(params) {
|
|
55548
56808
|
const { config, logger } = params;
|
|
56809
|
+
resetRootShadowingWarnings({ logger });
|
|
55549
56810
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
55550
56811
|
toolTarget,
|
|
55551
56812
|
outputRoot
|
|
@@ -55684,7 +56945,7 @@ async function generateRulesCore(params) {
|
|
|
55684
56945
|
toolTarget,
|
|
55685
56946
|
global: config.getGlobal()
|
|
55686
56947
|
}),
|
|
55687
|
-
|
|
56948
|
+
inputRoots: config.getInputRoots(),
|
|
55688
56949
|
toolTarget,
|
|
55689
56950
|
global: config.getGlobal(),
|
|
55690
56951
|
simulateCommands: config.getSimulateCommands(),
|
|
@@ -55734,7 +56995,7 @@ async function generateIgnoreCore(params) {
|
|
|
55734
56995
|
for (const outputRoot of config.getOutputRoots(toolTarget)) try {
|
|
55735
56996
|
const processor = new IgnoreProcessor({
|
|
55736
56997
|
outputRoot,
|
|
55737
|
-
|
|
56998
|
+
inputRoots: config.getInputRoots(),
|
|
55738
56999
|
toolTarget,
|
|
55739
57000
|
global,
|
|
55740
57001
|
dryRun: config.isPreviewMode(),
|
|
@@ -55781,7 +57042,7 @@ async function generateMcpCore(params) {
|
|
|
55781
57042
|
toolTarget,
|
|
55782
57043
|
global: config.getGlobal()
|
|
55783
57044
|
}),
|
|
55784
|
-
|
|
57045
|
+
inputRoots: config.getInputRoots(),
|
|
55785
57046
|
toolTarget,
|
|
55786
57047
|
global: config.getGlobal(),
|
|
55787
57048
|
dryRun: config.isPreviewMode(),
|
|
@@ -55827,7 +57088,7 @@ async function generateCommandsCore(params) {
|
|
|
55827
57088
|
toolTarget,
|
|
55828
57089
|
global: config.getGlobal()
|
|
55829
57090
|
}),
|
|
55830
|
-
|
|
57091
|
+
inputRoots: config.getInputRoots(),
|
|
55831
57092
|
toolTarget,
|
|
55832
57093
|
global: config.getGlobal(),
|
|
55833
57094
|
dryRun: config.isPreviewMode(),
|
|
@@ -55874,7 +57135,7 @@ async function generateSubagentsCore(params) {
|
|
|
55874
57135
|
toolTarget,
|
|
55875
57136
|
global: config.getGlobal()
|
|
55876
57137
|
}),
|
|
55877
|
-
|
|
57138
|
+
inputRoots: config.getInputRoots(),
|
|
55878
57139
|
toolTarget,
|
|
55879
57140
|
global: config.getGlobal(),
|
|
55880
57141
|
dryRun: config.isPreviewMode(),
|
|
@@ -55921,7 +57182,7 @@ async function generateSkillsCore(params) {
|
|
|
55921
57182
|
toolTarget,
|
|
55922
57183
|
global: config.getGlobal()
|
|
55923
57184
|
}),
|
|
55924
|
-
|
|
57185
|
+
inputRoots: config.getInputRoots(),
|
|
55925
57186
|
toolTarget,
|
|
55926
57187
|
global: config.getGlobal(),
|
|
55927
57188
|
dryRun: config.isPreviewMode(),
|
|
@@ -55966,7 +57227,7 @@ async function generateHooksCore(params) {
|
|
|
55966
57227
|
toolTarget,
|
|
55967
57228
|
global: config.getGlobal()
|
|
55968
57229
|
}),
|
|
55969
|
-
|
|
57230
|
+
inputRoots: config.getInputRoots(),
|
|
55970
57231
|
toolTarget,
|
|
55971
57232
|
global: config.getGlobal(),
|
|
55972
57233
|
dryRun: config.isPreviewMode(),
|
|
@@ -56008,7 +57269,7 @@ async function generatePermissionsCore(params) {
|
|
|
56008
57269
|
toolTarget,
|
|
56009
57270
|
global: config.getGlobal()
|
|
56010
57271
|
}),
|
|
56011
|
-
|
|
57272
|
+
inputRoots: config.getInputRoots(),
|
|
56012
57273
|
toolTarget,
|
|
56013
57274
|
global: config.getGlobal(),
|
|
56014
57275
|
dryRun: config.isPreviewMode(),
|
|
@@ -56054,7 +57315,7 @@ async function generateChecksCore(params) {
|
|
|
56054
57315
|
toolTarget,
|
|
56055
57316
|
global: config.getGlobal()
|
|
56056
57317
|
}),
|
|
56057
|
-
|
|
57318
|
+
inputRoots: config.getInputRoots(),
|
|
56058
57319
|
toolTarget,
|
|
56059
57320
|
global: config.getGlobal(),
|
|
56060
57321
|
dryRun: config.isPreviewMode(),
|
|
@@ -56918,12 +58179,6 @@ Object.defineProperty(exports, "checkPathTraversal", {
|
|
|
56918
58179
|
return checkPathTraversal;
|
|
56919
58180
|
}
|
|
56920
58181
|
});
|
|
56921
|
-
Object.defineProperty(exports, "checkRulesyncDirExists", {
|
|
56922
|
-
enumerable: true,
|
|
56923
|
-
get: function() {
|
|
56924
|
-
return checkRulesyncDirExists;
|
|
56925
|
-
}
|
|
56926
|
-
});
|
|
56927
58182
|
Object.defineProperty(exports, "convertFromTool", {
|
|
56928
58183
|
enumerable: true,
|
|
56929
58184
|
get: function() {
|
|
@@ -57020,6 +58275,12 @@ Object.defineProperty(exports, "importFromTool", {
|
|
|
57020
58275
|
return importFromTool;
|
|
57021
58276
|
}
|
|
57022
58277
|
});
|
|
58278
|
+
Object.defineProperty(exports, "inspectInputRoots", {
|
|
58279
|
+
enumerable: true,
|
|
58280
|
+
get: function() {
|
|
58281
|
+
return inspectInputRoots;
|
|
58282
|
+
}
|
|
58283
|
+
});
|
|
57023
58284
|
Object.defineProperty(exports, "isPackagingToolTarget", {
|
|
57024
58285
|
enumerable: true,
|
|
57025
58286
|
get: function() {
|
|
@@ -57044,6 +58305,12 @@ Object.defineProperty(exports, "loadYaml", {
|
|
|
57044
58305
|
return loadYaml;
|
|
57045
58306
|
}
|
|
57046
58307
|
});
|
|
58308
|
+
Object.defineProperty(exports, "mergeInputRootConfigs", {
|
|
58309
|
+
enumerable: true,
|
|
58310
|
+
get: function() {
|
|
58311
|
+
return mergeInputRootConfigs;
|
|
58312
|
+
}
|
|
58313
|
+
});
|
|
57047
58314
|
Object.defineProperty(exports, "parseJsonc", {
|
|
57048
58315
|
enumerable: true,
|
|
57049
58316
|
get: function() {
|
|
@@ -57092,6 +58359,12 @@ Object.defineProperty(exports, "removeTempDirectory", {
|
|
|
57092
58359
|
return removeTempDirectory;
|
|
57093
58360
|
}
|
|
57094
58361
|
});
|
|
58362
|
+
Object.defineProperty(exports, "resolveEffectiveInputRoots", {
|
|
58363
|
+
enumerable: true,
|
|
58364
|
+
get: function() {
|
|
58365
|
+
return resolveEffectiveInputRoots;
|
|
58366
|
+
}
|
|
58367
|
+
});
|
|
57095
58368
|
Object.defineProperty(exports, "resolvePath", {
|
|
57096
58369
|
enumerable: true,
|
|
57097
58370
|
get: function() {
|