rulesync 16.20.0 → 16.21.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 +248 -67
- package/dist/cli/index.js +248 -67
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-1-jjDDAm.js → import-5_n-Y8N6.js} +1769 -432
- package/dist/import-5_n-Y8N6.js.map +1 -0
- package/dist/{import-BlhbArzI.cjs → import-7UmDF35I.cjs} +1810 -455
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +14 -14
- package/dist/import-1-jjDDAm.js.map +0 -1
|
@@ -26,6 +26,7 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
26
26
|
let node_path = require("node:path");
|
|
27
27
|
node_path = __toESM(node_path, 1);
|
|
28
28
|
let jsonc_parser = require("jsonc-parser");
|
|
29
|
+
let node_fs = require("node:fs");
|
|
29
30
|
let node_os = require("node:os");
|
|
30
31
|
node_os = __toESM(node_os, 1);
|
|
31
32
|
let es_toolkit = require("es-toolkit");
|
|
@@ -36,7 +37,6 @@ let gray_matter = require("gray-matter");
|
|
|
36
37
|
gray_matter = __toESM(gray_matter, 1);
|
|
37
38
|
let js_yaml = require("js-yaml");
|
|
38
39
|
let es_toolkit_object = require("es-toolkit/object");
|
|
39
|
-
let node_fs = require("node:fs");
|
|
40
40
|
let node_crypto = require("node:crypto");
|
|
41
41
|
let smol_toml = require("smol-toml");
|
|
42
42
|
smol_toml = __toESM(smol_toml, 1);
|
|
@@ -63,6 +63,26 @@ function stripControlCharacters(text) {
|
|
|
63
63
|
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
+
* Strips the control characters from `text` and then JSON-quotes it, which is
|
|
67
|
+
* the form a remote-derived name — a fetched path, a directory name read off
|
|
68
|
+
* the disk, a branch name chosen by a remote repository — takes in a log line.
|
|
69
|
+
*
|
|
70
|
+
* The two steps do different jobs, and neither is enough on its own. The
|
|
71
|
+
* quoting delimits the untrusted text, so a reader can tell where the name
|
|
72
|
+
* ends and the diagnostic resumes, and it escapes the quotes and backslashes
|
|
73
|
+
* that would blur that edge. But `JSON.stringify` escapes the C0 controls and
|
|
74
|
+
* nothing else: the C1 range, the 8-bit CSI introducer among it, and the bidi
|
|
75
|
+
* overrides pass through it intact, and any one of them reaches the terminal
|
|
76
|
+
* with its power to forge a line or reorder one. The strip is what takes those
|
|
77
|
+
* out. It runs first so that the quoting sees only printable text and the line
|
|
78
|
+
* carries no escaped control characters the reader has to decode — what could
|
|
79
|
+
* do harm is gone rather than spelled out — and so that the rationale for the
|
|
80
|
+
* order lives here, once, rather than at every call site.
|
|
81
|
+
*/
|
|
82
|
+
function quoteForLog(text) {
|
|
83
|
+
return JSON.stringify(stripControlCharacters(text));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
66
86
|
* Removes every control character from `text` except the line feed, so a
|
|
67
87
|
* message written to be read over several lines still is.
|
|
68
88
|
*
|
|
@@ -395,34 +415,34 @@ const isFeatureValueEnabled = (value) => {
|
|
|
395
415
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
396
416
|
//#endregion
|
|
397
417
|
//#region src/constants/rulesync-paths.ts
|
|
398
|
-
const { join: join$
|
|
418
|
+
const { join: join$299 } = node_path.posix;
|
|
399
419
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
400
420
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
401
421
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
402
422
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
403
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
423
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$299(RULES_FEATURE_SUBDIR, ".curated");
|
|
404
424
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
405
425
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
406
426
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
407
427
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
408
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
409
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
410
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
411
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
412
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
413
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
414
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
415
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
416
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
417
|
-
join$
|
|
418
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
419
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
428
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$299(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
429
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
430
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
431
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
432
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
433
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
434
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
435
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
436
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
437
|
+
join$299(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
438
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
439
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
420
440
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
421
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
441
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
422
442
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
423
443
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
424
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
425
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
444
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
445
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$299(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
426
446
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
427
447
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
428
448
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -928,9 +948,9 @@ function toPosixPath(p) {
|
|
|
928
948
|
return p.replace(/\\/g, "/");
|
|
929
949
|
}
|
|
930
950
|
function checkPathTraversal({ relativePath, intendedRootDir }) {
|
|
931
|
-
if (relativePath.split(/[/\\]/).includes("..")) throw new Error(`Path traversal detected: ${
|
|
951
|
+
if (relativePath.split(/[/\\]/).includes("..")) throw new Error(`Path traversal detected: ${quoteForLog(relativePath)}`);
|
|
932
952
|
const resolved = (0, node_path.resolve)(intendedRootDir, relativePath);
|
|
933
|
-
if ((0, node_path.relative)(intendedRootDir, resolved).startsWith("..") || (0, node_path.resolve)(resolved) !== resolved) throw new Error(`Path traversal detected: ${
|
|
953
|
+
if ((0, node_path.relative)(intendedRootDir, resolved).startsWith("..") || (0, node_path.resolve)(resolved) !== resolved) throw new Error(`Path traversal detected: ${quoteForLog(relativePath)}`);
|
|
934
954
|
}
|
|
935
955
|
/**
|
|
936
956
|
* Resolves a path relative to a base directory, handling both absolute and relative paths
|
|
@@ -990,24 +1010,60 @@ async function writeFileContent(filepath, content) {
|
|
|
990
1010
|
* Apply a POSIX mode to an existing file. Windows has no executable bit and
|
|
991
1011
|
* `chmod` there only toggles the read-only flag, so the call is skipped rather
|
|
992
1012
|
* than writing a mode the platform cannot honor.
|
|
1013
|
+
*
|
|
1014
|
+
* The mode goes through a handle opened with `O_NOFOLLOW`, so a symbolic link
|
|
1015
|
+
* standing where the file should be is left alone rather than having the mode
|
|
1016
|
+
* land on whatever it points at -- possibly outside the output tree -- and the
|
|
1017
|
+
* file whose mode changes is the very one that was opened, with no path to
|
|
1018
|
+
* swap between the check and the `chmod`. The write side refuses to read
|
|
1019
|
+
* through links; the mode side refuses to write through them. A link is
|
|
1020
|
+
* skipped silently, like Windows; a file that does not exist still throws.
|
|
993
1021
|
*/
|
|
994
1022
|
async function applyFileMode(filepath, mode) {
|
|
995
1023
|
if (process.platform === "win32") return;
|
|
996
|
-
await (
|
|
1024
|
+
const fileHandle = await openNotFollowingLinks(filepath);
|
|
1025
|
+
if (fileHandle === void 0) return;
|
|
1026
|
+
try {
|
|
1027
|
+
await fileHandle.chmod(mode);
|
|
1028
|
+
} finally {
|
|
1029
|
+
await fileHandle.close();
|
|
1030
|
+
}
|
|
997
1031
|
}
|
|
998
1032
|
/**
|
|
999
1033
|
* Restore an executable bit that went missing (interrupted run, a copy that
|
|
1000
1034
|
* dropped the mode). A file whose mode is merely stricter than `mode` — the
|
|
1001
|
-
* user chose 0700 over 0755 — is left alone
|
|
1035
|
+
* user chose 0700 over 0755 — is left alone, and so is a symbolic link or a
|
|
1036
|
+
* file that cannot be opened: this repairs, it never creates.
|
|
1002
1037
|
*/
|
|
1003
1038
|
async function restoreMissingExecutableBit(filepath, mode) {
|
|
1004
1039
|
if (process.platform === "win32") return;
|
|
1040
|
+
let fileHandle;
|
|
1005
1041
|
try {
|
|
1006
|
-
|
|
1042
|
+
fileHandle = await openNotFollowingLinks(filepath);
|
|
1007
1043
|
} catch {
|
|
1008
1044
|
return;
|
|
1009
1045
|
}
|
|
1010
|
-
|
|
1046
|
+
if (fileHandle === void 0) return;
|
|
1047
|
+
try {
|
|
1048
|
+
if (((await fileHandle.stat()).mode & 73) !== 0) return;
|
|
1049
|
+
await fileHandle.chmod(mode);
|
|
1050
|
+
} finally {
|
|
1051
|
+
await fileHandle.close();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Open a file for its mode without following a symbolic link at the path.
|
|
1056
|
+
* Returns `undefined` when the path is a link (`O_NOFOLLOW` fails with
|
|
1057
|
+
* `ELOOP`, or `EMLINK` on some BSDs); any other failure is the caller's.
|
|
1058
|
+
*/
|
|
1059
|
+
async function openNotFollowingLinks(filepath) {
|
|
1060
|
+
try {
|
|
1061
|
+
return await (0, node_fs_promises.open)(filepath, node_fs.constants.O_RDONLY | (node_fs.constants.O_NOFOLLOW ?? 0));
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
const code = error.code;
|
|
1064
|
+
if (code === "ELOOP" || code === "EMLINK") return;
|
|
1065
|
+
throw error;
|
|
1066
|
+
}
|
|
1011
1067
|
}
|
|
1012
1068
|
async function writeFileBuffer(filepath, buffer) {
|
|
1013
1069
|
await ensureDir((0, node_path.dirname)(filepath));
|
|
@@ -1218,6 +1274,19 @@ function posixRelativePathEscapesRoot(relativePath) {
|
|
|
1218
1274
|
return relativePath === ".." || relativePath.startsWith("../") || node_path.posix.isAbsolute(relativePath);
|
|
1219
1275
|
}
|
|
1220
1276
|
/**
|
|
1277
|
+
* Whether the file `targetPath` really denotes sits outside `rootPath`. Unlike
|
|
1278
|
+
* `pathEscapesRoot`, which reads a path as it is spelled, this resolves every link
|
|
1279
|
+
* on both sides first, so a name that sits inside the root but is a link pointing
|
|
1280
|
+
* out of it is reported as the escape it is. Both sides fall back to their literal
|
|
1281
|
+
* path when they cannot be resolved, which reports an escape rather than hiding one.
|
|
1282
|
+
*/
|
|
1283
|
+
async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
1284
|
+
return posixRelativePathEscapesRoot(await resolvedRelativePath({
|
|
1285
|
+
rootPath,
|
|
1286
|
+
targetPath
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1221
1290
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1222
1291
|
*
|
|
1223
1292
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -1227,9 +1296,17 @@ function posixRelativePathEscapesRoot(relativePath) {
|
|
|
1227
1296
|
* what lets the comparison hold for a path that is not itself resolved: a glob
|
|
1228
1297
|
* rooted at a directory that is a link of its own gives every candidate the
|
|
1229
1298
|
* same unresolved prefix, and only the segments below it decide.
|
|
1299
|
+
*
|
|
1300
|
+
* `filePath` is spelled the way the platform reported it and `identity` is
|
|
1301
|
+
* posix-separated, so the two are split on both separators: on Windows a
|
|
1302
|
+
* backslash-separated path then shares its segments with the posix identity
|
|
1303
|
+
* without being rewritten first, and on a posix platform a backslash inside a
|
|
1304
|
+
* name is split the same way on both sides, which keeps the count symmetric.
|
|
1305
|
+
*
|
|
1306
|
+
* Exported for its tests, which need a Windows-style spelling on any platform.
|
|
1230
1307
|
*/
|
|
1231
1308
|
function sharedTrailingSegments(filePath, identity) {
|
|
1232
|
-
const left = splitPathSegments(
|
|
1309
|
+
const left = splitPathSegments(filePath);
|
|
1233
1310
|
const right = splitPathSegments(identity);
|
|
1234
1311
|
let shared = 0;
|
|
1235
1312
|
while (shared < left.length && shared < right.length && left[left.length - 1 - shared] === right[right.length - 1 - shared]) shared++;
|
|
@@ -1257,7 +1334,7 @@ function chooseRepresentative(candidates, identity) {
|
|
|
1257
1334
|
});
|
|
1258
1335
|
}
|
|
1259
1336
|
async function findFilesByGlobs(globs, options = {}) {
|
|
1260
|
-
const { type = "all", followSymbolicLinks = true, ignore, dot = false } = options;
|
|
1337
|
+
const { cwd, type = "all", followSymbolicLinks = true, ignore, dot = false } = options;
|
|
1261
1338
|
const globbyOptions = type === "file" ? {
|
|
1262
1339
|
onlyFiles: true,
|
|
1263
1340
|
onlyDirectories: false
|
|
@@ -1271,6 +1348,7 @@ async function findFilesByGlobs(globs, options = {}) {
|
|
|
1271
1348
|
const normalizedGlobs = Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/");
|
|
1272
1349
|
const results = (0, globby.globbySync)(normalizedGlobs, {
|
|
1273
1350
|
absolute: true,
|
|
1351
|
+
cwd,
|
|
1274
1352
|
followSymbolicLinks,
|
|
1275
1353
|
dot,
|
|
1276
1354
|
...ignore ? { ignore: ignore.map((pattern) => pattern.replaceAll("\\", "/")) } : {},
|
|
@@ -1343,14 +1421,16 @@ async function listEntryNames(params) {
|
|
|
1343
1421
|
*/
|
|
1344
1422
|
async function dedupeNamesByFileIdentity(params) {
|
|
1345
1423
|
const { dirPath, entries } = params;
|
|
1346
|
-
const
|
|
1424
|
+
const identifiedEntries = await mapWithConcurrency({
|
|
1347
1425
|
items: entries,
|
|
1348
1426
|
limit: ENTRY_CLASSIFY_CONCURRENCY,
|
|
1349
|
-
mapper: async (entry) =>
|
|
1427
|
+
mapper: async (entry) => ({
|
|
1428
|
+
entry,
|
|
1429
|
+
identity: await realFileIdentity((0, node_path.join)(dirPath, entry.name))
|
|
1430
|
+
})
|
|
1350
1431
|
});
|
|
1351
1432
|
const entriesByIdentity = /* @__PURE__ */ new Map();
|
|
1352
|
-
for (const
|
|
1353
|
-
const identity = identities[index] ?? nativePathToPosix((0, node_path.join)(dirPath, entry.name));
|
|
1433
|
+
for (const { entry, identity } of identifiedEntries) {
|
|
1354
1434
|
const group = entriesByIdentity.get(identity);
|
|
1355
1435
|
if (group === void 0) entriesByIdentity.set(identity, [entry]);
|
|
1356
1436
|
else group.push(entry);
|
|
@@ -1649,43 +1729,76 @@ var CLIError = class extends Error {
|
|
|
1649
1729
|
};
|
|
1650
1730
|
//#endregion
|
|
1651
1731
|
//#region src/utils/warned-once.ts
|
|
1732
|
+
function createRunWarningState() {
|
|
1733
|
+
return {
|
|
1734
|
+
messages: /* @__PURE__ */ new Set(),
|
|
1735
|
+
carriedFilesIncomplete: false
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1652
1738
|
/**
|
|
1653
|
-
* The
|
|
1739
|
+
* The state of a run that opened no scope of its own.
|
|
1654
1740
|
* This lives in its own module, importing nothing of rulesync's, so the vitest
|
|
1655
1741
|
* setup file can clear it between tests without pulling `logger.js` into every
|
|
1656
1742
|
* test's module graph (which would defeat the module mocks some of those tests
|
|
1657
1743
|
* install).
|
|
1658
1744
|
*/
|
|
1659
|
-
const
|
|
1745
|
+
const processWideState = createRunWarningState();
|
|
1660
1746
|
/**
|
|
1661
|
-
* The
|
|
1747
|
+
* The state an operation that opened its own scope uses instead.
|
|
1662
1748
|
*
|
|
1663
1749
|
* The MCP server does not serialize requests, so two runs can be in flight at
|
|
1664
1750
|
* once. Sharing one set between them would let the first run spend the token
|
|
1665
1751
|
* for a message and leave the second one's result silent about a diagnostic
|
|
1666
1752
|
* that applies to it just as much. A scope gives each run its own bookkeeping.
|
|
1667
1753
|
*/
|
|
1668
|
-
const
|
|
1669
|
-
function
|
|
1670
|
-
return
|
|
1754
|
+
const scopedState = new node_async_hooks.AsyncLocalStorage();
|
|
1755
|
+
function currentState() {
|
|
1756
|
+
return scopedState.getStore() ?? processWideState;
|
|
1671
1757
|
}
|
|
1672
1758
|
/** Whether `message` has not been emitted yet; records it when it has not. */
|
|
1673
1759
|
function claimWarnOnce(message) {
|
|
1674
|
-
const messages =
|
|
1760
|
+
const { messages } = currentState();
|
|
1675
1761
|
if (messages.has(message)) return false;
|
|
1676
1762
|
messages.add(message);
|
|
1677
1763
|
return true;
|
|
1678
1764
|
}
|
|
1679
|
-
/**
|
|
1680
|
-
|
|
1681
|
-
|
|
1765
|
+
/**
|
|
1766
|
+
* Record that this run could not read everything a directory carries -- a file
|
|
1767
|
+
* it could not open, a walk that hit one of its bounds, a subtree it was denied.
|
|
1768
|
+
*
|
|
1769
|
+
* Kept here, beside the once-per-run messages, because it shares their lifetime
|
|
1770
|
+
* exactly: it is set at the moment such a shortfall is warned about, and cleared
|
|
1771
|
+
* when the next run resets its warnings.
|
|
1772
|
+
*
|
|
1773
|
+
* Deliberate refusals are not shortfalls. A hidden entry, a credential-shaped
|
|
1774
|
+
* name, a link into a pseudo-filesystem: those are files Rulesync never carries,
|
|
1775
|
+
* on every run, and a run that leaves them out has read its source in full.
|
|
1776
|
+
*/
|
|
1777
|
+
function recordIncompleteCarriedFiles() {
|
|
1778
|
+
currentState().carriedFilesIncomplete = true;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Whether {@link recordIncompleteCarriedFiles} fired in this run.
|
|
1782
|
+
*
|
|
1783
|
+
* A caller that deletes what a run did not write has to ask: a run holding an
|
|
1784
|
+
* incomplete picture of its source cannot tell a stale file from one whose
|
|
1785
|
+
* source it merely failed to read.
|
|
1786
|
+
*/
|
|
1787
|
+
function hasIncompleteCarriedFiles() {
|
|
1788
|
+
return currentState().carriedFilesIncomplete;
|
|
1789
|
+
}
|
|
1790
|
+
/** Forget what was already reported, so the next run starts silent. */
|
|
1791
|
+
function resetRunWarningState() {
|
|
1792
|
+
const state = currentState();
|
|
1793
|
+
state.messages.clear();
|
|
1794
|
+
state.carriedFilesIncomplete = false;
|
|
1682
1795
|
}
|
|
1683
1796
|
/**
|
|
1684
1797
|
* Run `operation` with its own once-per-run bookkeeping, so a concurrent run
|
|
1685
1798
|
* neither spends its tokens nor clears its record.
|
|
1686
1799
|
*/
|
|
1687
1800
|
async function withWarnOnceScope(operation) {
|
|
1688
|
-
return await
|
|
1801
|
+
return await scopedState.run(createRunWarningState(), operation);
|
|
1689
1802
|
}
|
|
1690
1803
|
//#endregion
|
|
1691
1804
|
//#region src/utils/logger.ts
|
|
@@ -2059,6 +2172,148 @@ var WarningCollectingLogger = class extends ConsoleLogger {
|
|
|
2059
2172
|
}
|
|
2060
2173
|
};
|
|
2061
2174
|
//#endregion
|
|
2175
|
+
//#region src/types/language.ts
|
|
2176
|
+
/**
|
|
2177
|
+
* Response languages the root `language` key of `rulesync.jsonc` accepts.
|
|
2178
|
+
*
|
|
2179
|
+
* BCP 47-style codes: a bare ISO 639-1 code where one language name is
|
|
2180
|
+
* unambiguous, and a region-qualified code where the written form differs by
|
|
2181
|
+
* region (`zh-CN` / `zh-TW`, `pt-BR`). Widening the list is a non-breaking
|
|
2182
|
+
* change; respelling an existing code is not, so the code style is fixed here.
|
|
2183
|
+
*/
|
|
2184
|
+
const LANGUAGE_CODES = [
|
|
2185
|
+
"en",
|
|
2186
|
+
"ja",
|
|
2187
|
+
"zh-CN",
|
|
2188
|
+
"zh-TW",
|
|
2189
|
+
"ko",
|
|
2190
|
+
"fr",
|
|
2191
|
+
"de",
|
|
2192
|
+
"es",
|
|
2193
|
+
"pt-BR",
|
|
2194
|
+
"ru"
|
|
2195
|
+
];
|
|
2196
|
+
const LanguageSchema = zod_mini.z.enum(LANGUAGE_CODES);
|
|
2197
|
+
const LANGUAGE_DISPLAY = {
|
|
2198
|
+
en: {
|
|
2199
|
+
name: "English",
|
|
2200
|
+
claudecode: "english"
|
|
2201
|
+
},
|
|
2202
|
+
ja: {
|
|
2203
|
+
name: "Japanese",
|
|
2204
|
+
claudecode: "japanese"
|
|
2205
|
+
},
|
|
2206
|
+
"zh-CN": {
|
|
2207
|
+
name: "Simplified Chinese",
|
|
2208
|
+
claudecode: "simplified chinese"
|
|
2209
|
+
},
|
|
2210
|
+
"zh-TW": {
|
|
2211
|
+
name: "Traditional Chinese",
|
|
2212
|
+
claudecode: "traditional chinese"
|
|
2213
|
+
},
|
|
2214
|
+
ko: {
|
|
2215
|
+
name: "Korean",
|
|
2216
|
+
claudecode: "korean"
|
|
2217
|
+
},
|
|
2218
|
+
fr: {
|
|
2219
|
+
name: "French",
|
|
2220
|
+
claudecode: "french"
|
|
2221
|
+
},
|
|
2222
|
+
de: {
|
|
2223
|
+
name: "German",
|
|
2224
|
+
claudecode: "german"
|
|
2225
|
+
},
|
|
2226
|
+
es: {
|
|
2227
|
+
name: "Spanish",
|
|
2228
|
+
claudecode: "spanish"
|
|
2229
|
+
},
|
|
2230
|
+
"pt-BR": {
|
|
2231
|
+
name: "Brazilian Portuguese",
|
|
2232
|
+
claudecode: "brazilian portuguese"
|
|
2233
|
+
},
|
|
2234
|
+
ru: {
|
|
2235
|
+
name: "Russian",
|
|
2236
|
+
claudecode: "russian"
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
/** English display name of a language, as used in the appended prompt. */
|
|
2240
|
+
function getLanguageName(language) {
|
|
2241
|
+
return LANGUAGE_DISPLAY[language].name;
|
|
2242
|
+
}
|
|
2243
|
+
/** The value Claude Code's `language` settings key expects for a language. */
|
|
2244
|
+
function getClaudecodeLanguageValue(language) {
|
|
2245
|
+
return LANGUAGE_DISPLAY[language].claudecode;
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* The one sentence rulesync appends to generated root rule files when
|
|
2249
|
+
* `language` is set. Every supported language — `en` included — emits it, so
|
|
2250
|
+
* the instruction is explicit rather than implied by the absence of a block.
|
|
2251
|
+
*/
|
|
2252
|
+
function buildLanguageInstruction(language) {
|
|
2253
|
+
return `You must always answer in ${getLanguageName(language)}. On the other hand, reasoning (thinking) should be in English to improve token efficiency.`;
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Separator that opens the appended block. A thematic break makes the
|
|
2257
|
+
* concatenation visible, so a reader can tell the sentence was appended by
|
|
2258
|
+
* rulesync rather than authored as part of the rules.
|
|
2259
|
+
*/
|
|
2260
|
+
const LANGUAGE_BLOCK_SEPARATOR = "---";
|
|
2261
|
+
/**
|
|
2262
|
+
* Append the language block to a rule body: a blank line, the separator, a
|
|
2263
|
+
* blank line, then the instruction. A body that is empty (or whitespace only)
|
|
2264
|
+
* gets the instruction alone, because a file that starts with `---` reads as
|
|
2265
|
+
* the opening of a frontmatter block.
|
|
2266
|
+
*/
|
|
2267
|
+
function appendLanguageBlock({ content, language }) {
|
|
2268
|
+
const instruction = buildLanguageInstruction(language);
|
|
2269
|
+
const body = content.trimEnd();
|
|
2270
|
+
if (body.length === 0) return instruction;
|
|
2271
|
+
return `${body}\n\n${LANGUAGE_BLOCK_SEPARATOR}\n\n${instruction}`;
|
|
2272
|
+
}
|
|
2273
|
+
const INSTRUCTIONS = LANGUAGE_CODES.map((code) => buildLanguageInstruction(code));
|
|
2274
|
+
const isBlank = (value) => value === " " || value === " ";
|
|
2275
|
+
/**
|
|
2276
|
+
* Remove one trailing block exactly as {@link appendLanguageBlock} writes it,
|
|
2277
|
+
* for any supported language: the separator on its own line, blank space
|
|
2278
|
+
* with at least one line break, the instruction, trailing whitespace. Only
|
|
2279
|
+
* the end of the body is inspected, so a sentence quoted mid-file is left
|
|
2280
|
+
* alone. Returns the body unchanged when no block closes it.
|
|
2281
|
+
*
|
|
2282
|
+
* Walks the body from its end with string operations rather than a regular
|
|
2283
|
+
* expression: an unanchored pattern over optional blank lines backtracks in
|
|
2284
|
+
* polynomial time, and a rule file padded with a few hundred thousand
|
|
2285
|
+
* trailing newlines would hang `rulesync import` on it.
|
|
2286
|
+
*/
|
|
2287
|
+
function stripOneLanguageBlock(body) {
|
|
2288
|
+
const trimmed = body.trimEnd();
|
|
2289
|
+
const instruction = INSTRUCTIONS.find((candidate) => trimmed.endsWith(candidate));
|
|
2290
|
+
if (instruction === void 0) return body;
|
|
2291
|
+
const beforeInstruction = trimmed.slice(0, trimmed.length - instruction.length);
|
|
2292
|
+
const beforeGap = beforeInstruction.trimEnd();
|
|
2293
|
+
if (beforeGap.length === 0) return "";
|
|
2294
|
+
if (!beforeInstruction.slice(beforeGap.length).includes("\n")) return body;
|
|
2295
|
+
if (!beforeGap.endsWith(LANGUAGE_BLOCK_SEPARATOR)) return body;
|
|
2296
|
+
let lineStart = beforeGap.length - 3;
|
|
2297
|
+
while (lineStart > 0 && isBlank(beforeGap.charAt(lineStart - 1))) lineStart -= 1;
|
|
2298
|
+
if (lineStart > 0 && beforeGap.charAt(lineStart - 1) !== "\n") return body;
|
|
2299
|
+
return body.slice(0, lineStart).trimEnd();
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Remove the trailing language block(s) from an imported rule body so that
|
|
2303
|
+
* `rulesync import` followed by `rulesync generate` does not stack a second
|
|
2304
|
+
* copy. Repeats until nothing more comes off, so a file that already carries
|
|
2305
|
+
* two stacked blocks (generated twice by an older flow, say) comes back
|
|
2306
|
+
* clean. Returns the body unchanged when no block is present.
|
|
2307
|
+
*/
|
|
2308
|
+
function stripLanguageBlock(body) {
|
|
2309
|
+
let current = body;
|
|
2310
|
+
for (;;) {
|
|
2311
|
+
const next = stripOneLanguageBlock(current);
|
|
2312
|
+
if (next === current) return current;
|
|
2313
|
+
current = next;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
//#endregion
|
|
2062
2317
|
//#region src/utils/validation.ts
|
|
2063
2318
|
/**
|
|
2064
2319
|
* Shared validation utilities for input sanitization.
|
|
@@ -2130,7 +2385,15 @@ const ConfigParamsSchema = zod_mini.z.object({
|
|
|
2130
2385
|
simulateCommands: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
2131
2386
|
simulateSubagents: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
2132
2387
|
simulateSkills: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
2388
|
+
deriveSubprojectPathFromGlobs: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
2133
2389
|
flattenedCommandNaming: (0, zod_mini.optional)(FlattenedCommandNamingSchema),
|
|
2390
|
+
/**
|
|
2391
|
+
* Response language the generated rules steer the AI toward. Config-file
|
|
2392
|
+
* only (no CLI flag): it is a property of the project, not of one run.
|
|
2393
|
+
* Absent means "say nothing about language", which is why `en` is a real
|
|
2394
|
+
* value rather than the default.
|
|
2395
|
+
*/
|
|
2396
|
+
language: (0, zod_mini.optional)(LanguageSchema),
|
|
2134
2397
|
gitignoreTargetsOnly: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
2135
2398
|
gitignoreDestination: (0, zod_mini.optional)(GitignoreDestinationSchema),
|
|
2136
2399
|
dryRun: (0, zod_mini.optional)(zod_mini.z.boolean()),
|
|
@@ -2270,7 +2533,9 @@ var Config = class Config {
|
|
|
2270
2533
|
simulateCommands;
|
|
2271
2534
|
simulateSubagents;
|
|
2272
2535
|
simulateSkills;
|
|
2536
|
+
deriveSubprojectPathFromGlobs;
|
|
2273
2537
|
flattenedCommandNaming;
|
|
2538
|
+
language;
|
|
2274
2539
|
gitignoreTargetsOnly;
|
|
2275
2540
|
gitignoreDestination;
|
|
2276
2541
|
dryRun;
|
|
@@ -2294,7 +2559,7 @@ var Config = class Config {
|
|
|
2294
2559
|
inputRoots;
|
|
2295
2560
|
configFilePath;
|
|
2296
2561
|
sources;
|
|
2297
|
-
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
2562
|
+
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
2298
2563
|
assertTargetsFeaturesExclusive({
|
|
2299
2564
|
targets,
|
|
2300
2565
|
features
|
|
@@ -2326,7 +2591,9 @@ var Config = class Config {
|
|
|
2326
2591
|
this.simulateCommands = simulateCommands ?? false;
|
|
2327
2592
|
this.simulateSubagents = simulateSubagents ?? false;
|
|
2328
2593
|
this.simulateSkills = simulateSkills ?? false;
|
|
2594
|
+
this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs ?? false;
|
|
2329
2595
|
this.flattenedCommandNaming = flattenedCommandNaming ?? "basename";
|
|
2596
|
+
this.language = language;
|
|
2330
2597
|
this.gitignoreTargetsOnly = gitignoreTargetsOnly ?? true;
|
|
2331
2598
|
this.gitignoreDestination = gitignoreDestination ?? "gitignore";
|
|
2332
2599
|
this.dryRun = dryRun ?? false;
|
|
@@ -2505,12 +2772,22 @@ var Config = class Config {
|
|
|
2505
2772
|
getFlattenedCommandNaming() {
|
|
2506
2773
|
return this.flattenedCommandNaming;
|
|
2507
2774
|
}
|
|
2775
|
+
/**
|
|
2776
|
+
* The configured response language, or `undefined` when `rulesync.jsonc`
|
|
2777
|
+
* does not set one — in which case generation leaves language alone.
|
|
2778
|
+
*/
|
|
2779
|
+
getLanguage() {
|
|
2780
|
+
return this.language;
|
|
2781
|
+
}
|
|
2508
2782
|
getSimulateSubagents() {
|
|
2509
2783
|
return this.simulateSubagents;
|
|
2510
2784
|
}
|
|
2511
2785
|
getSimulateSkills() {
|
|
2512
2786
|
return this.simulateSkills;
|
|
2513
2787
|
}
|
|
2788
|
+
getDeriveSubprojectPathFromGlobs() {
|
|
2789
|
+
return this.deriveSubprojectPathFromGlobs;
|
|
2790
|
+
}
|
|
2514
2791
|
getGitignoreTargetsOnly() {
|
|
2515
2792
|
return this.gitignoreTargetsOnly;
|
|
2516
2793
|
}
|
|
@@ -2571,6 +2848,7 @@ const getDefaults = () => ({
|
|
|
2571
2848
|
simulateCommands: false,
|
|
2572
2849
|
simulateSubagents: false,
|
|
2573
2850
|
simulateSkills: false,
|
|
2851
|
+
deriveSubprojectPathFromGlobs: false,
|
|
2574
2852
|
flattenedCommandNaming: "basename",
|
|
2575
2853
|
gitignoreTargetsOnly: true,
|
|
2576
2854
|
gitignoreDestination: "gitignore",
|
|
@@ -2618,7 +2896,9 @@ const mergeConfigs = (baseConfig, localConfig) => {
|
|
|
2618
2896
|
simulateCommands: localConfig.simulateCommands ?? baseConfig.simulateCommands,
|
|
2619
2897
|
simulateSubagents: localConfig.simulateSubagents ?? baseConfig.simulateSubagents,
|
|
2620
2898
|
simulateSkills: localConfig.simulateSkills ?? baseConfig.simulateSkills,
|
|
2899
|
+
deriveSubprojectPathFromGlobs: localConfig.deriveSubprojectPathFromGlobs ?? baseConfig.deriveSubprojectPathFromGlobs,
|
|
2621
2900
|
flattenedCommandNaming: localConfig.flattenedCommandNaming ?? baseConfig.flattenedCommandNaming,
|
|
2901
|
+
language: localConfig.language ?? baseConfig.language,
|
|
2622
2902
|
gitignoreTargetsOnly: localConfig.gitignoreTargetsOnly ?? baseConfig.gitignoreTargetsOnly,
|
|
2623
2903
|
gitignoreDestination: localConfig.gitignoreDestination ?? baseConfig.gitignoreDestination,
|
|
2624
2904
|
dryRun: localConfig.dryRun ?? baseConfig.dryRun,
|
|
@@ -2740,7 +3020,7 @@ function resolveEffectiveInputRoots({ cliInputRoot, cliInputRoots, configByFile,
|
|
|
2740
3020
|
};
|
|
2741
3021
|
}
|
|
2742
3022
|
var ConfigResolver = class {
|
|
2743
|
-
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot, inputRoots }, { logger } = {}) {
|
|
3023
|
+
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, deriveSubprojectPathFromGlobs, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot, inputRoots }, { logger } = {}) {
|
|
2744
3024
|
const cwd = (0, node_path.resolve)(process.cwd());
|
|
2745
3025
|
assertInputRootFieldsExclusive({
|
|
2746
3026
|
inputRoot,
|
|
@@ -2839,6 +3119,11 @@ var ConfigResolver = class {
|
|
|
2839
3119
|
file: configByFile.simulateSkills,
|
|
2840
3120
|
fallback: getDefaults().simulateSkills
|
|
2841
3121
|
}),
|
|
3122
|
+
deriveSubprojectPathFromGlobs: pick({
|
|
3123
|
+
cli: deriveSubprojectPathFromGlobs,
|
|
3124
|
+
file: configByFile.deriveSubprojectPathFromGlobs,
|
|
3125
|
+
fallback: getDefaults().deriveSubprojectPathFromGlobs
|
|
3126
|
+
}),
|
|
2842
3127
|
gitignoreTargetsOnly: pick({
|
|
2843
3128
|
cli: gitignoreTargetsOnly,
|
|
2844
3129
|
file: configByFile.gitignoreTargetsOnly,
|
|
@@ -2863,6 +3148,7 @@ var ConfigResolver = class {
|
|
|
2863
3148
|
configFilePath: validatedConfigPath,
|
|
2864
3149
|
sources: configByFile.sources ?? getDefaults().sources,
|
|
2865
3150
|
flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
|
|
3151
|
+
language: configByFile.language,
|
|
2866
3152
|
configFileTargets: extractConfigFileTargets(configByFile.targets)
|
|
2867
3153
|
});
|
|
2868
3154
|
}
|
|
@@ -5043,7 +5329,7 @@ function collectPollutionKeyPaths({ node, path, found }) {
|
|
|
5043
5329
|
* configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
|
|
5044
5330
|
* `JSON.parse` on files that may contain comments or trailing commas.
|
|
5045
5331
|
*/
|
|
5046
|
-
function parseJsonc$
|
|
5332
|
+
function parseJsonc$6(content) {
|
|
5047
5333
|
return deepSanitize(parseStrict(content));
|
|
5048
5334
|
}
|
|
5049
5335
|
/**
|
|
@@ -7216,7 +7502,7 @@ function warnAboutDroppedKeys({ removed, sourcePath, logger }) {
|
|
|
7216
7502
|
* are removed here instead, and reported.
|
|
7217
7503
|
*/
|
|
7218
7504
|
function withoutBlankPermissionKeys({ fileContent, sourcePath, logger }) {
|
|
7219
|
-
const parsed = parseJsonc$
|
|
7505
|
+
const parsed = parseJsonc$6(fileContent);
|
|
7220
7506
|
if (!isRecord$1(parsed)) return fileContent;
|
|
7221
7507
|
const { config, removed } = stripBlankPermissionKeys(parsed);
|
|
7222
7508
|
if (removed.patterns.size === 0 && removed.categories.size === 0) return fileContent;
|
|
@@ -7266,7 +7552,80 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
|
7266
7552
|
hermesagent: "hermes"
|
|
7267
7553
|
};
|
|
7268
7554
|
//#endregion
|
|
7555
|
+
//#region src/utils/glob-static-prefix.ts
|
|
7556
|
+
/**
|
|
7557
|
+
* Characters that make a path segment a pattern rather than a literal name.
|
|
7558
|
+
*
|
|
7559
|
+
* `(` `)` and `!` are included because extglob syntax (`+(a|b)`, `!(x)`) uses
|
|
7560
|
+
* them; treating a segment that carries one as static could name a directory
|
|
7561
|
+
* that no matched file actually lives under.
|
|
7562
|
+
*/
|
|
7563
|
+
const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
|
|
7564
|
+
/**
|
|
7565
|
+
* The directory every file matched by `glob` lives under, or `undefined` when
|
|
7566
|
+
* the pattern pins down no such directory.
|
|
7567
|
+
*
|
|
7568
|
+
* The result is the longest leading run of POSIX path segments that contain no
|
|
7569
|
+
* glob metacharacter, minus the final segment when the pattern ends in one that
|
|
7570
|
+
* is static: `packages/api/**\/*.ts` yields `packages/api`, while
|
|
7571
|
+
* `packages/api/README.md` names a file and yields `packages/api` as well, and
|
|
7572
|
+
* a bare `README.md` yields nothing. A leading `./` is stripped. The following
|
|
7573
|
+
* patterns are rejected outright because the directory they would name is
|
|
7574
|
+
* either not inside the output root or not a single directory at all:
|
|
7575
|
+
*
|
|
7576
|
+
* - negation patterns (`!packages/**`), which exclude rather than select;
|
|
7577
|
+
* - absolute paths (`/etc/**`, `C:/x/**`) and backslash-separated patterns;
|
|
7578
|
+
* - patterns with a `..` segment anywhere;
|
|
7579
|
+
* - patterns with a brace expansion or any other metacharacter in their first
|
|
7580
|
+
* segment (`{a,b}/**`), which have no static prefix.
|
|
7581
|
+
*
|
|
7582
|
+
* Brace expansion elsewhere ends the prefix without rejecting the pattern:
|
|
7583
|
+
* `packages/{api,web}/**` yields `packages`, the directory both alternatives
|
|
7584
|
+
* share, exactly as `packages/*\/**` would.
|
|
7585
|
+
*/
|
|
7586
|
+
function getGlobStaticPrefix(glob) {
|
|
7587
|
+
if (glob.startsWith("!") || glob.includes("\\")) return;
|
|
7588
|
+
let pattern = glob;
|
|
7589
|
+
while (pattern.startsWith("./")) pattern = pattern.slice(2);
|
|
7590
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern)) return;
|
|
7591
|
+
const segments = pattern.split("/");
|
|
7592
|
+
if (segments.includes("..")) return;
|
|
7593
|
+
const lastIndex = segments.length - 1;
|
|
7594
|
+
const prefix = [];
|
|
7595
|
+
for (const [index, segment] of segments.entries()) {
|
|
7596
|
+
if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
|
|
7597
|
+
if (segment === ".") continue;
|
|
7598
|
+
if (index === lastIndex) break;
|
|
7599
|
+
prefix.push(segment);
|
|
7600
|
+
}
|
|
7601
|
+
return prefix.length > 0 ? prefix.join("/") : void 0;
|
|
7602
|
+
}
|
|
7603
|
+
/**
|
|
7604
|
+
* The single directory every one of `globs` scopes, or `undefined` when the
|
|
7605
|
+
* list is empty, any pattern yields no prefix, or the patterns disagree.
|
|
7606
|
+
*
|
|
7607
|
+
* Several globs derive a directory only when each of them yields the same one:
|
|
7608
|
+
* picking the first pattern's prefix, or the common ancestor of all of them,
|
|
7609
|
+
* would place the rule somewhere the author never named.
|
|
7610
|
+
*/
|
|
7611
|
+
function getGlobsStaticPrefix(globs) {
|
|
7612
|
+
let shared;
|
|
7613
|
+
for (const glob of globs) {
|
|
7614
|
+
const prefix = getGlobStaticPrefix(glob);
|
|
7615
|
+
if (prefix === void 0 || shared !== void 0 && prefix !== shared) return;
|
|
7616
|
+
shared = prefix;
|
|
7617
|
+
}
|
|
7618
|
+
return shared;
|
|
7619
|
+
}
|
|
7620
|
+
//#endregion
|
|
7269
7621
|
//#region src/features/rules/rulesync-rule.ts
|
|
7622
|
+
/**
|
|
7623
|
+
* The `agentsmd.subprojectPath` value that asks for the path to be derived from
|
|
7624
|
+
* the rule's `globs` (see {@link resolveSubprojectPath}). It is a request, not a
|
|
7625
|
+
* path: the constructor replaces it with the derived directory, or drops it,
|
|
7626
|
+
* before any consumer reads the frontmatter.
|
|
7627
|
+
*/
|
|
7628
|
+
const AUTO_SUBPROJECT_PATH = "auto";
|
|
7270
7629
|
const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
7271
7630
|
root: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7272
7631
|
localRoot: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
@@ -7318,10 +7677,90 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
7318
7677
|
facet: zod_mini.z.optional(zod_mini.z.enum(["policies", "output-contracts"]))
|
|
7319
7678
|
}))
|
|
7320
7679
|
});
|
|
7680
|
+
/**
|
|
7681
|
+
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
7682
|
+
* that no target has to know how it came about:
|
|
7683
|
+
*
|
|
7684
|
+
* 1. an explicit directory in the frontmatter wins, and an explicit `""` is an
|
|
7685
|
+
* opt-out: the rule keeps its default placement and nothing is derived;
|
|
7686
|
+
* 2. otherwise, when the rule says `"auto"` or `deriveFromGlobs` is on, the
|
|
7687
|
+
* directory the rule's `globs` share (see `getGlobsStaticPrefix`);
|
|
7688
|
+
* 3. otherwise none, which keeps the rule in the target's modular directory.
|
|
7689
|
+
*
|
|
7690
|
+
* A root rule never nests, so it never derives. When a derivation yields
|
|
7691
|
+
* nothing the rule falls back to step 3, and only a rule that asked with
|
|
7692
|
+
* `"auto"` is told, once: it named a placement it did not get, so a warning
|
|
7693
|
+
* names the file to fix (an error would stop every other rule from
|
|
7694
|
+
* generating). The config option applies to every non-root rule, most of
|
|
7695
|
+
* which are general guidance whose globs, if any, were written as activation
|
|
7696
|
+
* hints (`["src/**\/*.ts", "test/**\/*.ts"]`) rather than as a directory;
|
|
7697
|
+
* warning about each of those on every generate would drown out real ones,
|
|
7698
|
+
* so config-driven derivation falls back silently.
|
|
7699
|
+
*/
|
|
7700
|
+
function resolveSubprojectPath({ frontmatter, deriveFromGlobs, rulePath }) {
|
|
7701
|
+
const authored = frontmatter.agentsmd?.subprojectPath;
|
|
7702
|
+
if (authored === "") return;
|
|
7703
|
+
if (typeof authored === "string" && authored !== "auto") return authored;
|
|
7704
|
+
const requested = authored === AUTO_SUBPROJECT_PATH;
|
|
7705
|
+
if (!requested && !deriveFromGlobs) return;
|
|
7706
|
+
if (frontmatter.root) {
|
|
7707
|
+
if (requested) warnOnceWithFallback(void 0, `Ignoring agentsmd.subprojectPath: "${AUTO_SUBPROJECT_PATH}" on the root rule ${rulePath}: a root rule is never written as a nested AGENTS.md.`);
|
|
7708
|
+
return;
|
|
7709
|
+
}
|
|
7710
|
+
const globs = Array.isArray(frontmatter.globs) ? frontmatter.globs : [];
|
|
7711
|
+
const derived = getGlobsStaticPrefix(globs);
|
|
7712
|
+
if (derived === void 0 && requested) warnOnceWithFallback(void 0, `Could not derive agentsmd.subprojectPath for ${rulePath} from globs ${JSON.stringify(globs)}: every glob must start with the same wildcard-free directory (e.g. "packages/api/**/*"). The rule is generated without a nested AGENTS.md; set agentsmd.subprojectPath explicitly to nest it.`);
|
|
7713
|
+
return derived;
|
|
7714
|
+
}
|
|
7715
|
+
/**
|
|
7716
|
+
* `frontmatter` with `agentsmd.subprojectPath` replaced by its resolved value,
|
|
7717
|
+
* or removed when there is none, so the `"auto"` request never reaches a
|
|
7718
|
+
* consumer as if it were a directory name. An `agentsmd` block that held
|
|
7719
|
+
* nothing but the request goes with it, so a consumer sees the same shape it
|
|
7720
|
+
* would for a rule that never mentioned `agentsmd`. An authored `""` is left
|
|
7721
|
+
* as written: every consumer already reads it as "no nesting".
|
|
7722
|
+
*/
|
|
7723
|
+
function withResolvedSubprojectPath({ frontmatter, deriveFromGlobs, rulePath }) {
|
|
7724
|
+
const authored = frontmatter.agentsmd?.subprojectPath;
|
|
7725
|
+
const resolved = resolveSubprojectPath({
|
|
7726
|
+
frontmatter,
|
|
7727
|
+
deriveFromGlobs,
|
|
7728
|
+
rulePath
|
|
7729
|
+
});
|
|
7730
|
+
if (resolved === authored || resolved === void 0 && authored !== "auto") return frontmatter;
|
|
7731
|
+
const { subprojectPath: _authored, ...agentsmd } = frontmatter.agentsmd ?? {};
|
|
7732
|
+
if (resolved !== void 0) return {
|
|
7733
|
+
...frontmatter,
|
|
7734
|
+
agentsmd: {
|
|
7735
|
+
...agentsmd,
|
|
7736
|
+
subprojectPath: resolved
|
|
7737
|
+
}
|
|
7738
|
+
};
|
|
7739
|
+
if (Object.keys(agentsmd).length === 0) {
|
|
7740
|
+
const { agentsmd: _empty, ...rest } = frontmatter;
|
|
7741
|
+
return rest;
|
|
7742
|
+
}
|
|
7743
|
+
return {
|
|
7744
|
+
...frontmatter,
|
|
7745
|
+
agentsmd
|
|
7746
|
+
};
|
|
7747
|
+
}
|
|
7321
7748
|
var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
7749
|
+
/**
|
|
7750
|
+
* The frontmatter consumers read. It differs from what the file says in one
|
|
7751
|
+
* place: `agentsmd.subprojectPath` holds the resolved directory (see
|
|
7752
|
+
* `resolveSubprojectPath`), while `authoredFrontmatter` and
|
|
7753
|
+
* `getFileContent()` keep the authored value, so a rule written back out
|
|
7754
|
+
* still says `"auto"`.
|
|
7755
|
+
*/
|
|
7322
7756
|
frontmatter;
|
|
7757
|
+
/**
|
|
7758
|
+
* The frontmatter as written, after schema defaults but before
|
|
7759
|
+
* `agentsmd.subprojectPath` resolution: what `getFileContent()` serializes.
|
|
7760
|
+
*/
|
|
7761
|
+
authoredFrontmatter;
|
|
7323
7762
|
body;
|
|
7324
|
-
constructor({ frontmatter, body, ...rest }) {
|
|
7763
|
+
constructor({ frontmatter, body, deriveSubprojectPathFromGlobs = false, ...rest }) {
|
|
7325
7764
|
const parseResult = RulesyncRuleFrontmatterSchema.safeParse(frontmatter);
|
|
7326
7765
|
if (!parseResult.success && rest.validate !== false) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(parseResult.error)}`);
|
|
7327
7766
|
const parsedFrontmatter = parseResult.success ? parseResult.data : {
|
|
@@ -7332,7 +7771,12 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7332
7771
|
...rest,
|
|
7333
7772
|
fileContent: stringifyFrontmatter(body, parsedFrontmatter)
|
|
7334
7773
|
});
|
|
7335
|
-
this.
|
|
7774
|
+
this.authoredFrontmatter = parsedFrontmatter;
|
|
7775
|
+
this.frontmatter = withResolvedSubprojectPath({
|
|
7776
|
+
frontmatter: parsedFrontmatter,
|
|
7777
|
+
deriveFromGlobs: deriveSubprojectPathFromGlobs,
|
|
7778
|
+
rulePath: (0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)
|
|
7779
|
+
});
|
|
7336
7780
|
this.body = body;
|
|
7337
7781
|
}
|
|
7338
7782
|
static getSettablePaths() {
|
|
@@ -7341,9 +7785,23 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7341
7785
|
legacy: { relativeDirPath: RULESYNC_RELATIVE_DIR_PATH }
|
|
7342
7786
|
};
|
|
7343
7787
|
}
|
|
7788
|
+
/**
|
|
7789
|
+
* The frontmatter to act on: `agentsmd.subprojectPath` is the resolved
|
|
7790
|
+
* placement, never `"auto"`.
|
|
7791
|
+
*/
|
|
7344
7792
|
getFrontmatter() {
|
|
7345
7793
|
return this.frontmatter;
|
|
7346
7794
|
}
|
|
7795
|
+
/**
|
|
7796
|
+
* The frontmatter as the file states it, `agentsmd.subprojectPath: "auto"`
|
|
7797
|
+
* included. This is the view to hand back to whoever edits the file (the
|
|
7798
|
+
* MCP rule tools): returning the resolved placement instead would make a
|
|
7799
|
+
* get → edit → put round trip hardcode the derived directory, or drop the
|
|
7800
|
+
* request when nothing could be derived.
|
|
7801
|
+
*/
|
|
7802
|
+
getAuthoredFrontmatter() {
|
|
7803
|
+
return this.authoredFrontmatter;
|
|
7804
|
+
}
|
|
7347
7805
|
validate() {
|
|
7348
7806
|
if (!this.frontmatter) return {
|
|
7349
7807
|
success: true,
|
|
@@ -7359,7 +7817,7 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7359
7817
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
7360
7818
|
};
|
|
7361
7819
|
}
|
|
7362
|
-
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
7820
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, deriveSubprojectPathFromGlobs = false }) {
|
|
7363
7821
|
const dirPath = relativeDirPath ?? this.getSettablePaths().recommended.relativeDirPath;
|
|
7364
7822
|
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
7365
7823
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
@@ -7378,7 +7836,8 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7378
7836
|
relativeFilePath,
|
|
7379
7837
|
frontmatter: validatedFrontmatter,
|
|
7380
7838
|
body: content.trim(),
|
|
7381
|
-
validate
|
|
7839
|
+
validate,
|
|
7840
|
+
deriveSubprojectPathFromGlobs
|
|
7382
7841
|
});
|
|
7383
7842
|
}
|
|
7384
7843
|
getBody() {
|
|
@@ -7406,6 +7865,21 @@ function containsPathSeparator(name) {
|
|
|
7406
7865
|
return name.includes("/") || name.includes("\\");
|
|
7407
7866
|
}
|
|
7408
7867
|
/**
|
|
7868
|
+
* The permission bits worth carrying from a source file: only an executable
|
|
7869
|
+
* one has a mode the copy must keep, and only its read and execute bits plus
|
|
7870
|
+
* the owner's write bit are kept. Group and world write bits are dropped the
|
|
7871
|
+
* way a umask would drop them, so a source sitting on a mount that reports
|
|
7872
|
+
* everything as 0777 does not turn the generated copy world-writable; setuid,
|
|
7873
|
+
* setgid and sticky are never carried. Windows reports no executable bit, so
|
|
7874
|
+
* nothing is carried there and the copy takes the platform default, as it
|
|
7875
|
+
* always has.
|
|
7876
|
+
*/
|
|
7877
|
+
function carriedFileMode(mode) {
|
|
7878
|
+
const permissionBits = mode & 493;
|
|
7879
|
+
if ((permissionBits & 73) === 0) return {};
|
|
7880
|
+
return { fileMode: permissionBits };
|
|
7881
|
+
}
|
|
7882
|
+
/**
|
|
7409
7883
|
* Directories that hold credentials. Excluding these protects something, so
|
|
7410
7884
|
* their exclusion is reported rather than silent.
|
|
7411
7885
|
*/
|
|
@@ -8119,6 +8593,7 @@ var AiDir = class AiDir {
|
|
|
8119
8593
|
* files says so rather than generating a directory that is quietly short.
|
|
8120
8594
|
*/
|
|
8121
8595
|
static warnOnCarriedWalkLimits({ reportedDirPath, truncations, unreadablePaths }) {
|
|
8596
|
+
if (truncations.size > 0 || unreadablePaths.length > 0) recordIncompleteCarriedFiles();
|
|
8122
8597
|
if (truncations.has("depth")) warnOnceWithFallback(void 0, `Not carrying the entries more than 12 directories below ${reportedDirPath}: a skill directory is walked to that depth only. A symbolic link that reaches a large tree is the usual cause.`);
|
|
8123
8598
|
if (truncations.has("count")) warnOnceWithFallback(void 0, `Not carrying the entries under ${reportedDirPath} beyond the first ${MAX_CARRIED_FILES}: a directory may carry at most that many files. A symbolic link that reaches a large tree is the usual cause.`);
|
|
8124
8599
|
if (truncations.has("directories")) warnOnceWithFallback(void 0, `Not carrying the entries under ${reportedDirPath} below the first ${MAX_CARRIED_DIRECTORIES} directories: a directory may carry files from at most that many directories. A symbolic link that reaches a large tree is the usual cause.`);
|
|
@@ -8217,12 +8692,15 @@ var AiDir = class AiDir {
|
|
|
8217
8692
|
try {
|
|
8218
8693
|
fileHandle = await (0, node_fs_promises.open)(classifiedPath, node_fs.constants.O_RDONLY | (node_fs.constants.O_NOFOLLOW ?? 0));
|
|
8219
8694
|
} catch (error) {
|
|
8695
|
+
recordIncompleteCarriedFiles();
|
|
8220
8696
|
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
8221
8697
|
continue;
|
|
8222
8698
|
}
|
|
8223
8699
|
try {
|
|
8224
|
-
const
|
|
8700
|
+
const fileStats = await fileHandle.stat();
|
|
8701
|
+
const fileSize = fileStats.size;
|
|
8225
8702
|
if (carriedBytes + fileSize > 104857600) {
|
|
8703
|
+
recordIncompleteCarriedFiles();
|
|
8226
8704
|
warnOnceWithFallback(void 0, `Not carrying ${filteredPaths.length - index} of the ${filteredPaths.length} entries under ${reportedDirPath}: a directory may carry at most ${MAX_CARRIED_BYTES / 1024 / 1024}MB. A symbolic link that reaches a large tree is the usual cause.`);
|
|
8227
8705
|
break;
|
|
8228
8706
|
}
|
|
@@ -8230,9 +8708,11 @@ var AiDir = class AiDir {
|
|
|
8230
8708
|
carriedBytes += fileBuffer.byteLength;
|
|
8231
8709
|
files.push({
|
|
8232
8710
|
relativeFilePathToDirPath: (0, node_path.relative)(dirPath, filePath),
|
|
8233
|
-
fileBuffer
|
|
8711
|
+
fileBuffer,
|
|
8712
|
+
...carriedFileMode(fileStats.mode)
|
|
8234
8713
|
});
|
|
8235
8714
|
} catch (error) {
|
|
8715
|
+
recordIncompleteCarriedFiles();
|
|
8236
8716
|
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
8237
8717
|
} finally {
|
|
8238
8718
|
await fileHandle.close();
|
|
@@ -8265,6 +8745,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8265
8745
|
targets: zod_mini.z._default(RulesyncTargetsSchema, ["*"]),
|
|
8266
8746
|
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8267
8747
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8748
|
+
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
8749
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
8750
|
+
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
8268
8751
|
claudecode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8269
8752
|
when_to_use: zod_mini.z.optional(zod_mini.z.string()),
|
|
8270
8753
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
|
|
@@ -8314,7 +8797,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
8314
8797
|
kilo: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8315
8798
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8316
8799
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
8317
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
8800
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
8318
8801
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
8319
8802
|
})),
|
|
8320
8803
|
kiro: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -8691,6 +9174,61 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
8691
9174
|
function resolveUserInvocable({ rootFrontmatter, section }) {
|
|
8692
9175
|
return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
|
|
8693
9176
|
}
|
|
9177
|
+
/**
|
|
9178
|
+
* Resolve the effective `license` value for a tool skill.
|
|
9179
|
+
*
|
|
9180
|
+
* The rulesync skill frontmatter exposes a root-level `license` default that
|
|
9181
|
+
* applies to every tool modelling the Agent Skills standard field; the list of
|
|
9182
|
+
* those tools lives in `docs/reference/file-formats.md`. Each tool's own
|
|
9183
|
+
* section may override that default with a per-target value. A defined section
|
|
9184
|
+
* value always wins over the root default.
|
|
9185
|
+
*
|
|
9186
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9187
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9188
|
+
*
|
|
9189
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9190
|
+
*/
|
|
9191
|
+
function resolveLicense({ rootFrontmatter, section }) {
|
|
9192
|
+
return section?.license ?? rootFrontmatter.license;
|
|
9193
|
+
}
|
|
9194
|
+
/**
|
|
9195
|
+
* Resolve the effective `compatibility` value for a tool skill.
|
|
9196
|
+
*
|
|
9197
|
+
* The rulesync skill frontmatter exposes a root-level `compatibility` default
|
|
9198
|
+
* that applies to every tool modelling the Agent Skills standard field; the
|
|
9199
|
+
* list of those tools lives in `docs/reference/file-formats.md`. Each tool's
|
|
9200
|
+
* own section may override that default with a per-target value. A defined
|
|
9201
|
+
* section value always wins over the root default.
|
|
9202
|
+
*
|
|
9203
|
+
* The root value keeps the rulesync shape (a string, or the legacy object
|
|
9204
|
+
* form); any per-target normalization — such as the Agent Skills spec's string
|
|
9205
|
+
* coercion — stays with the adapter.
|
|
9206
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9207
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9208
|
+
*
|
|
9209
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9210
|
+
*/
|
|
9211
|
+
function resolveCompatibility({ rootFrontmatter, section }) {
|
|
9212
|
+
return section?.compatibility ?? rootFrontmatter.compatibility;
|
|
9213
|
+
}
|
|
9214
|
+
/**
|
|
9215
|
+
* Resolve the effective `metadata` value for a tool skill.
|
|
9216
|
+
*
|
|
9217
|
+
* The rulesync skill frontmatter exposes a root-level `metadata` default that
|
|
9218
|
+
* applies to every tool modelling the Agent Skills standard field; the list of
|
|
9219
|
+
* those tools lives in `docs/reference/file-formats.md`. Each tool's own
|
|
9220
|
+
* section may override that default with a per-target value. A defined section
|
|
9221
|
+
* value always wins over the root default; the two maps are never merged key
|
|
9222
|
+
* by key.
|
|
9223
|
+
*
|
|
9224
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9225
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9226
|
+
*
|
|
9227
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9228
|
+
*/
|
|
9229
|
+
function resolveMetadata({ rootFrontmatter, section }) {
|
|
9230
|
+
return section?.metadata ?? rootFrontmatter.metadata;
|
|
9231
|
+
}
|
|
8694
9232
|
//#endregion
|
|
8695
9233
|
//#region src/constants/augmentcode-paths.ts
|
|
8696
9234
|
const AUGMENTCODE_DIR = ".augment";
|
|
@@ -10635,6 +11173,40 @@ var RovodevCheck = class extends AggregatedToolCheck {
|
|
|
10635
11173
|
}
|
|
10636
11174
|
};
|
|
10637
11175
|
//#endregion
|
|
11176
|
+
//#region src/constants/claudecode-paths.ts
|
|
11177
|
+
/**
|
|
11178
|
+
* Claude Code configuration-layout conventions.
|
|
11179
|
+
*
|
|
11180
|
+
* Single source of truth for where Claude Code expects its files
|
|
11181
|
+
* (directories, file names, scope-specific paths). Every feature module
|
|
11182
|
+
* (rules, commands, skills, subagents, ignore, mcp, permissions, hooks)
|
|
11183
|
+
* and the gitignore entry registry import from here, so a change in the
|
|
11184
|
+
* Claude Code conventions is a change to this file only.
|
|
11185
|
+
*/
|
|
11186
|
+
/** Root directory for Claude Code configuration, relative to the scope root. */
|
|
11187
|
+
const CLAUDECODE_DIR = ".claude";
|
|
11188
|
+
const CLAUDECODE_RULE_FILE_NAME = "CLAUDE.md";
|
|
11189
|
+
const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
|
|
11190
|
+
/** Modular rules directory name under `.claude/` (current format). */
|
|
11191
|
+
const CLAUDECODE_RULES_DIR_NAME = "rules";
|
|
11192
|
+
/** Memories directory name under `.claude/` (legacy format). */
|
|
11193
|
+
const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
|
|
11194
|
+
const CLAUDECODE_COMMANDS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "commands");
|
|
11195
|
+
const CLAUDECODE_AGENTS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "agents");
|
|
11196
|
+
const CLAUDECODE_SKILLS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "skills");
|
|
11197
|
+
const CLAUDECODE_SCHEDULED_TASKS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "scheduled-tasks");
|
|
11198
|
+
const CLAUDECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
11199
|
+
const CLAUDECODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
11200
|
+
/**
|
|
11201
|
+
* The published JSON Schema for both settings files (SchemaStore). Written as
|
|
11202
|
+
* the `$schema` key of every settings file rulesync generates, when the file
|
|
11203
|
+
* does not already state one, so editors offer completion, hover
|
|
11204
|
+
* documentation and validation for the keys around the ones rulesync writes.
|
|
11205
|
+
*/
|
|
11206
|
+
const CLAUDECODE_SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
|
|
11207
|
+
const CLAUDECODE_MCP_FILE_NAME = ".mcp.json";
|
|
11208
|
+
const CLAUDECODE_GLOBAL_MCP_FILE_NAME = ".claude.json";
|
|
11209
|
+
//#endregion
|
|
10638
11210
|
//#region src/constants/codexcli-paths.ts
|
|
10639
11211
|
const CODEXCLI_DIR = ".codex";
|
|
10640
11212
|
const CODEXCLI_PROMPTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "prompts");
|
|
@@ -10655,6 +11227,52 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
10655
11227
|
"tui"
|
|
10656
11228
|
];
|
|
10657
11229
|
//#endregion
|
|
11230
|
+
//#region src/utils/quote-value.ts
|
|
11231
|
+
/**
|
|
11232
|
+
* How much of a value read off disk a diagnostic quotes.
|
|
11233
|
+
*
|
|
11234
|
+
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
11235
|
+
* offending value so the reader can find it, but the values these warnings
|
|
11236
|
+
* quote come from files rulesync did not write — a tool's own settings, a
|
|
11237
|
+
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
11238
|
+
* no longer stop at a terminal: they travel into a `--json` document another
|
|
11239
|
+
* program parses and into an MCP result an agent reads as context. A command
|
|
11240
|
+
* line or a header is the shape most likely to carry a credential, and a long
|
|
11241
|
+
* value is the shape most likely to carry instructions aimed at the agent.
|
|
11242
|
+
*/
|
|
11243
|
+
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
11244
|
+
/**
|
|
11245
|
+
* A short, quotable rendering of a value for a diagnostic.
|
|
11246
|
+
*
|
|
11247
|
+
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
11248
|
+
* crafted one read as a second line; stripped of the control characters
|
|
11249
|
+
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
11250
|
+
* bidirectional overrides); and truncated.
|
|
11251
|
+
*/
|
|
11252
|
+
function quoteValueForWarning(value) {
|
|
11253
|
+
return truncateText({
|
|
11254
|
+
text: stripControlCharacters(serialize(value)),
|
|
11255
|
+
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
11256
|
+
suffix: "…(truncated)"
|
|
11257
|
+
});
|
|
11258
|
+
}
|
|
11259
|
+
function serialize(value) {
|
|
11260
|
+
try {
|
|
11261
|
+
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
11262
|
+
} catch {
|
|
11263
|
+
return `[unserializable ${typeof value}]`;
|
|
11264
|
+
}
|
|
11265
|
+
}
|
|
11266
|
+
/**
|
|
11267
|
+
* Strip the control characters out of every string before `JSON.stringify`
|
|
11268
|
+
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
11269
|
+
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
11270
|
+
* over the output can recognize as a control character again.
|
|
11271
|
+
*/
|
|
11272
|
+
function stripStrings(_key, value) {
|
|
11273
|
+
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
11274
|
+
}
|
|
11275
|
+
//#endregion
|
|
10658
11276
|
//#region src/features/shared/shared-config-gateway.ts
|
|
10659
11277
|
/**
|
|
10660
11278
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
@@ -10790,39 +11408,35 @@ function detectJsoncFormattingOptions({ text, root }) {
|
|
|
10790
11408
|
eol
|
|
10791
11409
|
};
|
|
10792
11410
|
}
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
* it said before. For an owned key that is a silent ownership failure: a
|
|
10803
|
-
* `deny` rulesync just wrote would sit above the `allow` the tool reads.
|
|
10804
|
-
* - `__proto__`, `constructor` or `prototype`. None survives into the parsed
|
|
10805
|
-
* document — a nested one is dropped, a root-level `__proto__` replaces the
|
|
10806
|
-
* root's prototype — so an edit-based write would find no difference to
|
|
10807
|
-
* apply and leave the key in the file.
|
|
10808
|
-
*
|
|
10809
|
-
* The whole-document writer resolves duplicates last-wins and drops pollution
|
|
10810
|
-
* keys, which is what it has always done, so those files go to it.
|
|
10811
|
-
*/
|
|
10812
|
-
function statesUneditableKeys(node) {
|
|
10813
|
-
if (node.type === "array") return (node.children ?? []).some((child) => statesUneditableKeys(child));
|
|
10814
|
-
if (node.type !== "object") return false;
|
|
11411
|
+
function uneditableKeyOf(node) {
|
|
11412
|
+
if (node.type === "array") {
|
|
11413
|
+
for (const child of node.children ?? []) {
|
|
11414
|
+
const found = uneditableKeyOf(child);
|
|
11415
|
+
if (found !== void 0) return found;
|
|
11416
|
+
}
|
|
11417
|
+
return;
|
|
11418
|
+
}
|
|
11419
|
+
if (node.type !== "object") return void 0;
|
|
10815
11420
|
const seen = /* @__PURE__ */ new Set();
|
|
10816
11421
|
for (const property of node.children ?? []) {
|
|
10817
11422
|
const key = property.children?.[0]?.value;
|
|
10818
11423
|
if (typeof key === "string") {
|
|
10819
|
-
if (
|
|
11424
|
+
if (isPrototypePollutionKey(key)) return {
|
|
11425
|
+
kind: "prototype-pollution",
|
|
11426
|
+
key
|
|
11427
|
+
};
|
|
11428
|
+
if (seen.has(key)) return {
|
|
11429
|
+
kind: "duplicate",
|
|
11430
|
+
key
|
|
11431
|
+
};
|
|
10820
11432
|
seen.add(key);
|
|
10821
11433
|
}
|
|
10822
11434
|
const value = property.children?.[1];
|
|
10823
|
-
if (value !== void 0
|
|
11435
|
+
if (value !== void 0) {
|
|
11436
|
+
const found = uneditableKeyOf(value);
|
|
11437
|
+
if (found !== void 0) return found;
|
|
11438
|
+
}
|
|
10824
11439
|
}
|
|
10825
|
-
return false;
|
|
10826
11440
|
}
|
|
10827
11441
|
/**
|
|
10828
11442
|
* The offset just past the whitespace and comments starting at `from`.
|
|
@@ -11003,7 +11617,8 @@ function notesAt({ text, from }) {
|
|
|
11003
11617
|
/**
|
|
11004
11618
|
* Detach the notes written at the point where the object at `path` will take a
|
|
11005
11619
|
* new key: after its last property (and around the comma a trailing-comma file
|
|
11006
|
-
* spells there), or just inside the `{` when it has no properties yet
|
|
11620
|
+
* spells there), or just inside the `{` when it has no properties yet or when
|
|
11621
|
+
* the key is to be written first (`at: "start"`).
|
|
11007
11622
|
*
|
|
11008
11623
|
* `modify` computes its insert from exactly that point — in front of a note
|
|
11009
11624
|
* written there — so applying the edit unchanged re-emits the note *after* the
|
|
@@ -11016,11 +11631,11 @@ function notesAt({ text, from }) {
|
|
|
11016
11631
|
*
|
|
11017
11632
|
* Returns `undefined` when there is no such note, which is the common case.
|
|
11018
11633
|
*/
|
|
11019
|
-
function
|
|
11634
|
+
function detachInsertionNote({ text, path, at }) {
|
|
11020
11635
|
const root = (0, jsonc_parser.parseTree)(text, [], { allowTrailingComma: true });
|
|
11021
11636
|
const object = root === void 0 ? void 0 : (0, jsonc_parser.findNodeAtLocation)(root, [...path]);
|
|
11022
11637
|
if (object?.type !== "object") return void 0;
|
|
11023
|
-
const property = object.children?.at(-1);
|
|
11638
|
+
const property = at === "end" ? object.children?.at(-1) : void 0;
|
|
11024
11639
|
const anchorKey = property?.children?.[0]?.value;
|
|
11025
11640
|
if (property !== void 0 && typeof anchorKey !== "string") return void 0;
|
|
11026
11641
|
const spans = notesAt({
|
|
@@ -11037,7 +11652,7 @@ function detachTrailingNote({ text, path }) {
|
|
|
11037
11652
|
};
|
|
11038
11653
|
}
|
|
11039
11654
|
/**
|
|
11040
|
-
* Put a note detached by {@link
|
|
11655
|
+
* Put a note detached by {@link detachInsertionNote} back where it was: after
|
|
11041
11656
|
* the property it describes (behind the comma the insert gave that property),
|
|
11042
11657
|
* or just inside the `{` of the object it was written in when there was no
|
|
11043
11658
|
* property to describe. Returns `undefined` if that place can no longer be
|
|
@@ -11060,16 +11675,23 @@ function reattachTrailingNote({ text, path, anchorKey, note }) {
|
|
|
11060
11675
|
return text.slice(0, cursor) + note + text.slice(cursor);
|
|
11061
11676
|
}
|
|
11062
11677
|
/**
|
|
11063
|
-
* Write `value` at `[...path, key]`, keeping the
|
|
11064
|
-
*
|
|
11065
|
-
*
|
|
11066
|
-
*
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11678
|
+
* Write `value` at `[...path, key]`, keeping the note written at the point of
|
|
11679
|
+
* insertion where its author put it (see {@link detachInsertionNote}): the
|
|
11680
|
+
* trailing note of the property the new key is appended after, or — for a
|
|
11681
|
+
* `leading` key, which `modify` places before every other property — the note
|
|
11682
|
+
* written just inside the `{`. Replacing an existing key needs none of this:
|
|
11683
|
+
* `modify` rewrites the value's own span and leaves every comment where it is.
|
|
11684
|
+
*/
|
|
11685
|
+
function insertJsoncProperty({ text, path, key, value, options, leading = false }) {
|
|
11686
|
+
const modification = leading ? {
|
|
11687
|
+
...options,
|
|
11688
|
+
getInsertionIndex: () => 0
|
|
11689
|
+
} : options;
|
|
11690
|
+
const write = (source) => (0, jsonc_parser.applyEdits)(source, (0, jsonc_parser.modify)(source, [...path, key], value, modification));
|
|
11691
|
+
const detached = detachInsertionNote({
|
|
11071
11692
|
text,
|
|
11072
|
-
path
|
|
11693
|
+
path,
|
|
11694
|
+
at: leading ? "start" : "end"
|
|
11073
11695
|
});
|
|
11074
11696
|
if (detached === void 0) return write(text);
|
|
11075
11697
|
return reattachTrailingNote({
|
|
@@ -11189,7 +11811,7 @@ function countJsoncEdits({ base, next, depth }) {
|
|
|
11189
11811
|
* large enough and enough of it changing, the product of the two is what
|
|
11190
11812
|
* {@link JSONC_EDIT_BUDGET_BYTES} keeps off this path.
|
|
11191
11813
|
*/
|
|
11192
|
-
function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
11814
|
+
function applyJsoncObjectEdits({ text, base, next, path, options, leadingKeys }) {
|
|
11193
11815
|
let result = text;
|
|
11194
11816
|
for (const [key, value] of Object.entries(next)) {
|
|
11195
11817
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -11208,7 +11830,8 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11208
11830
|
base: previous,
|
|
11209
11831
|
next: value,
|
|
11210
11832
|
path: [...path, key],
|
|
11211
|
-
options
|
|
11833
|
+
options,
|
|
11834
|
+
leadingKeys: []
|
|
11212
11835
|
});
|
|
11213
11836
|
continue;
|
|
11214
11837
|
}
|
|
@@ -11222,7 +11845,8 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11222
11845
|
path,
|
|
11223
11846
|
key,
|
|
11224
11847
|
value,
|
|
11225
|
-
options
|
|
11848
|
+
options,
|
|
11849
|
+
leading: leadingKeys.includes(key)
|
|
11226
11850
|
});
|
|
11227
11851
|
}
|
|
11228
11852
|
for (const key of Object.keys(base)) if (!Object.hasOwn(next, key)) result = removeJsoncProperty({
|
|
@@ -11252,7 +11876,7 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11252
11876
|
* here have already decided (via `invalidRootPolicy`) that such a file is
|
|
11253
11877
|
* replaced;
|
|
11254
11878
|
* - a file stating the same key twice, or using `__proto__`, `constructor` or
|
|
11255
|
-
* `prototype` as a key (see {@link
|
|
11879
|
+
* `prototype` as a key (see {@link uneditableKeyOf});
|
|
11256
11880
|
* - a file so large, with so much of it changing, that editing it key by key
|
|
11257
11881
|
* would take longer than a user would wait (see
|
|
11258
11882
|
* {@link JSONC_EDIT_BUDGET_BYTES}), or changed keys that write more new text
|
|
@@ -11260,26 +11884,54 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11260
11884
|
* {@link JSONC_EDIT_WRITTEN_BYTES});
|
|
11261
11885
|
* - a file the editor itself refuses, which it answers with an exception
|
|
11262
11886
|
* rather than a result.
|
|
11887
|
+
*
|
|
11888
|
+
* Every one of those fallbacks except the empty file is reported once per run
|
|
11889
|
+
* through `logger` (or the fallback logger when none is given): the author's
|
|
11890
|
+
* comments are what the edit path exists to keep, and a `generate` that drops
|
|
11891
|
+
* them should say so rather than let the loss show up in the next diff.
|
|
11892
|
+
* `filePath` names the file in that warning; callers that know it should pass
|
|
11893
|
+
* it, since the once-per-run token is the message itself and two nameless
|
|
11894
|
+
* files would share one.
|
|
11895
|
+
*
|
|
11896
|
+
* `leadingKeys` names the root keys that go in front of every other property
|
|
11897
|
+
* when the edit path inserts them (the whole-document writer keeps the order
|
|
11898
|
+
* `document` states, so the caller places them there itself). A key the file
|
|
11899
|
+
* already has stays where the file put it.
|
|
11263
11900
|
*/
|
|
11264
|
-
function serializeSharedConfig({ format, document, existingContent }) {
|
|
11901
|
+
function serializeSharedConfig({ format, document, existingContent, leadingKeys = [], filePath, logger }) {
|
|
11265
11902
|
const whole = stringifySharedConfig({
|
|
11266
11903
|
format,
|
|
11267
11904
|
document
|
|
11268
11905
|
});
|
|
11269
11906
|
if (format !== "jsonc" || existingContent.trim() === "") return whole;
|
|
11907
|
+
const wholeBecause = (reason) => {
|
|
11908
|
+
warnAboutWholeJsoncRewrite({
|
|
11909
|
+
filePath,
|
|
11910
|
+
reason,
|
|
11911
|
+
logger
|
|
11912
|
+
});
|
|
11913
|
+
return whole;
|
|
11914
|
+
};
|
|
11270
11915
|
try {
|
|
11271
11916
|
const errors = [];
|
|
11272
11917
|
const root = (0, jsonc_parser.parseTree)(existingContent, errors, { allowTrailingComma: true });
|
|
11273
|
-
if (root === void 0 || errors.length > 0
|
|
11918
|
+
if (root === void 0 || errors.length > 0) return wholeBecause("it could not be parsed");
|
|
11919
|
+
if (root.type !== "object") return wholeBecause("its root is not an object");
|
|
11920
|
+
const uneditable = uneditableKeyOf(root);
|
|
11921
|
+
if (uneditable !== void 0) {
|
|
11922
|
+
const key = quoteValueForWarning(uneditable.key);
|
|
11923
|
+
return wholeBecause(uneditable.kind === "duplicate" ? `it states the key ${key} twice` : `it uses the key ${key}, which rulesync drops from every document it parses`);
|
|
11924
|
+
}
|
|
11274
11925
|
const base = sanitizeSharedConfigValue((0, jsonc_parser.getNodeValue)(root));
|
|
11275
|
-
if (!isPlainObject$1(base)) return
|
|
11926
|
+
if (!isPlainObject$1(base)) return wholeBecause("its root is not an object");
|
|
11276
11927
|
const span = Math.max(existingContent.length, whole.length);
|
|
11277
11928
|
const cost = countJsoncEdits({
|
|
11278
11929
|
base,
|
|
11279
11930
|
next: document,
|
|
11280
11931
|
depth: 0
|
|
11281
11932
|
});
|
|
11282
|
-
if (cost.written > JSONC_EDIT_WRITTEN_BYTES
|
|
11933
|
+
if (cost.written > JSONC_EDIT_WRITTEN_BYTES) return wholeBecause("its changed keys write more new text than editing in place can afford");
|
|
11934
|
+
if (cost.edits * span > JSONC_EDIT_BUDGET_BYTES) return wholeBecause("it is too large, with too much of it changing, to edit key by key");
|
|
11283
11935
|
return applyJsoncObjectEdits({
|
|
11284
11936
|
text: existingContent,
|
|
11285
11937
|
base,
|
|
@@ -11288,13 +11940,25 @@ function serializeSharedConfig({ format, document, existingContent }) {
|
|
|
11288
11940
|
options: { formattingOptions: detectJsoncFormattingOptions({
|
|
11289
11941
|
text: existingContent,
|
|
11290
11942
|
root
|
|
11291
|
-
}) }
|
|
11943
|
+
}) },
|
|
11944
|
+
leadingKeys
|
|
11292
11945
|
});
|
|
11293
|
-
} catch {
|
|
11294
|
-
return
|
|
11946
|
+
} catch (error) {
|
|
11947
|
+
return wholeBecause(`the editor refused it (${stripControlCharacters(formatError(error))})`);
|
|
11295
11948
|
}
|
|
11296
11949
|
}
|
|
11297
11950
|
/**
|
|
11951
|
+
* Report, once per run and file, that a JSONC file is being written whole
|
|
11952
|
+
* instead of edited in place. Named by `filePath` when the caller knows it;
|
|
11953
|
+
* a caller serializing text it never read from disk has no name to give.
|
|
11954
|
+
*/
|
|
11955
|
+
function warnAboutWholeJsoncRewrite({ filePath, reason, logger }) {
|
|
11956
|
+
const subject = filePath === void 0 ? "A shared JSONC config file" : quoteForLog(filePath);
|
|
11957
|
+
try {
|
|
11958
|
+
warnOnceWithFallback(logger, `${subject} is rewritten whole rather than edited in place because ${reason}; comments, blank lines and key order in the existing file are not preserved.`);
|
|
11959
|
+
} catch {}
|
|
11960
|
+
}
|
|
11961
|
+
/**
|
|
11298
11962
|
* Shallow merge: every top-level key in `patch` replaces the base key
|
|
11299
11963
|
* wholesale; all other base keys are preserved. The policy for a feature that
|
|
11300
11964
|
* owns a fixed set of top-level keys.
|
|
@@ -11332,6 +11996,7 @@ function mergeSharedConfigDeep({ base, patch }) {
|
|
|
11332
11996
|
return result;
|
|
11333
11997
|
}
|
|
11334
11998
|
const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
|
|
11999
|
+
const CLAUDE_SETTINGS_LOCAL_SHARED_FILE_KEY = ".claude/settings.local.json";
|
|
11335
12000
|
const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
|
|
11336
12001
|
const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
|
|
11337
12002
|
const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
|
|
@@ -11441,9 +12106,28 @@ const ZCODE_CONFIG_DECLARATION = {
|
|
|
11441
12106
|
ownedKeys: ["mcp"]
|
|
11442
12107
|
} }
|
|
11443
12108
|
};
|
|
12109
|
+
/**
|
|
12110
|
+
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12111
|
+
* and both validate against the one published schema, which the gateway
|
|
12112
|
+
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
12113
|
+
* file with fewer writers — the ignore feature's `fileMode: "local"` and the
|
|
12114
|
+
* project-scope `language` setting go there — so the shape is declared once
|
|
12115
|
+
* and the feature sets separately.
|
|
12116
|
+
*
|
|
12117
|
+
* `language` is the rules feature's: the root `language` key of
|
|
12118
|
+
* `rulesync.jsonc` is written natively for Claude Code instead of as a prompt
|
|
12119
|
+
* block in CLAUDE.md. At project scope it lands in the local file (a
|
|
12120
|
+
* per-developer preference, not a team commitment); at global scope it lands
|
|
12121
|
+
* in `~/.claude/settings.json`, because Claude Code reads no
|
|
12122
|
+
* `~/.claude/settings.local.json`.
|
|
12123
|
+
*/
|
|
12124
|
+
const CLAUDE_SETTINGS_FILE_SHAPE = {
|
|
12125
|
+
format: "json",
|
|
12126
|
+
ensuredKeys: { $schema: CLAUDECODE_SETTINGS_SCHEMA_URL }
|
|
12127
|
+
};
|
|
11444
12128
|
const SHARED_CONFIG_OWNERSHIP = {
|
|
11445
12129
|
[CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
|
|
11446
|
-
|
|
12130
|
+
...CLAUDE_SETTINGS_FILE_SHAPE,
|
|
11447
12131
|
features: {
|
|
11448
12132
|
ignore: {
|
|
11449
12133
|
kind: "custom",
|
|
@@ -11456,6 +12140,23 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
11456
12140
|
permissions: {
|
|
11457
12141
|
kind: "custom",
|
|
11458
12142
|
policyFunction: "applyPermissions"
|
|
12143
|
+
},
|
|
12144
|
+
rules: {
|
|
12145
|
+
kind: "replace-owned-keys",
|
|
12146
|
+
ownedKeys: ["language"]
|
|
12147
|
+
}
|
|
12148
|
+
}
|
|
12149
|
+
},
|
|
12150
|
+
[CLAUDE_SETTINGS_LOCAL_SHARED_FILE_KEY]: {
|
|
12151
|
+
...CLAUDE_SETTINGS_FILE_SHAPE,
|
|
12152
|
+
features: {
|
|
12153
|
+
ignore: {
|
|
12154
|
+
kind: "custom",
|
|
12155
|
+
policyFunction: "applyIgnoreReadDenies"
|
|
12156
|
+
},
|
|
12157
|
+
rules: {
|
|
12158
|
+
kind: "replace-owned-keys",
|
|
12159
|
+
ownedKeys: ["language"]
|
|
11459
12160
|
}
|
|
11460
12161
|
}
|
|
11461
12162
|
},
|
|
@@ -11897,17 +12598,69 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
11897
12598
|
}
|
|
11898
12599
|
};
|
|
11899
12600
|
/**
|
|
12601
|
+
* The keys a declaration ensures, added to `document` when it lacks them and
|
|
12602
|
+
* placed in front of every key it has. A document that states all of them —
|
|
12603
|
+
* with whatever value — comes back as it is, and no key of the document
|
|
12604
|
+
* moves: the whole-document writer emits keys in this order, and the JSONC
|
|
12605
|
+
* edit path is told which keys lead (see {@link serializeSharedConfig}).
|
|
12606
|
+
*/
|
|
12607
|
+
function withEnsuredKeys({ document, ensuredKeys }) {
|
|
12608
|
+
const missing = Object.entries(ensuredKeys).filter(([key]) => !isPrototypePollutionKey(key) && !Object.hasOwn(document, key));
|
|
12609
|
+
if (missing.length === 0) return document;
|
|
12610
|
+
return {
|
|
12611
|
+
...Object.fromEntries(missing),
|
|
12612
|
+
...document
|
|
12613
|
+
};
|
|
12614
|
+
}
|
|
12615
|
+
function serializeDeclaredSharedConfig({ declaration, document, existingContent, filePath, logger }) {
|
|
12616
|
+
const ensuredKeys = declaration.ensuredKeys ?? {};
|
|
12617
|
+
return serializeSharedConfig({
|
|
12618
|
+
format: declaration.format,
|
|
12619
|
+
document: withEnsuredKeys({
|
|
12620
|
+
document,
|
|
12621
|
+
ensuredKeys
|
|
12622
|
+
}),
|
|
12623
|
+
existingContent,
|
|
12624
|
+
leadingKeys: Object.keys(ensuredKeys),
|
|
12625
|
+
filePath,
|
|
12626
|
+
logger
|
|
12627
|
+
});
|
|
12628
|
+
}
|
|
12629
|
+
/**
|
|
12630
|
+
* Serialize a merged document back over a gateway-managed shared file, under
|
|
12631
|
+
* that file's declaration: its format, and the keys the gateway ensures on it
|
|
12632
|
+
* (see {@link SharedConfigFileDeclaration.ensuredKeys}). This is the one
|
|
12633
|
+
* write path of the gateway — {@link applySharedConfigPatch} ends in it, and a
|
|
12634
|
+
* feature whose policy is `custom` serializes its own merge through it — so a
|
|
12635
|
+
* key the gateway ensures is emitted whichever feature writes the file.
|
|
12636
|
+
* Throws when the file is undeclared. A JSONC file that had to be written
|
|
12637
|
+
* whole is warned about through `logger`, named by `filePath` when given and
|
|
12638
|
+
* by `fileKey` otherwise (see {@link serializeSharedConfig}).
|
|
12639
|
+
*/
|
|
12640
|
+
function serializeSharedConfigFile({ fileKey, document, existingContent, filePath, logger }) {
|
|
12641
|
+
const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
|
|
12642
|
+
if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
|
|
12643
|
+
return serializeDeclaredSharedConfig({
|
|
12644
|
+
declaration,
|
|
12645
|
+
document,
|
|
12646
|
+
existingContent,
|
|
12647
|
+
filePath: filePath ?? fileKey,
|
|
12648
|
+
logger
|
|
12649
|
+
});
|
|
12650
|
+
}
|
|
12651
|
+
/**
|
|
11900
12652
|
* Execute a feature's declared write to a gateway-managed shared file: parse
|
|
11901
12653
|
* the existing content, merge the patch under the feature's declared policy,
|
|
11902
12654
|
* and serialize it back over the existing content (see
|
|
11903
|
-
* {@link
|
|
11904
|
-
*
|
|
12655
|
+
* {@link serializeSharedConfigFile}, which adds the keys the file's declaration
|
|
12656
|
+
* ensures and keeps a JSONC file's comments and formatting outside the spans
|
|
12657
|
+
* the merge actually changed). Throws when the
|
|
11905
12658
|
* file or feature is undeclared, when a
|
|
11906
12659
|
* `replace-owned-keys` patch strays outside its owned keys, or when the
|
|
11907
12660
|
* feature's policy is `custom` (those calls go to the named policy function
|
|
11908
12661
|
* instead).
|
|
11909
12662
|
*/
|
|
11910
|
-
function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
|
|
12663
|
+
function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath, logger }) {
|
|
11911
12664
|
const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
|
|
11912
12665
|
if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
|
|
11913
12666
|
const policy = declaration.features[feature];
|
|
@@ -11928,10 +12681,12 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
|
|
|
11928
12681
|
patch
|
|
11929
12682
|
});
|
|
11930
12683
|
for (const [key, value] of Object.entries(patch)) if (value === void 0) delete document[key];
|
|
11931
|
-
return
|
|
11932
|
-
|
|
12684
|
+
return serializeDeclaredSharedConfig({
|
|
12685
|
+
declaration,
|
|
11933
12686
|
document,
|
|
11934
|
-
existingContent
|
|
12687
|
+
existingContent,
|
|
12688
|
+
filePath: filePath ?? fileKey,
|
|
12689
|
+
logger
|
|
11935
12690
|
});
|
|
11936
12691
|
}
|
|
11937
12692
|
const merged = mergeSharedConfigDeep({
|
|
@@ -11939,10 +12694,12 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
|
|
|
11939
12694
|
patch
|
|
11940
12695
|
});
|
|
11941
12696
|
for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
|
|
11942
|
-
return
|
|
11943
|
-
|
|
12697
|
+
return serializeDeclaredSharedConfig({
|
|
12698
|
+
declaration,
|
|
11944
12699
|
document: merged,
|
|
11945
|
-
existingContent
|
|
12700
|
+
existingContent,
|
|
12701
|
+
filePath: filePath ?? fileKey,
|
|
12702
|
+
logger
|
|
11946
12703
|
});
|
|
11947
12704
|
}
|
|
11948
12705
|
const READ_TOOL_NAME = "Read";
|
|
@@ -12486,7 +13243,10 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
12486
13243
|
const factory = this.getFactory(this.toolTarget);
|
|
12487
13244
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
12488
13245
|
const baseDir = (0, node_path.join)(this.outputRoot, paths.relativeDirPath);
|
|
12489
|
-
const checkFilePaths = await findFilesByGlobs(
|
|
13246
|
+
const checkFilePaths = await findFilesByGlobs(factory.meta.filePattern, {
|
|
13247
|
+
cwd: baseDir,
|
|
13248
|
+
followSymbolicLinks: !forDeletion
|
|
13249
|
+
});
|
|
12490
13250
|
const toRelativeFilePath = (path) => (0, node_path.relative)(baseDir, path);
|
|
12491
13251
|
if (forDeletion) {
|
|
12492
13252
|
const toolChecks = checkFilePaths.map((path) => factory.class.forDeletion({
|
|
@@ -13258,7 +14018,7 @@ var AugmentcodeCommand = class AugmentcodeCommand extends ToolCommand {
|
|
|
13258
14018
|
*/
|
|
13259
14019
|
static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
|
|
13260
14020
|
const rootDir = (0, node_path.join)(outputRoot, AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH);
|
|
13261
|
-
const filePaths = await findFilesByGlobs(
|
|
14021
|
+
const filePaths = await findFilesByGlobs("**/*.md", { cwd: rootDir });
|
|
13262
14022
|
const imported = (await Promise.all(filePaths.map(async (filePath) => {
|
|
13263
14023
|
const relativePath = (0, node_path.relative)(rootDir, filePath);
|
|
13264
14024
|
if (relativePath.startsWith("..") || (0, node_path.isAbsolute)(relativePath)) {
|
|
@@ -13295,33 +14055,6 @@ var AugmentcodeCommand = class AugmentcodeCommand extends ToolCommand {
|
|
|
13295
14055
|
}
|
|
13296
14056
|
};
|
|
13297
14057
|
//#endregion
|
|
13298
|
-
//#region src/constants/claudecode-paths.ts
|
|
13299
|
-
/**
|
|
13300
|
-
* Claude Code configuration-layout conventions.
|
|
13301
|
-
*
|
|
13302
|
-
* Single source of truth for where Claude Code expects its files
|
|
13303
|
-
* (directories, file names, scope-specific paths). Every feature module
|
|
13304
|
-
* (rules, commands, skills, subagents, ignore, mcp, permissions, hooks)
|
|
13305
|
-
* and the gitignore entry registry import from here, so a change in the
|
|
13306
|
-
* Claude Code conventions is a change to this file only.
|
|
13307
|
-
*/
|
|
13308
|
-
/** Root directory for Claude Code configuration, relative to the scope root. */
|
|
13309
|
-
const CLAUDECODE_DIR = ".claude";
|
|
13310
|
-
const CLAUDECODE_RULE_FILE_NAME = "CLAUDE.md";
|
|
13311
|
-
const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
|
|
13312
|
-
/** Modular rules directory name under `.claude/` (current format). */
|
|
13313
|
-
const CLAUDECODE_RULES_DIR_NAME = "rules";
|
|
13314
|
-
/** Memories directory name under `.claude/` (legacy format). */
|
|
13315
|
-
const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
|
|
13316
|
-
const CLAUDECODE_COMMANDS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "commands");
|
|
13317
|
-
const CLAUDECODE_AGENTS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "agents");
|
|
13318
|
-
const CLAUDECODE_SKILLS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "skills");
|
|
13319
|
-
const CLAUDECODE_SCHEDULED_TASKS_DIR_PATH = (0, node_path.join)(CLAUDECODE_DIR, "scheduled-tasks");
|
|
13320
|
-
const CLAUDECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
13321
|
-
const CLAUDECODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
13322
|
-
const CLAUDECODE_MCP_FILE_NAME = ".mcp.json";
|
|
13323
|
-
const CLAUDECODE_GLOBAL_MCP_FILE_NAME = ".claude.json";
|
|
13324
|
-
//#endregion
|
|
13325
14058
|
//#region src/features/commands/claudecode-command.ts
|
|
13326
14059
|
const ClaudecodeCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
13327
14060
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -13969,7 +14702,7 @@ function commandSlug(relativeFilePath) {
|
|
|
13969
14702
|
* it up as a regular orphan.
|
|
13970
14703
|
*/
|
|
13971
14704
|
async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
|
|
13972
|
-
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs((0, node_path.join)(root, COMMANDS_FEATURE_SUBDIR
|
|
14705
|
+
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs("**/*.md", { cwd: (0, node_path.join)(root, COMMANDS_FEATURE_SUBDIR) })))).flat().some((filePath) => commandSlug((0, node_path.basename)(filePath)) === dirName);
|
|
13973
14706
|
}
|
|
13974
14707
|
//#endregion
|
|
13975
14708
|
//#region src/features/commands/devin-command.ts
|
|
@@ -14932,6 +15665,41 @@ function toStringMetadata(metadata) {
|
|
|
14932
15665
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
14933
15666
|
}
|
|
14934
15667
|
/**
|
|
15668
|
+
* The `agentsskills` block with the three Agent Skills standard fields resolved
|
|
15669
|
+
* against the root-level rulesync defaults (a defined section value wins).
|
|
15670
|
+
*
|
|
15671
|
+
* Every writer of an Agent Skills `SKILL.md` — the native target, Hermes Agent
|
|
15672
|
+
* and the simulated `agentsmd` — reads the block through this helper so a
|
|
15673
|
+
* root-authored value goes through exactly the same normalization and
|
|
15674
|
+
* spec-violation reporting as a section value. `allowed-tools` has no root
|
|
15675
|
+
* counterpart and is carried through untouched.
|
|
15676
|
+
*
|
|
15677
|
+
* @returns The merged block. It is empty when neither the section nor any
|
|
15678
|
+
* root-level field is set, which `toSpecConformantAgentSkillFields` emits as
|
|
15679
|
+
* nothing.
|
|
15680
|
+
*/
|
|
15681
|
+
function resolveAgentsSkillsSection(rulesyncFrontmatter) {
|
|
15682
|
+
const section = rulesyncFrontmatter.agentsskills;
|
|
15683
|
+
const license = resolveLicense({
|
|
15684
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15685
|
+
section
|
|
15686
|
+
});
|
|
15687
|
+
const compatibility = resolveCompatibility({
|
|
15688
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15689
|
+
section
|
|
15690
|
+
});
|
|
15691
|
+
const metadata = resolveMetadata({
|
|
15692
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15693
|
+
section
|
|
15694
|
+
});
|
|
15695
|
+
return {
|
|
15696
|
+
...section,
|
|
15697
|
+
...license !== void 0 && { license },
|
|
15698
|
+
...compatibility !== void 0 && { compatibility },
|
|
15699
|
+
...metadata !== void 0 && { metadata }
|
|
15700
|
+
};
|
|
15701
|
+
}
|
|
15702
|
+
/**
|
|
14935
15703
|
* Convert the rulesync `agentsskills` block into the shapes the specification
|
|
14936
15704
|
* requires. Shared with `HermesagentSkill`, which writes the same fields to its
|
|
14937
15705
|
* own skill location, so one rulesync input can never produce two different
|
|
@@ -15079,7 +15847,7 @@ var AgentsSkillsSkill = class AgentsSkillsSkill extends ToolSkill {
|
|
|
15079
15847
|
const agentsSkillsFrontmatter = {
|
|
15080
15848
|
name: rulesyncFrontmatter.name,
|
|
15081
15849
|
description: rulesyncFrontmatter.description,
|
|
15082
|
-
...toSpecConformantAgentSkillFields(rulesyncFrontmatter
|
|
15850
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter))
|
|
15083
15851
|
};
|
|
15084
15852
|
AgentsSkillsSkill.reportSpecViolations({
|
|
15085
15853
|
outputRoot,
|
|
@@ -15187,7 +15955,7 @@ var HermesagentSkill = class HermesagentSkill extends AgentsSkillsSkill {
|
|
|
15187
15955
|
}
|
|
15188
15956
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
15189
15957
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
15190
|
-
const shared = toSpecConformantAgentSkillFields(rulesyncFrontmatter
|
|
15958
|
+
const shared = toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter), { coerceMetadata: false });
|
|
15191
15959
|
const hermes = rulesyncFrontmatter.hermesagent ?? {};
|
|
15192
15960
|
const dirName = rulesyncSkill.getDirName();
|
|
15193
15961
|
const frontmatter = {
|
|
@@ -17833,7 +18601,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
17833
18601
|
const treeName = (0, node_path.basename)(sourceTree);
|
|
17834
18602
|
const treeCommandsDirPath = (0, node_path.join)(treeName, COMMANDS_FEATURE_SUBDIR);
|
|
17835
18603
|
const basePath = (0, node_path.join)(sourceTree, COMMANDS_FEATURE_SUBDIR);
|
|
17836
|
-
const rulesyncCommandPaths = await directoryExistsStrict(basePath) ? await findFilesByGlobs(
|
|
18604
|
+
const rulesyncCommandPaths = await directoryExistsStrict(basePath) ? await findFilesByGlobs("**/*.md", { cwd: basePath }) : [];
|
|
17837
18605
|
return await Promise.all(rulesyncCommandPaths.map((path) => RulesyncCommand.fromFile({
|
|
17838
18606
|
outputRoot: treeParent,
|
|
17839
18607
|
relativeDirPath: treeCommandsDirPath,
|
|
@@ -17866,7 +18634,10 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
17866
18634
|
if (factory.meta.skipToolFileScan) return [];
|
|
17867
18635
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
17868
18636
|
const outputRootFull = (0, node_path.join)(this.outputRoot, paths.relativeDirPath);
|
|
17869
|
-
const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ?
|
|
18637
|
+
const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ? `**/*.${factory.meta.extension}` : `*.${factory.meta.extension}`, {
|
|
18638
|
+
cwd: outputRootFull,
|
|
18639
|
+
followSymbolicLinks: !forDeletion
|
|
18640
|
+
});
|
|
17870
18641
|
if (forDeletion) {
|
|
17871
18642
|
const toolCommands = commandFilePaths.map((path) => factory.class.forDeletion({
|
|
17872
18643
|
outputRoot: this.outputRoot,
|
|
@@ -18215,52 +18986,6 @@ function compact(obj) {
|
|
|
18215
18986
|
return result;
|
|
18216
18987
|
}
|
|
18217
18988
|
//#endregion
|
|
18218
|
-
//#region src/utils/quote-value.ts
|
|
18219
|
-
/**
|
|
18220
|
-
* How much of a value read off disk a diagnostic quotes.
|
|
18221
|
-
*
|
|
18222
|
-
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
18223
|
-
* offending value so the reader can find it, but the values these warnings
|
|
18224
|
-
* quote come from files rulesync did not write — a tool's own settings, a
|
|
18225
|
-
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
18226
|
-
* no longer stop at a terminal: they travel into a `--json` document another
|
|
18227
|
-
* program parses and into an MCP result an agent reads as context. A command
|
|
18228
|
-
* line or a header is the shape most likely to carry a credential, and a long
|
|
18229
|
-
* value is the shape most likely to carry instructions aimed at the agent.
|
|
18230
|
-
*/
|
|
18231
|
-
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
18232
|
-
/**
|
|
18233
|
-
* A short, quotable rendering of a value for a diagnostic.
|
|
18234
|
-
*
|
|
18235
|
-
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
18236
|
-
* crafted one read as a second line; stripped of the control characters
|
|
18237
|
-
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
18238
|
-
* bidirectional overrides); and truncated.
|
|
18239
|
-
*/
|
|
18240
|
-
function quoteValueForWarning(value) {
|
|
18241
|
-
return truncateText({
|
|
18242
|
-
text: stripControlCharacters(serialize(value)),
|
|
18243
|
-
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
18244
|
-
suffix: "…(truncated)"
|
|
18245
|
-
});
|
|
18246
|
-
}
|
|
18247
|
-
function serialize(value) {
|
|
18248
|
-
try {
|
|
18249
|
-
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
18250
|
-
} catch {
|
|
18251
|
-
return `[unserializable ${typeof value}]`;
|
|
18252
|
-
}
|
|
18253
|
-
}
|
|
18254
|
-
/**
|
|
18255
|
-
* Strip the control characters out of every string before `JSON.stringify`
|
|
18256
|
-
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
18257
|
-
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
18258
|
-
* over the output can recognize as a control character again.
|
|
18259
|
-
*/
|
|
18260
|
-
function stripStrings(_key, value) {
|
|
18261
|
-
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
18262
|
-
}
|
|
18263
|
-
//#endregion
|
|
18264
18989
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
18265
18990
|
function isToolMatcherEntry(x) {
|
|
18266
18991
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -24847,6 +25572,17 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
24847
25572
|
};
|
|
24848
25573
|
}
|
|
24849
25574
|
/**
|
|
25575
|
+
* The `fileMode: "local"` twin, `.claude/settings.local.json`: a settable
|
|
25576
|
+
* path only under that option, so the default `getSettablePaths` does not
|
|
25577
|
+
* report it, and the gateway would otherwise not know the file has a writer.
|
|
25578
|
+
*/
|
|
25579
|
+
static getExtraSharedWritePaths() {
|
|
25580
|
+
return [{
|
|
25581
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
25582
|
+
relativeFilePath: CLAUDECODE_SETTINGS_LOCAL_FILE_NAME
|
|
25583
|
+
}];
|
|
25584
|
+
}
|
|
25585
|
+
/**
|
|
24850
25586
|
* ClaudecodeIgnore uses settings.json (or settings.local.json), which can
|
|
24851
25587
|
* include non-ignore settings. It should not be deleted by rulesync.
|
|
24852
25588
|
*
|
|
@@ -24889,7 +25625,11 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
24889
25625
|
outputRoot,
|
|
24890
25626
|
relativeDirPath: paths.relativeDirPath,
|
|
24891
25627
|
relativeFilePath: paths.relativeFilePath,
|
|
24892
|
-
fileContent:
|
|
25628
|
+
fileContent: serializeSharedConfigFile({
|
|
25629
|
+
fileKey: sharedConfigFileKey(paths),
|
|
25630
|
+
document: jsonValue,
|
|
25631
|
+
existingContent: existingFileContent
|
|
25632
|
+
}),
|
|
24893
25633
|
validate: true
|
|
24894
25634
|
});
|
|
24895
25635
|
}
|
|
@@ -26265,18 +27005,48 @@ var AiassistantMcp = class AiassistantMcp extends ToolMcp {
|
|
|
26265
27005
|
}
|
|
26266
27006
|
};
|
|
26267
27007
|
//#endregion
|
|
26268
|
-
//#region src/features/
|
|
26269
|
-
|
|
26270
|
-
|
|
26271
|
-
|
|
26272
|
-
|
|
26273
|
-
|
|
26274
|
-
|
|
26275
|
-
|
|
27008
|
+
//#region src/features/shared/amp-settings.ts
|
|
27009
|
+
/**
|
|
27010
|
+
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
27011
|
+
*
|
|
27012
|
+
* Shared by the MCP and permissions adapters because both read the very same
|
|
27013
|
+
* file. Two separately maintained copies drifted once already: the permissions
|
|
27014
|
+
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
27015
|
+
* which left it importing MCP servers reachable only through an injected
|
|
27016
|
+
* `__proto__`.
|
|
27017
|
+
*
|
|
27018
|
+
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
27019
|
+
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
27020
|
+
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
27021
|
+
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
27022
|
+
* never survive as own keys, at any depth.
|
|
27023
|
+
*
|
|
27024
|
+
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
27025
|
+
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
27026
|
+
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
27027
|
+
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
27028
|
+
* explaining, whereas here the surrounding settings are the user's own file
|
|
27029
|
+
* and are left alone.
|
|
27030
|
+
*
|
|
27031
|
+
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
27032
|
+
* plain-object check, so such a document parses successfully with the key
|
|
27033
|
+
* stripped rather than failing that check. That is deliberate — dropping the
|
|
27034
|
+
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
27035
|
+
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
27036
|
+
*/
|
|
27037
|
+
function parseAmpSettings({ fileContent }) {
|
|
27038
|
+
let parsed;
|
|
27039
|
+
try {
|
|
27040
|
+
parsed = parseJsonc$6(fileContent || "{}");
|
|
27041
|
+
} catch (error) {
|
|
27042
|
+
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
26276
27043
|
}
|
|
26277
27044
|
if (!isPlainObject$1(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
26278
27045
|
return parsed;
|
|
26279
27046
|
}
|
|
27047
|
+
//#endregion
|
|
27048
|
+
//#region src/features/mcp/amp-mcp.ts
|
|
27049
|
+
const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
|
|
26280
27050
|
function filterMcpServers(mcpServers) {
|
|
26281
27051
|
const filtered = {};
|
|
26282
27052
|
if (!isRecord$1(mcpServers)) return filtered;
|
|
@@ -26295,7 +27065,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26295
27065
|
json;
|
|
26296
27066
|
constructor(params) {
|
|
26297
27067
|
super(params);
|
|
26298
|
-
this.json =
|
|
27068
|
+
this.json = parseAmpSettings({ fileContent: this.fileContent });
|
|
26299
27069
|
}
|
|
26300
27070
|
getJson() {
|
|
26301
27071
|
return structuredClone(this.json);
|
|
@@ -26339,7 +27109,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26339
27109
|
const basePaths = this.getSettablePaths({ global });
|
|
26340
27110
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
26341
27111
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
26342
|
-
const json = fileContent ?
|
|
27112
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
26343
27113
|
const mcpServers = json[AMP_MCP_SERVERS_KEY];
|
|
26344
27114
|
const newJson = {
|
|
26345
27115
|
...json,
|
|
@@ -26354,7 +27124,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26354
27124
|
global
|
|
26355
27125
|
});
|
|
26356
27126
|
}
|
|
26357
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
27127
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
26358
27128
|
const basePaths = this.getSettablePaths({ global });
|
|
26359
27129
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
26360
27130
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
@@ -26367,7 +27137,8 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26367
27137
|
feature: "mcp",
|
|
26368
27138
|
existingContent: fileContent ?? "",
|
|
26369
27139
|
patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
|
|
26370
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
27140
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
27141
|
+
logger
|
|
26371
27142
|
}),
|
|
26372
27143
|
validate,
|
|
26373
27144
|
global
|
|
@@ -26380,17 +27151,13 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26380
27151
|
validate() {
|
|
26381
27152
|
let json;
|
|
26382
27153
|
try {
|
|
26383
|
-
json =
|
|
27154
|
+
json = parseAmpSettings({ fileContent: this.fileContent });
|
|
26384
27155
|
} catch (error) {
|
|
26385
27156
|
return {
|
|
26386
27157
|
success: false,
|
|
26387
27158
|
error: error instanceof Error ? error : new Error(String(error))
|
|
26388
27159
|
};
|
|
26389
27160
|
}
|
|
26390
|
-
for (const key of Object.keys(json)) if (isPrototypePollutionKey(key)) return {
|
|
26391
|
-
success: false,
|
|
26392
|
-
error: /* @__PURE__ */ new Error(`Prototype pollution key "${key}" is not allowed`)
|
|
26393
|
-
};
|
|
26394
27161
|
const mcpServers = json[AMP_MCP_SERVERS_KEY];
|
|
26395
27162
|
if (mcpServers === void 0) return {
|
|
26396
27163
|
success: true,
|
|
@@ -26400,20 +27167,10 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26400
27167
|
success: false,
|
|
26401
27168
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
26402
27169
|
};
|
|
26403
|
-
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
};
|
|
26408
|
-
if (!isRecord$1(serverConfig)) return {
|
|
26409
|
-
success: false,
|
|
26410
|
-
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
26411
|
-
};
|
|
26412
|
-
for (const key of Object.keys(serverConfig)) if (isPrototypePollutionKey(key)) return {
|
|
26413
|
-
success: false,
|
|
26414
|
-
error: /* @__PURE__ */ new Error(`Config key "${key}" in server "${serverName}" is a prototype pollution key and is not allowed`)
|
|
26415
|
-
};
|
|
26416
|
-
}
|
|
27170
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) if (!isRecord$1(serverConfig)) return {
|
|
27171
|
+
success: false,
|
|
27172
|
+
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
27173
|
+
};
|
|
26417
27174
|
return {
|
|
26418
27175
|
success: true,
|
|
26419
27176
|
error: null
|
|
@@ -27344,7 +28101,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
27344
28101
|
json;
|
|
27345
28102
|
constructor(params) {
|
|
27346
28103
|
super(params);
|
|
27347
|
-
this.json = this.fileContent !== void 0 ? parseJsonc$
|
|
28104
|
+
this.json = this.fileContent !== void 0 ? parseJsonc$6(this.fileContent) : {};
|
|
27348
28105
|
}
|
|
27349
28106
|
getJson() {
|
|
27350
28107
|
return this.json;
|
|
@@ -27365,7 +28122,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
27365
28122
|
validate
|
|
27366
28123
|
});
|
|
27367
28124
|
}
|
|
27368
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
|
|
28125
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
27369
28126
|
const paths = this.getSettablePaths();
|
|
27370
28127
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
27371
28128
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
@@ -27378,7 +28135,8 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
27378
28135
|
feature: "mcp",
|
|
27379
28136
|
existingContent,
|
|
27380
28137
|
patch: { servers: rulesyncMcp.getMcpServers() },
|
|
27381
|
-
filePath
|
|
28138
|
+
filePath,
|
|
28139
|
+
logger
|
|
27382
28140
|
}),
|
|
27383
28141
|
validate
|
|
27384
28142
|
});
|
|
@@ -29509,7 +30267,8 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29509
30267
|
mcp: convertedMcp,
|
|
29510
30268
|
tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
|
|
29511
30269
|
},
|
|
29512
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
30270
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
30271
|
+
logger
|
|
29513
30272
|
}),
|
|
29514
30273
|
validate
|
|
29515
30274
|
});
|
|
@@ -29527,7 +30286,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29527
30286
|
*
|
|
29528
30287
|
* @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
|
|
29529
30288
|
*/
|
|
29530
|
-
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
|
|
30289
|
+
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false, logger }) {
|
|
29531
30290
|
const basePaths = this.getSettablePaths({ global });
|
|
29532
30291
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
29533
30292
|
let fileContent = null;
|
|
@@ -29554,7 +30313,8 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29554
30313
|
feature: "rules",
|
|
29555
30314
|
existingContent: fileContent ?? "",
|
|
29556
30315
|
patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
|
|
29557
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
30316
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
30317
|
+
logger
|
|
29558
30318
|
}),
|
|
29559
30319
|
validate
|
|
29560
30320
|
});
|
|
@@ -29567,7 +30327,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29567
30327
|
}, null, 2) });
|
|
29568
30328
|
}
|
|
29569
30329
|
validate() {
|
|
29570
|
-
const json = parseJsonc$
|
|
30330
|
+
const json = parseJsonc$6(this.fileContent || "{}");
|
|
29571
30331
|
const result = KiloConfigSchema.safeParse(json);
|
|
29572
30332
|
if (!result.success) return {
|
|
29573
30333
|
success: false,
|
|
@@ -30574,7 +31334,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30574
31334
|
mcp: convertedMcp,
|
|
30575
31335
|
tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
|
|
30576
31336
|
},
|
|
30577
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
31337
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
31338
|
+
logger
|
|
30578
31339
|
}),
|
|
30579
31340
|
validate
|
|
30580
31341
|
});
|
|
@@ -30595,7 +31356,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30595
31356
|
* @see https://opencode.ai/docs/rules/
|
|
30596
31357
|
* @see https://opencode.ai/docs/config/
|
|
30597
31358
|
*/
|
|
30598
|
-
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
|
|
31359
|
+
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false, logger }) {
|
|
30599
31360
|
const basePaths = this.getSettablePaths({ global });
|
|
30600
31361
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
30601
31362
|
const configDirPrefix = `${toPosixPath(basePaths.relativeDirPath).replace(/\/+$/, "")}/`;
|
|
@@ -30627,7 +31388,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30627
31388
|
feature: "rules",
|
|
30628
31389
|
existingContent: fileContent ?? "",
|
|
30629
31390
|
patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
|
|
30630
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
31391
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
31392
|
+
logger
|
|
30631
31393
|
}),
|
|
30632
31394
|
validate
|
|
30633
31395
|
});
|
|
@@ -30643,7 +31405,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30643
31405
|
}, null, 2) });
|
|
30644
31406
|
}
|
|
30645
31407
|
validate() {
|
|
30646
|
-
const json = parseJsonc$
|
|
31408
|
+
const json = parseJsonc$6(this.fileContent || "{}");
|
|
30647
31409
|
const result = OpencodeConfigSchema.safeParse(json);
|
|
30648
31410
|
if (!result.success) return {
|
|
30649
31411
|
success: false,
|
|
@@ -33115,16 +33877,6 @@ function isCanonicalAmpEntry(entry) {
|
|
|
33115
33877
|
const keys = Object.keys(matches);
|
|
33116
33878
|
return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
|
|
33117
33879
|
}
|
|
33118
|
-
function parseAmpSettings(fileContent) {
|
|
33119
|
-
const errors = [];
|
|
33120
|
-
const parsed = (0, jsonc_parser.parse)(fileContent || "{}", errors, { allowTrailingComma: true });
|
|
33121
|
-
if (errors.length > 0) {
|
|
33122
|
-
const details = errors.map((error) => `${(0, jsonc_parser.printParseErrorCode)(error.error)} at offset ${error.offset}`).join(", ");
|
|
33123
|
-
throw new Error(`Failed to parse Amp settings: ${details}`);
|
|
33124
|
-
}
|
|
33125
|
-
if (!isPlainObject$1(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
33126
|
-
return parsed;
|
|
33127
|
-
}
|
|
33128
33880
|
function toDisableList(value) {
|
|
33129
33881
|
if (!Array.isArray(value)) return [];
|
|
33130
33882
|
return value.filter((entry) => typeof entry === "string");
|
|
@@ -33240,7 +33992,7 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33240
33992
|
const basePaths = AmpPermissions.getSettablePaths({ global });
|
|
33241
33993
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
33242
33994
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
33243
|
-
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
33995
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
33244
33996
|
const newJson = {
|
|
33245
33997
|
...json,
|
|
33246
33998
|
[AMP_TOOLS_DISABLE_KEY]: toDisableList(json[AMP_TOOLS_DISABLE_KEY])
|
|
@@ -33253,11 +34005,11 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33253
34005
|
validate
|
|
33254
34006
|
});
|
|
33255
34007
|
}
|
|
33256
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
34008
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
33257
34009
|
const basePaths = AmpPermissions.getSettablePaths({ global });
|
|
33258
34010
|
const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
|
|
33259
34011
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
33260
|
-
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
34012
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
33261
34013
|
const config = rulesyncPermissions.getJson();
|
|
33262
34014
|
const { disable, permissions } = convertRulesyncToAmp(config);
|
|
33263
34015
|
const override = config.amp;
|
|
@@ -33284,13 +34036,14 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33284
34036
|
feature: "permissions",
|
|
33285
34037
|
existingContent: fileContent ?? "",
|
|
33286
34038
|
patch,
|
|
33287
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
34039
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
34040
|
+
logger
|
|
33288
34041
|
}),
|
|
33289
34042
|
validate: true
|
|
33290
34043
|
});
|
|
33291
34044
|
}
|
|
33292
34045
|
toRulesyncPermissions() {
|
|
33293
|
-
const json = parseAmpSettings(this.getFileContent());
|
|
34046
|
+
const json = parseAmpSettings({ fileContent: this.getFileContent() });
|
|
33294
34047
|
const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
|
|
33295
34048
|
const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
|
|
33296
34049
|
const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
|
|
@@ -33310,7 +34063,7 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33310
34063
|
}
|
|
33311
34064
|
validate() {
|
|
33312
34065
|
try {
|
|
33313
|
-
const json = parseAmpSettings(this.fileContent);
|
|
34066
|
+
const json = parseAmpSettings({ fileContent: this.fileContent });
|
|
33314
34067
|
const disable = json[AMP_TOOLS_DISABLE_KEY];
|
|
33315
34068
|
if (disable !== void 0 && !Array.isArray(disable)) return {
|
|
33316
34069
|
success: false,
|
|
@@ -35360,15 +36113,15 @@ const CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS = Object.entries(SHARED_CONFIG_OWNE
|
|
|
35360
36113
|
* passthrough must not carry. `permissions` and `sandbox` have their own merge
|
|
35361
36114
|
* branches (the managed `allow`/`ask`/`deny` arrays and the scope filtering
|
|
35362
36115
|
* respectively), `permission` is rulesync's own canonical tool-scoped block
|
|
35363
|
-
* rather than a settings key,
|
|
35364
|
-
*
|
|
35365
|
-
* shared file.
|
|
36116
|
+
* rather than a settings key, the keys the gateway ensures on the file
|
|
36117
|
+
* (`$schema`) are editor pointers rather than Claude Code settings, and the
|
|
36118
|
+
* rest belong to the other features writing this shared file.
|
|
35366
36119
|
*/
|
|
35367
36120
|
const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
35368
36121
|
"permission",
|
|
35369
36122
|
"permissions",
|
|
35370
36123
|
"sandbox",
|
|
35371
|
-
"
|
|
36124
|
+
...Object.keys(SHARED_CONFIG_OWNERSHIP[".claude/settings.json"]?.ensuredKeys ?? {}),
|
|
35372
36125
|
...CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS
|
|
35373
36126
|
]);
|
|
35374
36127
|
/**
|
|
@@ -35740,7 +36493,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
35740
36493
|
deny,
|
|
35741
36494
|
logger
|
|
35742
36495
|
});
|
|
35743
|
-
const fileContent =
|
|
36496
|
+
const fileContent = serializeSharedConfigFile({
|
|
36497
|
+
fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
|
|
36498
|
+
document: merged,
|
|
36499
|
+
existingContent
|
|
36500
|
+
});
|
|
35744
36501
|
return new ClaudecodePermissions({
|
|
35745
36502
|
outputRoot,
|
|
35746
36503
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -36767,7 +37524,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
36767
37524
|
validate
|
|
36768
37525
|
});
|
|
36769
37526
|
}
|
|
36770
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
|
|
37527
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
|
|
36771
37528
|
const paths = CopilotPermissions.getSettablePaths();
|
|
36772
37529
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
36773
37530
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
@@ -36787,7 +37544,8 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
36787
37544
|
feature: "permissions",
|
|
36788
37545
|
existingContent,
|
|
36789
37546
|
patch,
|
|
36790
|
-
filePath
|
|
37547
|
+
filePath,
|
|
37548
|
+
logger
|
|
36791
37549
|
}),
|
|
36792
37550
|
validate: true
|
|
36793
37551
|
});
|
|
@@ -37507,6 +38265,18 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
|
|
|
37507
38265
|
isDeletable() {
|
|
37508
38266
|
return false;
|
|
37509
38267
|
}
|
|
38268
|
+
/**
|
|
38269
|
+
* `config.toml` is dcode's file, not one rulesync owns: rulesync merges into
|
|
38270
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
38271
|
+
* nothing. When no rule maps, `[shell]` is dropped and
|
|
38272
|
+
* `smolToml.stringify({})` leaves a lone newline, which would otherwise be
|
|
38273
|
+
* written as a fresh `~/.deepagents/config.toml` that says nothing. An
|
|
38274
|
+
* existing file is still rewritten as before, so user content is never
|
|
38275
|
+
* dropped — the skip only applies when there is no file yet.
|
|
38276
|
+
*/
|
|
38277
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
38278
|
+
return true;
|
|
38279
|
+
}
|
|
37510
38280
|
static getSettablePaths(_options) {
|
|
37511
38281
|
return {
|
|
37512
38282
|
relativeDirPath: DEEPAGENTS_DIR,
|
|
@@ -40439,7 +41209,8 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
40439
41209
|
feature: "permissions",
|
|
40440
41210
|
existingContent: fileContent ?? "",
|
|
40441
41211
|
patch: { permission },
|
|
40442
|
-
filePath: (0, node_path.join)(jsonDir, relativeFilePath)
|
|
41212
|
+
filePath: (0, node_path.join)(jsonDir, relativeFilePath),
|
|
41213
|
+
logger
|
|
40443
41214
|
}),
|
|
40444
41215
|
validate: true
|
|
40445
41216
|
});
|
|
@@ -40465,7 +41236,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
40465
41236
|
}
|
|
40466
41237
|
validate() {
|
|
40467
41238
|
try {
|
|
40468
|
-
const json = parseJsonc$
|
|
41239
|
+
const json = parseJsonc$6(this.fileContent || "{}");
|
|
40469
41240
|
const result = OpencodePermissionsConfigSchema.safeParse(json);
|
|
40470
41241
|
if (!result.success) return {
|
|
40471
41242
|
success: false,
|
|
@@ -42564,18 +43335,20 @@ function pickSecurityPolicies(source, report) {
|
|
|
42564
43335
|
* not emoji at all are named beside it: the angle brackets, the trigrams and
|
|
42565
43336
|
* digrams, and the hexagrams of U+4DC0–U+4DFF.
|
|
42566
43337
|
*
|
|
42567
|
-
* The ranges are the standard
|
|
42568
|
-
*
|
|
42569
|
-
*
|
|
42570
|
-
*
|
|
42571
|
-
*
|
|
42572
|
-
*
|
|
42573
|
-
*
|
|
42574
|
-
*
|
|
42575
|
-
*
|
|
42576
|
-
*
|
|
42577
|
-
*
|
|
42578
|
-
*
|
|
43338
|
+
* The ranges are the standard blocks rather than the full `East_Asian_Width`
|
|
43339
|
+
* property, because the wide characters sit in blocks and a block is coarse in
|
|
43340
|
+
* the safe direction: the whole of U+1F000–U+1FAFF is counted as wide, which
|
|
43341
|
+
* overstates the few narrow symbols in it, because overstating a width shortens
|
|
43342
|
+
* a label that did not need it while understating one lets a label wrap. That
|
|
43343
|
+
* is also why an emoji built from a chain of joiners is counted per component:
|
|
43344
|
+
* two columns each rather than the two the whole chain draws.
|
|
43345
|
+
*
|
|
43346
|
+
* The East Asian Ambiguous class is counted at two columns as well, by the
|
|
43347
|
+
* table below this one rather than by these ranges, and that table is the
|
|
43348
|
+
* whole of its property: the class is scattered among the Neutral characters
|
|
43349
|
+
* rather than gathered in blocks, so a block list over it would either miss
|
|
43350
|
+
* members or take in characters that are not wide anywhere. The reason for
|
|
43351
|
+
* counting it, and how the table is kept in step with Unicode, is given there.
|
|
42579
43352
|
*
|
|
42580
43353
|
* Two of the ranges are here for a narrower reason: a name the skill prompt can
|
|
42581
43354
|
* offer may not be counted narrower here than the prompt's own renderer counts
|
|
@@ -42613,6 +43386,64 @@ function pickSecurityPolicies(source, report) {
|
|
|
42613
43386
|
*/
|
|
42614
43387
|
const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|[\u2329\u232a\u2630-\u2637\u268a-\u268f\u4dc0-\u4dff]|[\u1100-\u11ff\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\ua960-\ua97f\uac00-\ud7a3\ud7b0-\ud7fb\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]|[\u{16fe0}-\u{16ff6}]|[\u{17000}-\u{18dff}]|[\u{1aff0}-\u{1b2ff}]|[\u{1f000}-\u{1faff}]|[\u{20000}-\u{3fffd}]/u;
|
|
42615
43388
|
/**
|
|
43389
|
+
* The East Asian Ambiguous class of UAX #11: the characters a terminal draws
|
|
43390
|
+
* one column wide under a Latin font and two columns wide where it is set to
|
|
43391
|
+
* draw the ambiguous class wide, which is a common setting in CJK locales. The
|
|
43392
|
+
* box-drawing characters, the geometric shapes, the Greek and Cyrillic
|
|
43393
|
+
* alphabets, and the accented letters of the Latin-1 supplement are all here.
|
|
43394
|
+
*
|
|
43395
|
+
* Counted at two columns, which is the wider of the two answers, because the
|
|
43396
|
+
* two ways of being wrong are not the same size. Overstating a width shortens
|
|
43397
|
+
* a label that did not need it: a name carrying a Greek letter or an accented
|
|
43398
|
+
* letter is cut a little earlier on a Latin terminal than it had to be.
|
|
43399
|
+
* Understating one lets a forged row wrap: a name of sixty box-drawing
|
|
43400
|
+
* characters and `● pdf-tools` measures 71 columns at one column apiece,
|
|
43401
|
+
* inside the prompt's budget, and draws at 132 in a terminal that draws the
|
|
43402
|
+
* class wide, so the terminal breaks the row itself and puts `● pdf-tools` at
|
|
43403
|
+
* the left margin of a continuation line that carries no pointer and no
|
|
43404
|
+
* checkbox.
|
|
43405
|
+
*
|
|
43406
|
+
* The prompt's own renderer, `fast-string-width`, counts the class at one
|
|
43407
|
+
* column and never sees the wrap coming either; the terminal is what breaks
|
|
43408
|
+
* the row, and only the budget can keep the row short enough not to be broken.
|
|
43409
|
+
*
|
|
43410
|
+
* One width model rather than two — a wide one for bounding an untrusted name
|
|
43411
|
+
* and a narrow one for laying out the tool's own text — because the second
|
|
43412
|
+
* model would buy a column or two of alignment in the tool's own output at the
|
|
43413
|
+
* price of two measurements that have to be kept from being confused for each
|
|
43414
|
+
* other. Where this is used to lay out text of the tool's own, the cost of the
|
|
43415
|
+
* single model is a line cut a little earlier than it needed to be.
|
|
43416
|
+
*
|
|
43417
|
+
* The ranges are the `East_Asian_Width=A` property of Unicode 17.0, taken from
|
|
43418
|
+
* the table `get-east-asian-width` 1.6.0 carries and merged where two are
|
|
43419
|
+
* adjacent. The table is written out here rather than read from the package so
|
|
43420
|
+
* that the one lookup adds no dependency of its own, and the drift test in
|
|
43421
|
+
* `display-width.test.ts` is what keeps it honest: it loads the package through
|
|
43422
|
+
* the prompt renderer that already depends on it, walks every code point, and
|
|
43423
|
+
* fails when this table and the property disagree in either direction. When
|
|
43424
|
+
* the renderer's copy moves to a newer Unicode, that test is what fails, and
|
|
43425
|
+
* the table is regenerated from the new property — its ranges, marks aside,
|
|
43426
|
+
* merged where adjacent and written as escapes — with the version named here.
|
|
43427
|
+
*
|
|
43428
|
+
* Three stretches of the property are left out because they never reach this
|
|
43429
|
+
* pattern: the combining diacritical marks of U+0300–U+036F and the variation
|
|
43430
|
+
* selectors of U+FE00–U+FE0F and U+E0100–U+E01EF are marks by category and are
|
|
43431
|
+
* counted by the mark rule before the width of a character is looked up.
|
|
43432
|
+
* Leaving them out is also what keeps the class from holding a combining
|
|
43433
|
+
* character, which `no-misleading-character-class` is there to catch, and they
|
|
43434
|
+
* are the one exception the drift test allows. U+00AD SOFT HYPHEN never reaches
|
|
43435
|
+
* this pattern either — it is a format character, and the zero-width rule
|
|
43436
|
+
* counts it at nothing first — but it is kept, as the property has it, so that
|
|
43437
|
+
* the test holds the table to the property exactly rather than to a list of
|
|
43438
|
+
* exceptions. The private use areas are in it, as the property says they are: a
|
|
43439
|
+
* name is free to carry them and a terminal is free to draw them wide.
|
|
43440
|
+
*
|
|
43441
|
+
* Escapes rather than the characters themselves, for the reason given above.
|
|
43442
|
+
* Exported for the drift test alone; the width of a string is asked for
|
|
43443
|
+
* through `displayWidthOf`.
|
|
43444
|
+
*/
|
|
43445
|
+
const AMBIGUOUS_CHARACTERS_PATTERN = /[\u00a1\u00a4\u00a7-\u00a8\u00aa\u00ad-\u00ae\u00b0-\u00b4\u00b6-\u00ba\u00bc-\u00bf\u00c6\u00d0\u00d7-\u00d8\u00de-\u00e1\u00e6\u00e8-\u00ea\u00ec-\u00ed\u00f0\u00f2-\u00f3\u00f7-\u00fa\u00fc\u00fe\u0101\u0111\u0113\u011b\u0126-\u0127\u012b\u0131-\u0133\u0138\u013f-\u0142\u0144\u0148-\u014b\u014d\u0152-\u0153\u0166-\u0167\u016b\u01ce\u01d0\u01d2\u01d4\u01d6\u01d8\u01da\u01dc\u0251\u0261\u02c4\u02c7\u02c9-\u02cb\u02cd\u02d0\u02d8-\u02db\u02dd\u02df\u0391-\u03a1\u03a3-\u03a9\u03b1-\u03c1\u03c3-\u03c9\u0401\u0410-\u044f\u0451\u2010\u2013-\u2016\u2018-\u2019\u201c-\u201d\u2020-\u2022\u2024-\u2027\u2030\u2032-\u2033\u2035\u203b\u203e\u2074\u207f\u2081-\u2084\u20ac\u2103\u2105\u2109\u2113\u2116\u2121-\u2122\u2126\u212b\u2153-\u2154\u215b-\u215e\u2160-\u216b\u2170-\u2179\u2189\u2190-\u2199\u21b8-\u21b9\u21d2\u21d4\u21e7\u2200\u2202-\u2203\u2207-\u2208\u220b\u220f\u2211\u2215\u221a\u221d-\u2220\u2223\u2225\u2227-\u222c\u222e\u2234-\u2237\u223c-\u223d\u2248\u224c\u2252\u2260-\u2261\u2264-\u2267\u226a-\u226b\u226e-\u226f\u2282-\u2283\u2286-\u2287\u2295\u2299\u22a5\u22bf\u2312\u2460-\u24e9\u24eb-\u254b\u2550-\u2573\u2580-\u258f\u2592-\u2595\u25a0-\u25a1\u25a3-\u25a9\u25b2-\u25b3\u25b6-\u25b7\u25bc-\u25bd\u25c0-\u25c1\u25c6-\u25c8\u25cb\u25ce-\u25d1\u25e2-\u25e5\u25ef\u2605-\u2606\u2609\u260e-\u260f\u261c\u261e\u2640\u2642\u2660-\u2661\u2663-\u2665\u2667-\u266a\u266c-\u266d\u266f\u269e-\u269f\u26bf\u26c6-\u26cd\u26cf-\u26d3\u26d5-\u26e1\u26e3\u26e8-\u26e9\u26eb-\u26f1\u26f4\u26f6-\u26f9\u26fb-\u26fc\u26fe-\u26ff\u273d\u2776-\u277f\u2b56-\u2b59\u3248-\u324f\ue000-\uf8ff\ufffd\u{1f100}-\u{1f10a}\u{1f110}-\u{1f12d}\u{1f130}-\u{1f169}\u{1f170}-\u{1f18d}\u{1f18f}-\u{1f190}\u{1f19b}-\u{1f1ac}\u{f0000}-\u{ffffd}\u{100000}-\u{10fffd}]/u;
|
|
43446
|
+
/**
|
|
42616
43447
|
* U+FE0F VARIATION SELECTOR-16, which takes no width of its own but asks the
|
|
42617
43448
|
* character before it to be drawn as an emoji — that is, in two columns rather
|
|
42618
43449
|
* than one. Counting it as a column of its own is how that promotion is paid
|
|
@@ -42620,7 +43451,12 @@ const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|
|
|
|
42620
43451
|
* draws rather than the one the heart alone would.
|
|
42621
43452
|
*/
|
|
42622
43453
|
const EMOJI_PRESENTATION_SELECTOR = "️";
|
|
42623
|
-
/**
|
|
43454
|
+
/**
|
|
43455
|
+
* Combining marks are drawn on top of the character before them, not beside it.
|
|
43456
|
+
*
|
|
43457
|
+
* Exported for the drift test on the ambiguous table, which has to leave out
|
|
43458
|
+
* exactly the characters this rule catches first.
|
|
43459
|
+
*/
|
|
42624
43460
|
const COMBINING_MARK_PATTERN = /[\p{Mn}\p{Me}]/u;
|
|
42625
43461
|
/** The characters that take no width at all, marks aside. */
|
|
42626
43462
|
const ZERO_WIDTH_CHARACTERS_PATTERN = /[\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
|
|
@@ -42666,16 +43502,28 @@ function isCombiningMark(character) {
|
|
|
42666
43502
|
return character !== EMOJI_PRESENTATION_SELECTOR && COMBINING_MARK_PATTERN.test(character);
|
|
42667
43503
|
}
|
|
42668
43504
|
/**
|
|
43505
|
+
* A lone surrogate, which is no character at all: a string can carry one — a
|
|
43506
|
+
* `"\ud800"` escape in JSON is enough — and the encoder that writes stdout
|
|
43507
|
+
* replaces it with U+FFFD on the way out, so the replacement character is what
|
|
43508
|
+
* the terminal draws and what has to be measured. U+FFFD is East Asian
|
|
43509
|
+
* Ambiguous, so the difference is a column apiece: 72 of them measured as
|
|
43510
|
+
* themselves fit a 72-column budget and are drawn in 144.
|
|
43511
|
+
*/
|
|
43512
|
+
const LONE_SURROGATE_PATTERN = /\p{Cs}/u;
|
|
43513
|
+
/** What the stdout encoder writes in place of a lone surrogate. */
|
|
43514
|
+
const REPLACEMENT_CHARACTER = "�";
|
|
43515
|
+
/**
|
|
42669
43516
|
* The width of one character, given how many marks already sit on the character
|
|
42670
43517
|
* before it.
|
|
42671
43518
|
*/
|
|
42672
43519
|
function widthInContext(params) {
|
|
42673
|
-
const {
|
|
43520
|
+
const { precedingMarks } = params;
|
|
43521
|
+
const character = LONE_SURROGATE_PATTERN.test(params.character) ? REPLACEMENT_CHARACTER : params.character;
|
|
42674
43522
|
if (character === EMOJI_PRESENTATION_SELECTOR) return 1;
|
|
42675
43523
|
if (isCombiningMark(character)) return precedingMarks < FREE_MARKS_PER_CHARACTER ? 0 : 1;
|
|
42676
43524
|
if (RENDERER_COUNTED_JOINERS.test(character)) return 1;
|
|
42677
43525
|
if (ZERO_WIDTH_CHARACTERS_PATTERN.test(character)) return 0;
|
|
42678
|
-
return WIDE_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
43526
|
+
return WIDE_CHARACTERS_PATTERN.test(character) || AMBIGUOUS_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
42679
43527
|
}
|
|
42680
43528
|
/**
|
|
42681
43529
|
* How many terminal columns a string occupies.
|
|
@@ -42699,6 +43547,20 @@ function displayWidthOf(text) {
|
|
|
42699
43547
|
/** The mark a cut string ends in. */
|
|
42700
43548
|
const SHORTENING_ELLIPSIS = "…";
|
|
42701
43549
|
/**
|
|
43550
|
+
* The columns a cut string is drawn in at the very least: what is left when
|
|
43551
|
+
* the budget has room for the mark of the cut and nothing before it. Two, since
|
|
43552
|
+
* the ellipsis is itself East Asian Ambiguous and is measured the way every
|
|
43553
|
+
* other such character is, so that a shortened label is not wider than it was
|
|
43554
|
+
* measured to be on the terminal the measurement is for.
|
|
43555
|
+
*
|
|
43556
|
+
* Exported because a caller composing several shortened pieces into one line
|
|
43557
|
+
* has to leave room for it to decide which of them gives way, and the width of
|
|
43558
|
+
* the mark is this module's business rather than something to be counted again
|
|
43559
|
+
* at the other end. Leaving room for it is not what bounds the line: cutting
|
|
43560
|
+
* the composed line is.
|
|
43561
|
+
*/
|
|
43562
|
+
const ELLIPSIS_WIDTH = displayWidthOf(SHORTENING_ELLIPSIS);
|
|
43563
|
+
/**
|
|
42702
43564
|
* Cut `text` down to at most `budget` columns, marking the cut with an ellipsis.
|
|
42703
43565
|
*
|
|
42704
43566
|
* The ellipsis is paid for out of the budget rather than added on top of it, so
|
|
@@ -42709,7 +43571,7 @@ const SHORTENING_ELLIPSIS = "…";
|
|
|
42709
43571
|
function shortenToWidth(params) {
|
|
42710
43572
|
const { text, budget } = params;
|
|
42711
43573
|
if (displayWidthOf(text) <= budget) return text;
|
|
42712
|
-
const target = budget -
|
|
43574
|
+
const target = budget - ELLIPSIS_WIDTH;
|
|
42713
43575
|
let width = 0;
|
|
42714
43576
|
let marks = 0;
|
|
42715
43577
|
const kept = [];
|
|
@@ -45647,7 +46509,7 @@ var AgentsmdSkill = class AgentsmdSkill extends SimulatedSkill {
|
|
|
45647
46509
|
const relativeDirPath = this.getSettablePaths().relativeDirPath;
|
|
45648
46510
|
const frontmatter = {
|
|
45649
46511
|
...defaults.frontmatter,
|
|
45650
|
-
...toSpecConformantAgentSkillFields(params.rulesyncSkill.getFrontmatter()
|
|
46512
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(params.rulesyncSkill.getFrontmatter()))
|
|
45651
46513
|
};
|
|
45652
46514
|
AgentsSkillsSkill.reportSpecViolations({
|
|
45653
46515
|
outputRoot: params.outputRoot ?? process.cwd(),
|
|
@@ -45773,13 +46635,25 @@ var RovodevSkill = class RovodevSkill extends ToolSkill {
|
|
|
45773
46635
|
const settablePaths = RovodevSkill.getSettablePaths({ global });
|
|
45774
46636
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
45775
46637
|
const rovodevSection = rulesyncFrontmatter.rovodev;
|
|
46638
|
+
const license = resolveLicense({
|
|
46639
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46640
|
+
section: rovodevSection
|
|
46641
|
+
});
|
|
46642
|
+
const compatibility = resolveCompatibility({
|
|
46643
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46644
|
+
section: rovodevSection
|
|
46645
|
+
});
|
|
46646
|
+
const metadata = resolveMetadata({
|
|
46647
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46648
|
+
section: rovodevSection
|
|
46649
|
+
});
|
|
45776
46650
|
const rovodevFrontmatter = {
|
|
45777
46651
|
name: rulesyncFrontmatter.name,
|
|
45778
46652
|
description: rulesyncFrontmatter.description,
|
|
45779
46653
|
...rovodevSection?.["allowed-tools"] !== void 0 && { "allowed-tools": rovodevSection["allowed-tools"] },
|
|
45780
|
-
...
|
|
45781
|
-
...
|
|
45782
|
-
...
|
|
46654
|
+
...license !== void 0 && { license },
|
|
46655
|
+
...compatibility !== void 0 && { compatibility },
|
|
46656
|
+
...metadata !== void 0 && { metadata }
|
|
45783
46657
|
};
|
|
45784
46658
|
return new RovodevSkill({
|
|
45785
46659
|
outputRoot,
|
|
@@ -45961,7 +46835,12 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45961
46835
|
composed: file.composed
|
|
45962
46836
|
})) dirHasChanges = true;
|
|
45963
46837
|
}
|
|
45964
|
-
if (!dirHasChanges)
|
|
46838
|
+
if (!dirHasChanges) {
|
|
46839
|
+
if (!this.dryRun) {
|
|
46840
|
+
for (const file of otherFiles) if (file.fileMode !== void 0) await restoreMissingExecutableBit((0, node_path.join)(dirPath, file.relativeFilePathToDirPath), file.fileMode);
|
|
46841
|
+
}
|
|
46842
|
+
continue;
|
|
46843
|
+
}
|
|
45965
46844
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
45966
46845
|
if (this.dryRun) {
|
|
45967
46846
|
this.logger.info(`[DRY RUN] Would create directory: ${stripControlCharacters(dirPath)}`);
|
|
@@ -45980,7 +46859,9 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45980
46859
|
changedPaths.push((0, node_path.join)(relativeDir, mainFile.name));
|
|
45981
46860
|
}
|
|
45982
46861
|
for (const file of otherFiles) {
|
|
45983
|
-
|
|
46862
|
+
const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
|
|
46863
|
+
await writeFileBuffer(filePath, file.fileBuffer);
|
|
46864
|
+
if (file.fileMode !== void 0) await applyFileMode(filePath, file.fileMode);
|
|
45984
46865
|
changedPaths.push((0, node_path.join)(relativeDir, file.relativeFilePathToDirPath));
|
|
45985
46866
|
}
|
|
45986
46867
|
}
|
|
@@ -45998,7 +46879,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45998
46879
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
45999
46880
|
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
46000
46881
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46001
|
-
const quotedOutputRoot =
|
|
46882
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46002
46883
|
for (const aiDir of existingDirs) {
|
|
46003
46884
|
const dirPath = aiDir.getDirPath();
|
|
46004
46885
|
const { verdict, root } = locateInOwnRoot({
|
|
@@ -46006,14 +46887,14 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46006
46887
|
dirPath,
|
|
46007
46888
|
outputRoot: this.outputRoot
|
|
46008
46889
|
});
|
|
46009
|
-
const quotedDirPath =
|
|
46010
|
-
const quotedRoot =
|
|
46890
|
+
const quotedDirPath = quoteForLog(dirPath);
|
|
46891
|
+
const quotedRoot = quoteForLog(root);
|
|
46011
46892
|
if (verdict === "root-outside") {
|
|
46012
46893
|
this.logger.warn(`Refusing to delete ${quotedDirPath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
46013
46894
|
continue;
|
|
46014
46895
|
}
|
|
46015
46896
|
if (!aiDir.ownsDirTree()) {
|
|
46016
|
-
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${
|
|
46897
|
+
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${quoteForLog(aiDir.getDirName())}: ${quotedDirPath} is a shared root, not a directory of its own`);
|
|
46017
46898
|
else this.logger.warn(`Refusing to delete ${quotedDirPath}: it does not own that directory, and it is not the shared root ${quotedRoot} it was found in either`);
|
|
46018
46899
|
continue;
|
|
46019
46900
|
}
|
|
@@ -46029,6 +46910,126 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46029
46910
|
});
|
|
46030
46911
|
}
|
|
46031
46912
|
/**
|
|
46913
|
+
* Remove the files left inside a directory this run still generates, but
|
|
46914
|
+
* which the run no longer writes.
|
|
46915
|
+
*
|
|
46916
|
+
* The directory sweep above cannot see them: it removes a directory that no
|
|
46917
|
+
* longer corresponds to any generated entry, and never looks inside one that
|
|
46918
|
+
* does. So deleting a companion file from a source directory that is
|
|
46919
|
+
* otherwise kept left the generated copy in place — and, because change
|
|
46920
|
+
* detection compares only the files the run will write, the run reported
|
|
46921
|
+
* itself up to date while an agent went on reading the stale file. The same
|
|
46922
|
+
* gap left any file that was never rulesync's sitting inside a directory the
|
|
46923
|
+
* user now believes rulesync owns.
|
|
46924
|
+
*
|
|
46925
|
+
* Only a directory that owns its whole tree is swept, and only when this run
|
|
46926
|
+
* generated it. That is the same claim the directory sweep already acts on,
|
|
46927
|
+
* one level down: a directory whose entry disappears is deleted outright,
|
|
46928
|
+
* companion files and all, so a file inside one whose entry is still here and
|
|
46929
|
+
* which no source produces is stale by exactly the same reasoning.
|
|
46930
|
+
*
|
|
46931
|
+
* Two kinds of file are left alone:
|
|
46932
|
+
*
|
|
46933
|
+
* - **Hidden entries.** The loader does carry a hidden companion — a
|
|
46934
|
+
* `.env.example` beside the script that reads it is skill content — so a
|
|
46935
|
+
* hidden file here may be rulesync's. But a hidden name is also where a
|
|
46936
|
+
* user's own files live: a `.gitkeep`, a `.env` with real values in it. The
|
|
46937
|
+
* sweep cannot tell the two apart by name, and deleting the user's is the
|
|
46938
|
+
* worse mistake, so a stale hidden companion is the one leftover it
|
|
46939
|
+
* knowingly keeps.
|
|
46940
|
+
* - **Symbolic links**, which the writer never creates. The walk neither
|
|
46941
|
+
* follows nor reports them, so a link is never removed and never resolved
|
|
46942
|
+
* into a deletion somewhere outside the tree.
|
|
46943
|
+
*
|
|
46944
|
+
* Nothing is swept at all by a run that could not read its sources in full.
|
|
46945
|
+
* `AiDir` drops a companion file it cannot open, and stops short of a subtree
|
|
46946
|
+
* it is denied or that runs past one of its bounds -- warning each time, but
|
|
46947
|
+
* carrying on, because a skill that is short one file is still worth writing.
|
|
46948
|
+
* The output copy of such a file is then indistinguishable here from a file
|
|
46949
|
+
* whose source was deleted, and the wrong guess deletes something the next
|
|
46950
|
+
* readable run would put straight back. So a shortfall anywhere in the run
|
|
46951
|
+
* calls the whole sweep off: it is the sweeps that are optional, not the
|
|
46952
|
+
* files.
|
|
46953
|
+
*
|
|
46954
|
+
* @param isClaimed - Whether some other target or feature in this run wrote
|
|
46955
|
+
* this exact path. A shared output root -- `.agents/skills/`, written by
|
|
46956
|
+
* several targets at once -- is a directory whose entry here lists only
|
|
46957
|
+
* *this* target's files, so without the run's own record a sibling's fresh
|
|
46958
|
+
* output reads as an orphan. Asked per path rather than per tree: a tree
|
|
46959
|
+
* claim covers the directory this sweep is looking inside of, and would
|
|
46960
|
+
* answer yes to every file in it.
|
|
46961
|
+
*/
|
|
46962
|
+
async removeOrphanFilesInAiDirs({ generatedDirs, isClaimed }) {
|
|
46963
|
+
if (hasIncompleteCarriedFiles()) {
|
|
46964
|
+
warnOnceWithFallback(this.logger, "Not sweeping the files inside generated directories: this run could not read every file its sources carry, so a file it did not write may still be one it wants. The warnings above name what it could not read.");
|
|
46965
|
+
return 0;
|
|
46966
|
+
}
|
|
46967
|
+
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46968
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46969
|
+
for (const aiDir of generatedDirs) {
|
|
46970
|
+
const dirPath = aiDir.getDirPath();
|
|
46971
|
+
const { verdict, root } = locateInOwnRoot({
|
|
46972
|
+
aiDir,
|
|
46973
|
+
dirPath,
|
|
46974
|
+
outputRoot: this.outputRoot
|
|
46975
|
+
});
|
|
46976
|
+
const quotedDirPath = quoteForLog(dirPath);
|
|
46977
|
+
const quotedRoot = quoteForLog(root);
|
|
46978
|
+
if (verdict === "root-outside") {
|
|
46979
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
46980
|
+
continue;
|
|
46981
|
+
}
|
|
46982
|
+
if (!aiDir.ownsDirTree()) {
|
|
46983
|
+
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${quoteForLog(aiDir.getDirName())}: ${quotedDirPath} is a shared root, not a directory of its own`);
|
|
46984
|
+
else this.logger.warn(`Refusing to sweep ${quotedDirPath}: it does not own that directory, and it is not the shared root ${quotedRoot} it was found in either`);
|
|
46985
|
+
continue;
|
|
46986
|
+
}
|
|
46987
|
+
if (verdict !== "inside") {
|
|
46988
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: it is not a directory inside ${quotedRoot}, the root it was found in`);
|
|
46989
|
+
continue;
|
|
46990
|
+
}
|
|
46991
|
+
try {
|
|
46992
|
+
await assertWritablePathInsideRoot({
|
|
46993
|
+
rootPath: this.outputRoot,
|
|
46994
|
+
targetPath: dirPath
|
|
46995
|
+
});
|
|
46996
|
+
} catch (error) {
|
|
46997
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
46998
|
+
continue;
|
|
46999
|
+
}
|
|
47000
|
+
const generatedNames = /* @__PURE__ */ new Set();
|
|
47001
|
+
const mainFile = aiDir.getMainFile();
|
|
47002
|
+
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
47003
|
+
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
47004
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => name.toLowerCase()));
|
|
47005
|
+
let existingNames;
|
|
47006
|
+
try {
|
|
47007
|
+
existingNames = await listFilePathsRecursively(dirPath, {
|
|
47008
|
+
followSymbolicLinks: false,
|
|
47009
|
+
includeHidden: false
|
|
47010
|
+
});
|
|
47011
|
+
} catch (error) {
|
|
47012
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
47013
|
+
continue;
|
|
47014
|
+
}
|
|
47015
|
+
for (const existingName of existingNames) {
|
|
47016
|
+
const posixName = toPosixPath(existingName);
|
|
47017
|
+
if (generatedNames.has(posixName)) continue;
|
|
47018
|
+
const filePath = (0, node_path.join)(dirPath, existingName);
|
|
47019
|
+
if (generatedNamesFolded.has(posixName.toLowerCase())) {
|
|
47020
|
+
this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47021
|
+
continue;
|
|
47022
|
+
}
|
|
47023
|
+
if (isClaimed(filePath)) continue;
|
|
47024
|
+
orphanPaths.add(filePath);
|
|
47025
|
+
}
|
|
47026
|
+
}
|
|
47027
|
+
return await this.deleteOrphanPaths({
|
|
47028
|
+
paths: orphanPaths,
|
|
47029
|
+
kind: "file"
|
|
47030
|
+
});
|
|
47031
|
+
}
|
|
47032
|
+
/**
|
|
46032
47033
|
* Delete the paths the sweeps decided on, or report what a real run would
|
|
46033
47034
|
* have deleted. Shared by both halves so the dry-run wording, the quoting of
|
|
46034
47035
|
* a name that came off disk, and the count they return stay one behavior
|
|
@@ -46036,7 +47037,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46036
47037
|
*/
|
|
46037
47038
|
async deleteOrphanPaths({ paths, kind }) {
|
|
46038
47039
|
for (const targetPath of paths) {
|
|
46039
|
-
const loggedPath =
|
|
47040
|
+
const loggedPath = quoteForLog(targetPath);
|
|
46040
47041
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would delete ${kind}: ${loggedPath}`);
|
|
46041
47042
|
else {
|
|
46042
47043
|
await (kind === "directory" ? removeDirectory(targetPath) : removeFile(targetPath));
|
|
@@ -46073,11 +47074,11 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46073
47074
|
}
|
|
46074
47075
|
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath.toLowerCase()));
|
|
46075
47076
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46076
|
-
const quotedOutputRoot =
|
|
47077
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46077
47078
|
for (const aiDir of existingFlatFiles) {
|
|
46078
47079
|
const filePath = aiDir.getFlatFilePath();
|
|
46079
47080
|
if (filePath === void 0) {
|
|
46080
|
-
this.logger.warn(`Refusing to sweep ${
|
|
47081
|
+
this.logger.warn(`Refusing to sweep ${quoteForLog(aiDir.getDirName())}: it owns a directory of its own, or names no file directly under the root it was found in`);
|
|
46081
47082
|
continue;
|
|
46082
47083
|
}
|
|
46083
47084
|
const dirPath = aiDir.getDirPath();
|
|
@@ -46086,8 +47087,8 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46086
47087
|
dirPath,
|
|
46087
47088
|
outputRoot: this.outputRoot
|
|
46088
47089
|
});
|
|
46089
|
-
const quotedFilePath =
|
|
46090
|
-
const quotedRoot =
|
|
47090
|
+
const quotedFilePath = quoteForLog(filePath);
|
|
47091
|
+
const quotedRoot = quoteForLog(root);
|
|
46091
47092
|
if (verdict === "root-outside") {
|
|
46092
47093
|
this.logger.warn(`Refusing to delete ${quotedFilePath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
46093
47094
|
continue;
|
|
@@ -46829,9 +47830,9 @@ async function checkNestedSkillsRoot({ outputRoot, dirPath }) {
|
|
|
46829
47830
|
if (posixRelativePathEscapesRoot(realRelativeDirPath)) return { reason: "it resolves outside the project." };
|
|
46830
47831
|
const segments = realRelativeDirPath.split("/");
|
|
46831
47832
|
const aboveTailSegments = segments.slice(0, -CLAUDECODE_SKILLS_DIR_SEGMENTS.length);
|
|
46832
|
-
if (segments.slice(-CLAUDECODE_SKILLS_DIR_SEGMENTS.length).join("/") !== CLAUDECODE_SKILLS_DIR_POSIX_PATH) return { reason: `it resolves to ${realRelativeDirPath === "" ? "the project root" :
|
|
47833
|
+
if (segments.slice(-CLAUDECODE_SKILLS_DIR_SEGMENTS.length).join("/") !== CLAUDECODE_SKILLS_DIR_POSIX_PATH) return { reason: `it resolves to ${realRelativeDirPath === "" ? "the project root" : quoteForLog(realRelativeDirPath)}, which is not a ${CLAUDECODE_SKILLS_DIR_POSIX_PATH} directory.` };
|
|
46833
47834
|
const excludedSegment = excludedNestedScanSegment(aboveTailSegments);
|
|
46834
|
-
if (excludedSegment !== void 0) return { reason: `it resolves inside ${
|
|
47835
|
+
if (excludedSegment !== void 0) return { reason: `it resolves inside ${quoteForLog(excludedSegment)}, which the nested scan excludes.` };
|
|
46835
47836
|
return { realRelativeDirPath };
|
|
46836
47837
|
}
|
|
46837
47838
|
const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
@@ -46882,9 +47883,18 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
46882
47883
|
"disable-model-invocation": resolvedDisableModelInvocation,
|
|
46883
47884
|
"user-invocable": resolvedUserInvocable,
|
|
46884
47885
|
paths: section.paths,
|
|
46885
|
-
license:
|
|
46886
|
-
|
|
46887
|
-
|
|
47886
|
+
license: resolveLicense({
|
|
47887
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47888
|
+
section
|
|
47889
|
+
}),
|
|
47890
|
+
compatibility: resolveCompatibility({
|
|
47891
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47892
|
+
section
|
|
47893
|
+
}),
|
|
47894
|
+
metadata: resolveMetadata({
|
|
47895
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47896
|
+
section
|
|
47897
|
+
})
|
|
46888
47898
|
};
|
|
46889
47899
|
const frontmatter = {
|
|
46890
47900
|
name: rulesyncFrontmatter.name,
|
|
@@ -47084,21 +48094,25 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
47084
48094
|
*/
|
|
47085
48095
|
static async getConfiguredImportRoots({ outputRoot, global = false, logger }) {
|
|
47086
48096
|
if (global) return [];
|
|
47087
|
-
const
|
|
48097
|
+
const skillsDirPath = toPosixPath(CLAUDECODE_SKILLS_DIR_PATH);
|
|
47088
48098
|
const filteredDirPaths = filterOutPathsInGitIgnoredDirectories({
|
|
47089
48099
|
rootDir: outputRoot,
|
|
47090
|
-
filePaths: await findFilesByGlobs([
|
|
48100
|
+
filePaths: await findFilesByGlobs([`*/**/${skillsDirPath}`], {
|
|
48101
|
+
cwd: outputRoot,
|
|
47091
48102
|
type: "dir",
|
|
47092
48103
|
followSymbolicLinks: false,
|
|
47093
48104
|
ignore: [
|
|
47094
|
-
|
|
47095
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
47096
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
48105
|
+
`**/.*/**/${skillsDirPath}`,
|
|
48106
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
48107
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
47097
48108
|
]
|
|
47098
48109
|
})
|
|
47099
48110
|
}).toSorted();
|
|
47100
48111
|
const roots = [];
|
|
47101
|
-
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH
|
|
48112
|
+
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH, await resolvedRelativePath({
|
|
48113
|
+
rootPath: outputRoot,
|
|
48114
|
+
targetPath: (0, node_path.join)(outputRoot, CLAUDECODE_SKILLS_DIR_PATH)
|
|
48115
|
+
})]);
|
|
47102
48116
|
for (const dirPath of filteredDirPaths) {
|
|
47103
48117
|
const scannedDirPath = (0, node_path.resolve)(dirPath);
|
|
47104
48118
|
const check = await checkNestedSkillsRoot({
|
|
@@ -47106,7 +48120,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
47106
48120
|
dirPath: scannedDirPath
|
|
47107
48121
|
});
|
|
47108
48122
|
if ("reason" in check) {
|
|
47109
|
-
logger?.warn(`Skipping the nested Claude Code skills directory ${
|
|
48123
|
+
logger?.warn(`Skipping the nested Claude Code skills directory ${quoteForLog(scannedDirPath)}: ${check.reason} Its skills are not imported.`);
|
|
47110
48124
|
continue;
|
|
47111
48125
|
}
|
|
47112
48126
|
if (seenRealRelativeDirPaths.has(check.realRelativeDirPath)) continue;
|
|
@@ -47693,11 +48707,16 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
47693
48707
|
rootFrontmatter: rulesyncFrontmatter,
|
|
47694
48708
|
section: copilotSection
|
|
47695
48709
|
});
|
|
47696
|
-
const
|
|
48710
|
+
const license = resolveLicense({
|
|
48711
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
48712
|
+
section: copilotSection
|
|
48713
|
+
});
|
|
48714
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, license: _license, ...copilotFields } = copilotSection ?? {};
|
|
47697
48715
|
const copilotFrontmatter = {
|
|
47698
48716
|
...copilotFields,
|
|
47699
48717
|
name: rulesyncFrontmatter.name,
|
|
47700
48718
|
description: rulesyncFrontmatter.description,
|
|
48719
|
+
...license !== void 0 && { license },
|
|
47701
48720
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
47702
48721
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
47703
48722
|
};
|
|
@@ -47848,11 +48867,16 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
47848
48867
|
rootFrontmatter: rulesyncFrontmatter,
|
|
47849
48868
|
section: copilotcliSection
|
|
47850
48869
|
});
|
|
47851
|
-
const
|
|
48870
|
+
const license = resolveLicense({
|
|
48871
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
48872
|
+
section: copilotcliSection
|
|
48873
|
+
});
|
|
48874
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, license: _license, ...copilotcliFields } = copilotcliSection ?? {};
|
|
47852
48875
|
const copilotcliFrontmatter = {
|
|
47853
48876
|
...copilotcliFields,
|
|
47854
48877
|
name: rulesyncFrontmatter.name,
|
|
47855
48878
|
description: rulesyncFrontmatter.description,
|
|
48879
|
+
...license !== void 0 && { license },
|
|
47856
48880
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
47857
48881
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
47858
48882
|
};
|
|
@@ -48003,13 +49027,17 @@ var CursorSkill = class CursorSkill extends ToolSkill {
|
|
|
48003
49027
|
rootFrontmatter: rulesyncFrontmatter,
|
|
48004
49028
|
section: cursorSection
|
|
48005
49029
|
});
|
|
49030
|
+
const metadata = resolveMetadata({
|
|
49031
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49032
|
+
section: cursorSection
|
|
49033
|
+
});
|
|
48006
49034
|
const cursorFrontmatter = {
|
|
48007
49035
|
name: rulesyncFrontmatter.name,
|
|
48008
49036
|
description: rulesyncFrontmatter.description,
|
|
48009
49037
|
...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
|
|
48010
49038
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
48011
49039
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
48012
|
-
...
|
|
49040
|
+
...metadata !== void 0 && { metadata }
|
|
48013
49041
|
};
|
|
48014
49042
|
return new CursorSkill({
|
|
48015
49043
|
outputRoot,
|
|
@@ -48150,13 +49178,25 @@ var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
|
48150
49178
|
const deepagentsSection = rulesyncFrontmatter.deepagents;
|
|
48151
49179
|
const allowedTools = deepagentsSection?.["allowed-tools"];
|
|
48152
49180
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
49181
|
+
const license = resolveLicense({
|
|
49182
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49183
|
+
section: deepagentsSection
|
|
49184
|
+
});
|
|
49185
|
+
const compatibility = resolveCompatibility({
|
|
49186
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49187
|
+
section: deepagentsSection
|
|
49188
|
+
});
|
|
49189
|
+
const metadata = resolveMetadata({
|
|
49190
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49191
|
+
section: deepagentsSection
|
|
49192
|
+
});
|
|
48153
49193
|
const deepagentsFrontmatter = {
|
|
48154
49194
|
name: rulesyncFrontmatter.name,
|
|
48155
49195
|
description: rulesyncFrontmatter.description,
|
|
48156
49196
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
48157
|
-
...
|
|
48158
|
-
...
|
|
48159
|
-
...
|
|
49197
|
+
...license !== void 0 && { license },
|
|
49198
|
+
...compatibility !== void 0 && { compatibility },
|
|
49199
|
+
...metadata !== void 0 && { metadata }
|
|
48160
49200
|
};
|
|
48161
49201
|
return new DeepagentsSkill({
|
|
48162
49202
|
outputRoot,
|
|
@@ -48508,11 +49548,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
48508
49548
|
rootFrontmatter: rulesyncFrontmatter,
|
|
48509
49549
|
section: factorydroidSection
|
|
48510
49550
|
});
|
|
49551
|
+
const license = resolveLicense({
|
|
49552
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49553
|
+
section: factorydroidSection
|
|
49554
|
+
});
|
|
49555
|
+
const compatibility = resolveCompatibility({
|
|
49556
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49557
|
+
section: factorydroidSection
|
|
49558
|
+
});
|
|
49559
|
+
const metadata = resolveMetadata({
|
|
49560
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49561
|
+
section: factorydroidSection
|
|
49562
|
+
});
|
|
48511
49563
|
const { name: _sectionName, description: _sectionDescription, ...section } = factorydroidSection ?? {};
|
|
48512
49564
|
const factorydroidFrontmatter = {
|
|
48513
49565
|
name: rulesyncFrontmatter.name,
|
|
48514
49566
|
description: rulesyncFrontmatter.description,
|
|
48515
49567
|
...section,
|
|
49568
|
+
...license !== void 0 && { license },
|
|
49569
|
+
...compatibility !== void 0 && { compatibility },
|
|
49570
|
+
...metadata !== void 0 && { metadata },
|
|
48516
49571
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
48517
49572
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
48518
49573
|
};
|
|
@@ -49104,7 +50159,7 @@ const KiloSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
49104
50159
|
name: zod_mini.z.string(),
|
|
49105
50160
|
description: zod_mini.z.string(),
|
|
49106
50161
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
49107
|
-
compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
50162
|
+
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
49108
50163
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
49109
50164
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
49110
50165
|
});
|
|
@@ -49179,13 +50234,25 @@ var KiloSkill = class KiloSkill extends ToolSkill {
|
|
|
49179
50234
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49180
50235
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49181
50236
|
const kiloSection = rulesyncFrontmatter.kilo;
|
|
50237
|
+
const license = resolveLicense({
|
|
50238
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50239
|
+
section: kiloSection
|
|
50240
|
+
});
|
|
50241
|
+
const compatibility = resolveCompatibility({
|
|
50242
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50243
|
+
section: kiloSection
|
|
50244
|
+
});
|
|
50245
|
+
const metadata = resolveMetadata({
|
|
50246
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50247
|
+
section: kiloSection
|
|
50248
|
+
});
|
|
49182
50249
|
const kiloFrontmatter = {
|
|
49183
50250
|
name: rulesyncFrontmatter.name,
|
|
49184
50251
|
description: rulesyncFrontmatter.description,
|
|
49185
50252
|
...kiloSection?.["allowed-tools"] !== void 0 && { "allowed-tools": kiloSection["allowed-tools"] },
|
|
49186
|
-
...
|
|
49187
|
-
...
|
|
49188
|
-
...
|
|
50253
|
+
...license !== void 0 && { license },
|
|
50254
|
+
...compatibility !== void 0 && { compatibility },
|
|
50255
|
+
...metadata !== void 0 && { metadata }
|
|
49189
50256
|
};
|
|
49190
50257
|
const settablePaths = KiloSkill.getSettablePaths({ global });
|
|
49191
50258
|
return new KiloSkill({
|
|
@@ -49517,10 +50584,26 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
49517
50584
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49518
50585
|
const settablePaths = KiroSkill.getSettablePaths({ global });
|
|
49519
50586
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
50587
|
+
const kiroSection = rulesyncFrontmatter.kiro;
|
|
50588
|
+
const license = resolveLicense({
|
|
50589
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50590
|
+
section: kiroSection
|
|
50591
|
+
});
|
|
50592
|
+
const compatibility = resolveCompatibility({
|
|
50593
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50594
|
+
section: kiroSection
|
|
50595
|
+
});
|
|
50596
|
+
const metadata = resolveMetadata({
|
|
50597
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50598
|
+
section: kiroSection
|
|
50599
|
+
});
|
|
49520
50600
|
const kiroFrontmatter = {
|
|
49521
|
-
...
|
|
50601
|
+
...kiroSection,
|
|
49522
50602
|
name: rulesyncFrontmatter.name,
|
|
49523
|
-
description: rulesyncFrontmatter.description
|
|
50603
|
+
description: rulesyncFrontmatter.description,
|
|
50604
|
+
...license !== void 0 && { license },
|
|
50605
|
+
...compatibility !== void 0 && { compatibility },
|
|
50606
|
+
...metadata !== void 0 && { metadata }
|
|
49524
50607
|
};
|
|
49525
50608
|
return new KiroSkill({
|
|
49526
50609
|
outputRoot,
|
|
@@ -49754,15 +50837,6 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
49754
50837
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
49755
50838
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
49756
50839
|
});
|
|
49757
|
-
/**
|
|
49758
|
-
* Reads a top-level `compatibility` value from rulesync frontmatter, accepting
|
|
49759
|
-
* both the documented string form (e.g. `compatibility: opencode`) and the
|
|
49760
|
-
* legacy object form. Returns `undefined` for any other shape.
|
|
49761
|
-
*/
|
|
49762
|
-
function readTopLevelCompatibility(value) {
|
|
49763
|
-
if (typeof value === "string") return value;
|
|
49764
|
-
if (typeof value === "object" && value !== null) return value;
|
|
49765
|
-
}
|
|
49766
50840
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
49767
50841
|
constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49768
50842
|
super({
|
|
@@ -49798,9 +50872,16 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49798
50872
|
* configured path is read but never generated into. `skills.urls` is a
|
|
49799
50873
|
* remote-fetch surface and is out of scope for a file-based generator.
|
|
49800
50874
|
*
|
|
49801
|
-
* Absolute paths and paths escaping the
|
|
49802
|
-
* root is joined onto
|
|
49803
|
-
* a project config should be able to ask for.
|
|
50875
|
+
* Absolute paths and paths escaping the config directory are dropped — an
|
|
50876
|
+
* import root is joined onto that directory, and reaching outside it is not
|
|
50877
|
+
* something a project config should be able to ask for. A path is judged by
|
|
50878
|
+
* where it resolves, not by how it is spelled: a relative name that is a
|
|
50879
|
+
* symbolic link pointing out of the directory is an escape all the same and
|
|
50880
|
+
* is dropped too. A path that does not exist has nothing to resolve and is
|
|
50881
|
+
* compared as spelled against the resolved config directory, so it survives
|
|
50882
|
+
* only when that directory resolves to its own spelling and is dropped when
|
|
50883
|
+
* the directory is itself reached through a link. Either way it yields no
|
|
50884
|
+
* skills — the scan finds no directory under it.
|
|
49804
50885
|
*
|
|
49805
50886
|
* @see https://opencode.ai/config.json
|
|
49806
50887
|
*/
|
|
@@ -49814,7 +50895,12 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49814
50895
|
outputRoot,
|
|
49815
50896
|
global
|
|
49816
50897
|
});
|
|
49817
|
-
|
|
50898
|
+
const lexicallyContained = skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !(0, node_path.isAbsolute)(candidate) && !(0, node_path.normalize)(candidate).startsWith(".."));
|
|
50899
|
+
const escapes = await Promise.all(lexicallyContained.map((candidate) => resolvedPathEscapesRoot({
|
|
50900
|
+
rootPath: configDir,
|
|
50901
|
+
targetPath: (0, node_path.join)(configDir, candidate)
|
|
50902
|
+
})));
|
|
50903
|
+
return lexicallyContained.filter((_, index) => !escapes[index]).map((relativeDirPath) => ({
|
|
49818
50904
|
outputRoot: configDir,
|
|
49819
50905
|
relativeDirPath
|
|
49820
50906
|
}));
|
|
@@ -49868,13 +50954,18 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49868
50954
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49869
50955
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49870
50956
|
const opencodeSection = rulesyncFrontmatter.opencode;
|
|
49871
|
-
const
|
|
49872
|
-
|
|
49873
|
-
|
|
49874
|
-
|
|
49875
|
-
const
|
|
49876
|
-
|
|
49877
|
-
|
|
50957
|
+
const license = resolveLicense({
|
|
50958
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50959
|
+
section: opencodeSection
|
|
50960
|
+
});
|
|
50961
|
+
const compatibility = resolveCompatibility({
|
|
50962
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50963
|
+
section: opencodeSection
|
|
50964
|
+
});
|
|
50965
|
+
const metadata = resolveMetadata({
|
|
50966
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50967
|
+
section: opencodeSection
|
|
50968
|
+
});
|
|
49878
50969
|
const opencodeFrontmatter = {
|
|
49879
50970
|
name: rulesyncFrontmatter.name,
|
|
49880
50971
|
description: rulesyncFrontmatter.description,
|
|
@@ -50043,11 +51134,26 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
50043
51134
|
});
|
|
50044
51135
|
const { "allowed-tools": allowedTools, ...piSectionRest } = piSection ?? {};
|
|
50045
51136
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
51137
|
+
const license = resolveLicense({
|
|
51138
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51139
|
+
section: piSection
|
|
51140
|
+
});
|
|
51141
|
+
const compatibility = resolveCompatibility({
|
|
51142
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51143
|
+
section: piSection
|
|
51144
|
+
});
|
|
51145
|
+
const metadata = resolveMetadata({
|
|
51146
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51147
|
+
section: piSection
|
|
51148
|
+
});
|
|
50046
51149
|
const piFrontmatter = {
|
|
50047
51150
|
name: rulesyncFrontmatter.name,
|
|
50048
51151
|
description: rulesyncFrontmatter.description,
|
|
50049
51152
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
50050
51153
|
...piSectionRest,
|
|
51154
|
+
...license !== void 0 && { license },
|
|
51155
|
+
...compatibility !== void 0 && { compatibility },
|
|
51156
|
+
...metadata !== void 0 && { metadata },
|
|
50051
51157
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
50052
51158
|
};
|
|
50053
51159
|
return new PiSkill({
|
|
@@ -50529,11 +51635,26 @@ var ReplitSkill = class ReplitSkill extends ToolSkill {
|
|
|
50529
51635
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
50530
51636
|
const { "allowed-tools": allowedTools, ...replitSection } = rulesyncFrontmatter.replit ?? {};
|
|
50531
51637
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
51638
|
+
const license = resolveLicense({
|
|
51639
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51640
|
+
section: replitSection
|
|
51641
|
+
});
|
|
51642
|
+
const compatibility = resolveCompatibility({
|
|
51643
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51644
|
+
section: replitSection
|
|
51645
|
+
});
|
|
51646
|
+
const metadata = resolveMetadata({
|
|
51647
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51648
|
+
section: replitSection
|
|
51649
|
+
});
|
|
50532
51650
|
const replitFrontmatter = {
|
|
50533
51651
|
name: rulesyncFrontmatter.name,
|
|
50534
51652
|
description: rulesyncFrontmatter.description,
|
|
50535
51653
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
50536
|
-
...replitSection
|
|
51654
|
+
...replitSection,
|
|
51655
|
+
...license !== void 0 && { license },
|
|
51656
|
+
...compatibility !== void 0 && { compatibility },
|
|
51657
|
+
...metadata !== void 0 && { metadata }
|
|
50537
51658
|
};
|
|
50538
51659
|
return new ReplitSkill({
|
|
50539
51660
|
outputRoot,
|
|
@@ -50886,30 +52007,24 @@ const VibeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
50886
52007
|
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
50887
52008
|
"allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
50888
52009
|
});
|
|
50889
|
-
/** Resolve the top-level `license` field, if present as a string. */
|
|
50890
|
-
function resolveTopLevelLicense(looseTopLevel) {
|
|
50891
|
-
return typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
|
|
50892
|
-
}
|
|
50893
|
-
/** Resolve the top-level `compatibility` field (string or object), if present. */
|
|
50894
|
-
function resolveTopLevelCompatibility(looseTopLevel) {
|
|
50895
|
-
const value = looseTopLevel.compatibility;
|
|
50896
|
-
if (typeof value === "string" || typeof value === "object" && value !== null) return value;
|
|
50897
|
-
}
|
|
50898
|
-
/** Resolve the top-level `metadata` field (object), if present. */
|
|
50899
|
-
function resolveTopLevelMetadata(looseTopLevel) {
|
|
50900
|
-
const value = looseTopLevel.metadata;
|
|
50901
|
-
return typeof value === "object" && value !== null ? value : void 0;
|
|
50902
|
-
}
|
|
50903
52010
|
/**
|
|
50904
52011
|
* Build the Vibe frontmatter from a rulesync skill frontmatter, preferring the
|
|
50905
|
-
* dedicated `vibe` section over
|
|
52012
|
+
* dedicated `vibe` section over the shared root-level fields.
|
|
50906
52013
|
*/
|
|
50907
52014
|
function buildVibeFrontmatter(rulesyncFrontmatter) {
|
|
50908
52015
|
const vibeSection = rulesyncFrontmatter.vibe;
|
|
50909
|
-
const
|
|
50910
|
-
|
|
50911
|
-
|
|
50912
|
-
|
|
52016
|
+
const license = resolveLicense({
|
|
52017
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
52018
|
+
section: vibeSection
|
|
52019
|
+
});
|
|
52020
|
+
const compatibility = resolveCompatibility({
|
|
52021
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
52022
|
+
section: vibeSection
|
|
52023
|
+
});
|
|
52024
|
+
const metadata = resolveMetadata({
|
|
52025
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
52026
|
+
section: vibeSection
|
|
52027
|
+
});
|
|
50913
52028
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
50914
52029
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50915
52030
|
section: vibeSection
|
|
@@ -50917,9 +52032,9 @@ function buildVibeFrontmatter(rulesyncFrontmatter) {
|
|
|
50917
52032
|
return {
|
|
50918
52033
|
name: rulesyncFrontmatter.name,
|
|
50919
52034
|
description: rulesyncFrontmatter.description,
|
|
50920
|
-
...
|
|
50921
|
-
...
|
|
50922
|
-
...
|
|
52035
|
+
...license !== void 0 && { license },
|
|
52036
|
+
...compatibility !== void 0 && { compatibility },
|
|
52037
|
+
...metadata !== void 0 && { metadata },
|
|
50923
52038
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
50924
52039
|
...vibeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": vibeSection["allowed-tools"] }
|
|
50925
52040
|
};
|
|
@@ -51899,7 +53014,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
51899
53014
|
global: this.global
|
|
51900
53015
|
});
|
|
51901
53016
|
if (dirWriteBlockReason !== void 0 && dirWriteBlockReason !== null) {
|
|
51902
|
-
this.logger.warn(`Skipping skill ${
|
|
53017
|
+
this.logger.warn(`Skipping skill ${quoteForLog(dirName)} for '${this.toolTarget}': ${dirWriteBlockReason}`);
|
|
51903
53018
|
return null;
|
|
51904
53019
|
}
|
|
51905
53020
|
return factory.class.fromRulesyncSkill({
|
|
@@ -52055,7 +53170,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52055
53170
|
};
|
|
52056
53171
|
} catch (error) {
|
|
52057
53172
|
if (!isLenientRoot) throw error;
|
|
52058
|
-
this.logger.warn(`Skipping ${
|
|
53173
|
+
this.logger.warn(`Skipping ${quoteForLog(sourcePath)}: ` + stripControlCharacters(formatError(error)));
|
|
52059
53174
|
return null;
|
|
52060
53175
|
}
|
|
52061
53176
|
}))).filter((loaded) => loaded !== null);
|
|
@@ -52086,7 +53201,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52086
53201
|
};
|
|
52087
53202
|
} catch (error) {
|
|
52088
53203
|
if (!isLenientRoot) throw error;
|
|
52089
|
-
this.logger.warn(`Skipping ${
|
|
53204
|
+
this.logger.warn(`Skipping ${quoteForLog(sourcePath)}: ` + stripControlCharacters(formatError(error)));
|
|
52090
53205
|
return null;
|
|
52091
53206
|
}
|
|
52092
53207
|
}))).filter((loaded) => loaded !== null);
|
|
@@ -52114,7 +53229,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52114
53229
|
return names.filter((name) => {
|
|
52115
53230
|
if (isAddressableSkillName(kind === "file" ? (0, node_path.basename)(name, ".md") : name)) return true;
|
|
52116
53231
|
const consequence = kind === "file" ? "skill name cannot contain a path separator, so this file is neither imported nor swept as an orphan. Rename it by hand." : "skill directory name cannot contain a path separator, so this directory is neither generated from nor swept as an orphan. Rename or remove it by hand.";
|
|
52117
|
-
warnOnceWithFallback(this.logger, `Skipping ${
|
|
53232
|
+
warnOnceWithFallback(this.logger, `Skipping ${quoteForLog((0, node_path.join)(dirPath, name))}: a ${consequence}`);
|
|
52118
53233
|
return false;
|
|
52119
53234
|
});
|
|
52120
53235
|
}
|
|
@@ -57050,7 +58165,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57050
58165
|
supportsProject: true,
|
|
57051
58166
|
supportsSimulated: false,
|
|
57052
58167
|
supportsGlobal: true,
|
|
57053
|
-
filePattern:
|
|
58168
|
+
filePattern: "*/AGENTS.md"
|
|
57054
58169
|
}
|
|
57055
58170
|
}],
|
|
57056
58171
|
["devin", {
|
|
@@ -57059,7 +58174,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57059
58174
|
supportsProject: true,
|
|
57060
58175
|
supportsSimulated: false,
|
|
57061
58176
|
supportsGlobal: true,
|
|
57062
|
-
filePattern:
|
|
58177
|
+
filePattern: "*/AGENT.md"
|
|
57063
58178
|
}
|
|
57064
58179
|
}],
|
|
57065
58180
|
["factorydroid", {
|
|
@@ -57149,7 +58264,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57149
58264
|
supportsProject: true,
|
|
57150
58265
|
supportsSimulated: false,
|
|
57151
58266
|
supportsGlobal: true,
|
|
57152
|
-
filePattern:
|
|
58267
|
+
filePattern: "**/*.md"
|
|
57153
58268
|
}
|
|
57154
58269
|
}],
|
|
57155
58270
|
["opencode", {
|
|
@@ -57176,7 +58291,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57176
58291
|
supportsProject: true,
|
|
57177
58292
|
supportsSimulated: false,
|
|
57178
58293
|
supportsGlobal: true,
|
|
57179
|
-
filePattern:
|
|
58294
|
+
filePattern: "*/SKILL.md"
|
|
57180
58295
|
}
|
|
57181
58296
|
}],
|
|
57182
58297
|
["roo", {
|
|
@@ -57413,7 +58528,10 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
57413
58528
|
rootPath: rootOutputRoot,
|
|
57414
58529
|
targetPath: baseDir
|
|
57415
58530
|
});
|
|
57416
|
-
const subagentFilePaths = await findFilesByGlobs(
|
|
58531
|
+
const subagentFilePaths = await findFilesByGlobs(factory.meta.filePattern, {
|
|
58532
|
+
cwd: baseDir,
|
|
58533
|
+
followSymbolicLinks: !forDeletion
|
|
58534
|
+
});
|
|
57417
58535
|
const toRelativeFilePath = (path) => (0, node_path.relative)(baseDir, path);
|
|
57418
58536
|
let ownedFilePaths = subagentFilePaths;
|
|
57419
58537
|
if (factory.class.isFileOwned) {
|
|
@@ -57636,15 +58754,14 @@ var ToolRule = class extends ToolFile {
|
|
|
57636
58754
|
* than files under a rulesync-owned directory, so enumerating them for
|
|
57637
58755
|
* `--delete` would sweep away work rulesync never wrote.
|
|
57638
58756
|
*/
|
|
57639
|
-
static buildNestedFilePatterns({
|
|
57640
|
-
const root = toPosixPath(outputRoot);
|
|
58757
|
+
static buildNestedFilePatterns({ fileName }) {
|
|
57641
58758
|
return {
|
|
57642
|
-
include: [
|
|
58759
|
+
include: [`**/${fileName}`],
|
|
57643
58760
|
ignore: [
|
|
57644
|
-
|
|
57645
|
-
|
|
57646
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
57647
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
58761
|
+
fileName,
|
|
58762
|
+
"**/.*/**",
|
|
58763
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
58764
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
57648
58765
|
]
|
|
57649
58766
|
};
|
|
57650
58767
|
}
|
|
@@ -57805,11 +58922,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
|
|
|
57805
58922
|
*
|
|
57806
58923
|
* @see https://agents.md/
|
|
57807
58924
|
*/
|
|
57808
|
-
static getNestedFilePatterns(
|
|
57809
|
-
return this.buildNestedFilePatterns({
|
|
57810
|
-
outputRoot,
|
|
57811
|
-
fileName: AGENTSMD_RULE_FILE_NAME
|
|
57812
|
-
});
|
|
58925
|
+
static getNestedFilePatterns() {
|
|
58926
|
+
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
57813
58927
|
}
|
|
57814
58928
|
/**
|
|
57815
58929
|
* The subproject directory this rule scopes, or `undefined` for the project
|
|
@@ -58758,6 +59872,78 @@ var AugmentcodeRule = class AugmentcodeRule extends ToolRule {
|
|
|
58758
59872
|
}
|
|
58759
59873
|
};
|
|
58760
59874
|
//#endregion
|
|
59875
|
+
//#region src/features/rules/claudecode-language-settings.ts
|
|
59876
|
+
/**
|
|
59877
|
+
* Claude Code's native home for the root `language` key of `rulesync.jsonc`.
|
|
59878
|
+
*
|
|
59879
|
+
* Claude Code reads a top-level `language` setting, so for this target the
|
|
59880
|
+
* rules feature writes the preference there instead of appending a prompt
|
|
59881
|
+
* block to CLAUDE.md. The key is patched into the existing settings file
|
|
59882
|
+
* through the shared config gateway (the `rules` feature owns `language`
|
|
59883
|
+
* there and nothing else), so hooks, permissions, and deny lists written by
|
|
59884
|
+
* sibling features — and everything the user authored — are left alone.
|
|
59885
|
+
*
|
|
59886
|
+
* Removing `language` from `rulesync.jsonc` later does not retract the key
|
|
59887
|
+
* already written: the settings file is shared with the user's own
|
|
59888
|
+
* configuration, so nothing is ever deleted from it, and the value stays
|
|
59889
|
+
* until the user removes it by hand.
|
|
59890
|
+
*/
|
|
59891
|
+
var ClaudecodeLanguageSettings = class ClaudecodeLanguageSettings extends ToolFile {
|
|
59892
|
+
/**
|
|
59893
|
+
* Project scope goes to `.claude/settings.local.json`: a response language
|
|
59894
|
+
* is a per-developer preference, and the local file is the one Claude Code
|
|
59895
|
+
* keeps out of version control. Global scope goes to `~/.claude/settings.json`
|
|
59896
|
+
* because Claude Code reads no `~/.claude/settings.local.json`.
|
|
59897
|
+
*/
|
|
59898
|
+
static getSettablePaths({ global = false } = {}) {
|
|
59899
|
+
return {
|
|
59900
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
59901
|
+
relativeFilePath: global ? CLAUDECODE_SETTINGS_FILE_NAME : CLAUDECODE_SETTINGS_LOCAL_FILE_NAME
|
|
59902
|
+
};
|
|
59903
|
+
}
|
|
59904
|
+
static async fromLanguage({ outputRoot = process.cwd(), language, global = false, validate = true }) {
|
|
59905
|
+
const paths = this.getSettablePaths({ global });
|
|
59906
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
59907
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
59908
|
+
return new ClaudecodeLanguageSettings({
|
|
59909
|
+
outputRoot,
|
|
59910
|
+
relativeDirPath: paths.relativeDirPath,
|
|
59911
|
+
relativeFilePath: paths.relativeFilePath,
|
|
59912
|
+
fileContent: applySharedConfigPatch({
|
|
59913
|
+
fileKey: sharedConfigFileKey(paths),
|
|
59914
|
+
feature: "rules",
|
|
59915
|
+
existingContent,
|
|
59916
|
+
patch: { language: getClaudecodeLanguageValue(language) },
|
|
59917
|
+
filePath
|
|
59918
|
+
}),
|
|
59919
|
+
validate,
|
|
59920
|
+
global
|
|
59921
|
+
});
|
|
59922
|
+
}
|
|
59923
|
+
/**
|
|
59924
|
+
* A settings file the user (and other features) share: never swept as an
|
|
59925
|
+
* orphan, even though the rules feature stops writing it when `language`
|
|
59926
|
+
* is unset again.
|
|
59927
|
+
*/
|
|
59928
|
+
isDeletable() {
|
|
59929
|
+
return false;
|
|
59930
|
+
}
|
|
59931
|
+
validate() {
|
|
59932
|
+
try {
|
|
59933
|
+
JSON.parse(this.fileContent);
|
|
59934
|
+
return {
|
|
59935
|
+
success: true,
|
|
59936
|
+
error: null
|
|
59937
|
+
};
|
|
59938
|
+
} catch (error) {
|
|
59939
|
+
return {
|
|
59940
|
+
success: false,
|
|
59941
|
+
error: new Error(`Invalid JSON in ${this.getRelativePathFromCwd()}: ${formatError(error)}`, { cause: error })
|
|
59942
|
+
};
|
|
59943
|
+
}
|
|
59944
|
+
}
|
|
59945
|
+
};
|
|
59946
|
+
//#endregion
|
|
58761
59947
|
//#region src/features/rules/claudecode-legacy-rule.ts
|
|
58762
59948
|
/**
|
|
58763
59949
|
* Legacy rule generator for Claude Code AI assistant
|
|
@@ -58893,6 +60079,15 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
|
|
|
58893
60079
|
nonRoot: { relativeDirPath: buildToolPath(CLAUDECODE_DIR, CLAUDECODE_RULES_DIR_NAME, excludeToolDir) }
|
|
58894
60080
|
};
|
|
58895
60081
|
}
|
|
60082
|
+
/**
|
|
60083
|
+
* The settings file the rules feature patches `language` into (see
|
|
60084
|
+
* {@link ClaudecodeLanguageSettings}): not a rule path, so `getSettablePaths`
|
|
60085
|
+
* does not report it, but the shared-config gateway must know the rules
|
|
60086
|
+
* feature writes there.
|
|
60087
|
+
*/
|
|
60088
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
60089
|
+
return [ClaudecodeLanguageSettings.getSettablePaths({ global })];
|
|
60090
|
+
}
|
|
58896
60091
|
constructor({ frontmatter, body, ...rest }) {
|
|
58897
60092
|
if (rest.validate) {
|
|
58898
60093
|
const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -59598,6 +60793,12 @@ const CursorRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
59598
60793
|
globs: zod_mini.z.optional(zod_mini.z.string()),
|
|
59599
60794
|
alwaysApply: zod_mini.z.optional(zod_mini.z.boolean())
|
|
59600
60795
|
});
|
|
60796
|
+
/**
|
|
60797
|
+
* Globs that match the whole project, and so say nothing `alwaysApply: true`
|
|
60798
|
+
* does not already say. Same pair the Cline and Qwen Code adapters treat as
|
|
60799
|
+
* universal.
|
|
60800
|
+
*/
|
|
60801
|
+
const UNIVERSAL_GLOBS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
59601
60802
|
var CursorRule = class CursorRule extends ToolRule {
|
|
59602
60803
|
frontmatter;
|
|
59603
60804
|
body;
|
|
@@ -59626,7 +60827,10 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59626
60827
|
const rawDescription = frontmatter.description;
|
|
59627
60828
|
const description = typeof rawDescription === "string" ? rawDescription.replace(/\n+/g, " ").trim() : rawDescription;
|
|
59628
60829
|
if (description) lines.push((0, js_yaml.dump)({ description }, { lineWidth: -1 }).trimEnd());
|
|
59629
|
-
if (frontmatter.globs !== void 0)
|
|
60830
|
+
if (frontmatter.globs !== void 0) {
|
|
60831
|
+
const globs = String(frontmatter.globs).replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim();
|
|
60832
|
+
lines.push(`globs: ${globs}`);
|
|
60833
|
+
}
|
|
59630
60834
|
lines.push("---");
|
|
59631
60835
|
lines.push("");
|
|
59632
60836
|
if (body) lines.push(body);
|
|
@@ -59644,11 +60848,12 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59644
60848
|
toRulesyncRule() {
|
|
59645
60849
|
const targets = ["*"];
|
|
59646
60850
|
const isAlways = this.frontmatter.alwaysApply === true;
|
|
59647
|
-
const
|
|
59648
|
-
|
|
59649
|
-
|
|
59650
|
-
|
|
59651
|
-
|
|
60851
|
+
const rawGlobs = this.frontmatter.globs;
|
|
60852
|
+
const sourceGlobs = rawGlobs && rawGlobs.trim() !== "" ? rawGlobs.split(",").map((g) => g.trim()).filter((g) => g.length > 0) : [];
|
|
60853
|
+
const globs = sourceGlobs.length === 0 && isAlways ? ["**/*"] : sourceGlobs;
|
|
60854
|
+
let cursorGlobs;
|
|
60855
|
+
if (sourceGlobs.length > 0) cursorGlobs = sourceGlobs;
|
|
60856
|
+
else if (isAlways) cursorGlobs = [];
|
|
59652
60857
|
return new RulesyncRule({
|
|
59653
60858
|
frontmatter: {
|
|
59654
60859
|
targets,
|
|
@@ -59658,7 +60863,7 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59658
60863
|
cursor: {
|
|
59659
60864
|
alwaysApply: this.frontmatter.alwaysApply,
|
|
59660
60865
|
description: this.frontmatter.description,
|
|
59661
|
-
globs:
|
|
60866
|
+
globs: cursorGlobs
|
|
59662
60867
|
}
|
|
59663
60868
|
},
|
|
59664
60869
|
body: this.body,
|
|
@@ -59668,21 +60873,44 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59668
60873
|
});
|
|
59669
60874
|
}
|
|
59670
60875
|
/**
|
|
59671
|
-
* Resolve
|
|
59672
|
-
*
|
|
59673
|
-
*
|
|
59674
|
-
*
|
|
60876
|
+
* Resolve the Cursor `globs` string with priority: cursor-specific > parent.
|
|
60877
|
+
*
|
|
60878
|
+
* The two ways `cursorSpecificGlobs` can be absent are deliberately different
|
|
60879
|
+
* and must stay that way: `undefined` means "no Cursor-specific opinion", so
|
|
60880
|
+
* the canonical globs are used, while an explicit `[]` means "this rule has
|
|
60881
|
+
* no Cursor globs" and wins over them. Collapsing the two -- say, to
|
|
60882
|
+
* `cursorSpecificGlobs?.length ? … : parentGlobs` -- would put a universal
|
|
60883
|
+
* canonical glob back onto every Always Apply rule.
|
|
60884
|
+
*
|
|
60885
|
+
* A universal glob (see {@link UNIVERSAL_GLOBS}) is dropped outright when the
|
|
60886
|
+
* rule is Always Apply. `alwaysApply: true` already applies the rule
|
|
60887
|
+
* everywhere, Cursor's docs say globs are ignored once it is set, and
|
|
60888
|
+
* Cursor's own staff describe the two together as a semantic conflict that
|
|
60889
|
+
* some versions resolve by classifying the rule as a glob rule instead of an
|
|
60890
|
+
* Always one. Dropping it here is what heals, on the next generate, both a
|
|
60891
|
+
* `.rulesync` file an older version wrote with the invented universal glob in
|
|
60892
|
+
* `cursor.globs` and a rule hand-written with a universal canonical glob
|
|
60893
|
+
* beside `cursor.alwaysApply: true`. Specific globs are left alone: they say
|
|
60894
|
+
* something a flag cannot, so they are the author's to keep even alongside
|
|
60895
|
+
* it.
|
|
59675
60896
|
*/
|
|
59676
|
-
static resolveCursorGlobs(cursorSpecificGlobs, parentGlobs) {
|
|
60897
|
+
static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
|
|
59677
60898
|
const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
|
|
59678
|
-
|
|
60899
|
+
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
60900
|
+
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS.has(glob.trim()))) return;
|
|
60901
|
+
return targetGlobs.join(",");
|
|
59679
60902
|
}
|
|
59680
60903
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
59681
60904
|
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
60905
|
+
const alwaysApply = rulesyncFrontmatter.cursor?.alwaysApply;
|
|
59682
60906
|
const cursorFrontmatter = {
|
|
59683
60907
|
description: rulesyncFrontmatter.description,
|
|
59684
|
-
globs: this.resolveCursorGlobs(
|
|
59685
|
-
|
|
60908
|
+
globs: this.resolveCursorGlobs({
|
|
60909
|
+
cursorSpecificGlobs: rulesyncFrontmatter.cursor?.globs,
|
|
60910
|
+
parentGlobs: rulesyncFrontmatter.globs,
|
|
60911
|
+
alwaysApply: alwaysApply === true
|
|
60912
|
+
}),
|
|
60913
|
+
alwaysApply
|
|
59686
60914
|
};
|
|
59687
60915
|
const body = rulesyncRule.getBody();
|
|
59688
60916
|
const newFileName = `${rulesyncRule.getRelativeFilePath().replace(/\.md$/, "")}.mdc`;
|
|
@@ -61007,15 +62235,14 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
61007
62235
|
* never wrote.
|
|
61008
62236
|
* @see https://kiro.dev/docs/steering/
|
|
61009
62237
|
*/
|
|
61010
|
-
static getNestedFilePatterns(
|
|
61011
|
-
const root = toPosixPath(outputRoot);
|
|
62238
|
+
static getNestedFilePatterns() {
|
|
61012
62239
|
return {
|
|
61013
|
-
include: [
|
|
62240
|
+
include: [`**/${KIRO_NESTED_STEERING_FILE_NAME}`],
|
|
61014
62241
|
ignore: [
|
|
61015
|
-
|
|
61016
|
-
|
|
61017
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
61018
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
62242
|
+
KIRO_NESTED_STEERING_FILE_NAME,
|
|
62243
|
+
"**/.*/**",
|
|
62244
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
62245
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
61019
62246
|
]
|
|
61020
62247
|
};
|
|
61021
62248
|
}
|
|
@@ -61645,8 +62872,8 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
|
|
|
61645
62872
|
* The personal local context file lives under `.qwen/`, not at the project
|
|
61646
62873
|
* root where the settable root path points, so override the deletion glob.
|
|
61647
62874
|
*/
|
|
61648
|
-
static getLocalRootFileGlob({
|
|
61649
|
-
return
|
|
62875
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
62876
|
+
return node_path.posix.join(toPosixPath(QWENCODE_DIR), fileName);
|
|
61650
62877
|
}
|
|
61651
62878
|
};
|
|
61652
62879
|
//#endregion
|
|
@@ -61673,11 +62900,8 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
|
|
|
61673
62900
|
* (same exclusions, import-only, project scope).
|
|
61674
62901
|
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/v1.18.0/docs/SESSION_MEMORY_RETRIEVAL.md
|
|
61675
62902
|
*/
|
|
61676
|
-
static getNestedFilePatterns(
|
|
61677
|
-
return this.buildNestedFilePatterns({
|
|
61678
|
-
outputRoot,
|
|
61679
|
-
fileName: REASONIX_RULE_FILE_NAME
|
|
61680
|
-
});
|
|
62903
|
+
static getNestedFilePatterns() {
|
|
62904
|
+
return this.buildNestedFilePatterns({ fileName: REASONIX_RULE_FILE_NAME });
|
|
61681
62905
|
}
|
|
61682
62906
|
/**
|
|
61683
62907
|
* The subproject directory this rule scopes, or `undefined` for the root
|
|
@@ -61934,9 +63158,9 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
61934
63158
|
* generic directory is the only one the deletion sweep enumerates, because a
|
|
61935
63159
|
* `rules-*` glob would also match mode rules a user wrote by hand.
|
|
61936
63160
|
*/
|
|
61937
|
-
static getNestedFilePatterns(
|
|
63161
|
+
static getNestedFilePatterns() {
|
|
61938
63162
|
return {
|
|
61939
|
-
include: [`${toPosixPath(
|
|
63163
|
+
include: [`${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
|
|
61940
63164
|
ignore: []
|
|
61941
63165
|
};
|
|
61942
63166
|
}
|
|
@@ -61974,8 +63198,8 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
61974
63198
|
* Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
|
|
61975
63199
|
* at the project root, not under `.roo/` (mirrors rovodev).
|
|
61976
63200
|
*/
|
|
61977
|
-
static getLocalRootFileGlob({
|
|
61978
|
-
return
|
|
63201
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
63202
|
+
return fileName;
|
|
61979
63203
|
}
|
|
61980
63204
|
};
|
|
61981
63205
|
//#endregion
|
|
@@ -62155,15 +63379,15 @@ var RovodevRule = class RovodevRule extends ToolRule {
|
|
|
62155
63379
|
root: true
|
|
62156
63380
|
})];
|
|
62157
63381
|
},
|
|
62158
|
-
getMirrorDeletionGlobs: (
|
|
62159
|
-
primaryGlob:
|
|
62160
|
-
mirrorGlob:
|
|
63382
|
+
getMirrorDeletionGlobs: () => ({
|
|
63383
|
+
primaryGlob: node_path.posix.join(toPosixPath(ROVODEV_DIR), ROVODEV_RULE_FILE_NAME),
|
|
63384
|
+
mirrorGlob: ROVODEV_RULE_FILE_NAME
|
|
62161
63385
|
})
|
|
62162
63386
|
};
|
|
62163
63387
|
}
|
|
62164
63388
|
/** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
|
|
62165
|
-
static getLocalRootFileGlob({
|
|
62166
|
-
return
|
|
63389
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
63390
|
+
return fileName;
|
|
62167
63391
|
}
|
|
62168
63392
|
};
|
|
62169
63393
|
//#endregion
|
|
@@ -62303,11 +63527,8 @@ var VibeRule = class VibeRule extends ToolRule {
|
|
|
62303
63527
|
* literally the same files.
|
|
62304
63528
|
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
62305
63529
|
*/
|
|
62306
|
-
static getNestedFilePatterns(
|
|
62307
|
-
return this.buildNestedFilePatterns({
|
|
62308
|
-
outputRoot,
|
|
62309
|
-
fileName: AGENTSMD_RULE_FILE_NAME
|
|
62310
|
-
});
|
|
63530
|
+
static getNestedFilePatterns() {
|
|
63531
|
+
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
62311
63532
|
}
|
|
62312
63533
|
/**
|
|
62313
63534
|
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
@@ -63058,21 +64279,34 @@ const MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS = 10;
|
|
|
63058
64279
|
* the tool will read" — can keep the two apart. A legacy root is a file
|
|
63059
64280
|
* Rulesync reads but never writes, and the difference matters to them.
|
|
63060
64281
|
*/
|
|
63061
|
-
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob) => {
|
|
64282
|
+
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob, outputRoot) => {
|
|
63062
64283
|
if (primaryFilePaths.length > 0) return primaryFilePaths;
|
|
63063
|
-
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
64284
|
+
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob), { cwd: outputRoot });
|
|
63064
64285
|
return [];
|
|
63065
64286
|
};
|
|
64287
|
+
/**
|
|
64288
|
+
* A project-root-relative glob for a file a tool keeps at a fixed path, joined
|
|
64289
|
+
* with `/` because a glob is always posix-separated.
|
|
64290
|
+
*
|
|
64291
|
+
* Relative because the root goes to `findFilesByGlobs` as `cwd` rather than into
|
|
64292
|
+
* the pattern: a project directory named `project(a)` or `project{a,b}` would
|
|
64293
|
+
* otherwise be read as a glob and match nothing at all — and on a `--delete`
|
|
64294
|
+
* sweep an empty result reads as "every source was removed", so rulesync would
|
|
64295
|
+
* delete generated files it can no longer regenerate and report success.
|
|
64296
|
+
*/
|
|
64297
|
+
const rootRelativeGlob = (...segments) => node_path.posix.join(...segments.filter((segment) => segment !== void 0).map(toPosixPath));
|
|
63066
64298
|
var RulesProcessor = class extends FeatureProcessor {
|
|
63067
64299
|
toolTarget;
|
|
63068
64300
|
simulateCommands;
|
|
63069
64301
|
simulateSubagents;
|
|
63070
64302
|
simulateSkills;
|
|
64303
|
+
language;
|
|
64304
|
+
deriveSubprojectPathFromGlobs;
|
|
63071
64305
|
global;
|
|
63072
64306
|
getFactory;
|
|
63073
64307
|
skills;
|
|
63074
64308
|
featureOptions;
|
|
63075
|
-
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
64309
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, language, deriveSubprojectPathFromGlobs = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
63076
64310
|
super({
|
|
63077
64311
|
outputRoot,
|
|
63078
64312
|
inputRoots,
|
|
@@ -63086,6 +64320,8 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63086
64320
|
this.simulateCommands = simulateCommands;
|
|
63087
64321
|
this.simulateSubagents = simulateSubagents;
|
|
63088
64322
|
this.simulateSkills = simulateSkills;
|
|
64323
|
+
this.language = language;
|
|
64324
|
+
this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs;
|
|
63089
64325
|
this.getFactory = getFactory;
|
|
63090
64326
|
this.skills = skills;
|
|
63091
64327
|
this.featureOptions = featureOptions;
|
|
@@ -63129,8 +64365,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63129
64365
|
});
|
|
63130
64366
|
this.applyRootRuleSections({
|
|
63131
64367
|
toolRules,
|
|
63132
|
-
factory
|
|
64368
|
+
factory,
|
|
64369
|
+
convertedRules
|
|
63133
64370
|
});
|
|
64371
|
+
extraFiles.push(...await this.buildLanguageSettingsFiles());
|
|
63134
64372
|
const outputFiles = [...toolRules, ...extraFiles];
|
|
63135
64373
|
this.warnForOutputPathCollisions({
|
|
63136
64374
|
outputFiles,
|
|
@@ -63188,7 +64426,8 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63188
64426
|
outputRoot: this.outputRoot,
|
|
63189
64427
|
instructions: instructionPaths,
|
|
63190
64428
|
validate: true,
|
|
63191
|
-
global: this.global
|
|
64429
|
+
global: this.global,
|
|
64430
|
+
logger: this.logger
|
|
63192
64431
|
});
|
|
63193
64432
|
return registered ? [registered] : [];
|
|
63194
64433
|
}
|
|
@@ -63197,11 +64436,19 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63197
64436
|
* reference and conventions sections to the root rule content. Mutates the
|
|
63198
64437
|
* root rule in place.
|
|
63199
64438
|
*/
|
|
63200
|
-
applyRootRuleSections({ toolRules, factory }) {
|
|
64439
|
+
applyRootRuleSections({ toolRules, factory, convertedRules }) {
|
|
63201
64440
|
const { meta } = factory;
|
|
63202
64441
|
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
63203
|
-
if (!rootRule)
|
|
63204
|
-
|
|
64442
|
+
if (!rootRule) {
|
|
64443
|
+
this.appendLanguageBlockToRootSourceRules({ convertedRules });
|
|
64444
|
+
return;
|
|
64445
|
+
}
|
|
64446
|
+
const assembledContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
|
|
64447
|
+
const promptLanguage = this.getPromptBlockLanguage();
|
|
64448
|
+
const newContent = promptLanguage === void 0 ? assembledContent : appendLanguageBlock({
|
|
64449
|
+
content: assembledContent,
|
|
64450
|
+
language: promptLanguage
|
|
64451
|
+
});
|
|
63205
64452
|
rootRule.setFileContent(newContent);
|
|
63206
64453
|
const rootMirror = factory.class.getRootMirror?.();
|
|
63207
64454
|
if (rootMirror && !this.global) toolRules.push(...rootMirror.getMirrorFiles({
|
|
@@ -63210,6 +64457,53 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63210
64457
|
content: newContent
|
|
63211
64458
|
}));
|
|
63212
64459
|
}
|
|
64460
|
+
/**
|
|
64461
|
+
* The language delivered as a prompt block, or `undefined` when none is:
|
|
64462
|
+
* `language` is unset, or the target is Claude Code, which has a native
|
|
64463
|
+
* `language` setting (see {@link ClaudecodeLanguageSettings}) and so gets
|
|
64464
|
+
* no block in its root file.
|
|
64465
|
+
*/
|
|
64466
|
+
getPromptBlockLanguage() {
|
|
64467
|
+
return this.isClaudecodeTarget() ? void 0 : this.language;
|
|
64468
|
+
}
|
|
64469
|
+
isClaudecodeTarget() {
|
|
64470
|
+
return this.toolTarget === "claudecode" || this.toolTarget === "claudecode-legacy";
|
|
64471
|
+
}
|
|
64472
|
+
/**
|
|
64473
|
+
* The language block for targets whose adapters never mark a ToolRule as
|
|
64474
|
+
* root: Cursor emits every rule as `.cursor/rules/*.mdc`, and the fixed-name
|
|
64475
|
+
* targets (Cline, Roo, Kiro, ...) file the `root: true` source beside the
|
|
64476
|
+
* others. The file produced from the `root: true` source is still the root
|
|
64477
|
+
* rule from the user's point of view, so it is the one that gets the block.
|
|
64478
|
+
* Nested rules never do. When several sources are marked `root: true`, only
|
|
64479
|
+
* the file built from the first one (in conversion order, which follows the
|
|
64480
|
+
* source order) carries the block: one instruction per target is the
|
|
64481
|
+
* contract, and a root-marking target gets exactly one as well.
|
|
64482
|
+
*/
|
|
64483
|
+
appendLanguageBlockToRootSourceRules({ convertedRules }) {
|
|
64484
|
+
const language = this.getPromptBlockLanguage();
|
|
64485
|
+
if (language === void 0) return;
|
|
64486
|
+
const firstRootSource = convertedRules.find(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
|
|
64487
|
+
if (firstRootSource === void 0) return;
|
|
64488
|
+
const { toolRule } = firstRootSource;
|
|
64489
|
+
toolRule.setFileContent(appendLanguageBlock({
|
|
64490
|
+
content: toolRule.getFileContent(),
|
|
64491
|
+
language
|
|
64492
|
+
}));
|
|
64493
|
+
}
|
|
64494
|
+
/**
|
|
64495
|
+
* Claude Code's native delivery of `language`: a patch to the settings file
|
|
64496
|
+
* instead of a prompt block. Empty for every other target and when
|
|
64497
|
+
* `language` is unset, so an unset key never touches the settings file.
|
|
64498
|
+
*/
|
|
64499
|
+
async buildLanguageSettingsFiles() {
|
|
64500
|
+
if (this.language === void 0 || !this.isClaudecodeTarget()) return [];
|
|
64501
|
+
return [await ClaudecodeLanguageSettings.fromLanguage({
|
|
64502
|
+
outputRoot: this.outputRoot,
|
|
64503
|
+
language: this.language,
|
|
64504
|
+
global: this.global
|
|
64505
|
+
})];
|
|
64506
|
+
}
|
|
63213
64507
|
buildSkillList(skillClass) {
|
|
63214
64508
|
if (!this.skills) return [];
|
|
63215
64509
|
const toolRelativeDirPath = skillClass.getSettablePaths({ global: this.global }).relativeDirPath;
|
|
@@ -63439,7 +64733,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63439
64733
|
const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
|
|
63440
64734
|
const rulesyncRules = toolRules.map((toolRule) => {
|
|
63441
64735
|
if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
|
|
63442
|
-
return
|
|
64736
|
+
return this.withoutLanguageBlock({
|
|
64737
|
+
toolRule,
|
|
64738
|
+
rulesyncRule: toolRule.toRulesyncRule()
|
|
64739
|
+
});
|
|
63443
64740
|
});
|
|
63444
64741
|
const claimedBy = /* @__PURE__ */ new Map();
|
|
63445
64742
|
for (const [index, rulesyncRule] of rulesyncRules.entries()) {
|
|
@@ -63455,6 +64752,33 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63455
64752
|
return rulesyncRules;
|
|
63456
64753
|
}
|
|
63457
64754
|
/**
|
|
64755
|
+
* Drop the language block a previous `generate` appended, so that importing
|
|
64756
|
+
* a generated root file and generating again yields one block, not two.
|
|
64757
|
+
* Every imported rule is checked, not just root ones: Cursor and the
|
|
64758
|
+
* fixed-name targets import their root file as a non-root rule. The
|
|
64759
|
+
* `language` key itself lives in `rulesync.jsonc`, so nothing about the
|
|
64760
|
+
* detected language is carried into the rulesync rule — which is why the
|
|
64761
|
+
* strip is reported: a user who imports a file carrying the block and has
|
|
64762
|
+
* not set `language` would otherwise lose the instruction without a trace.
|
|
64763
|
+
* Once per file per run, since an import over several targets reads the
|
|
64764
|
+
* same root file for each of them.
|
|
64765
|
+
*/
|
|
64766
|
+
withoutLanguageBlock({ toolRule, rulesyncRule }) {
|
|
64767
|
+
const body = rulesyncRule.getBody();
|
|
64768
|
+
const stripped = stripLanguageBlock(body);
|
|
64769
|
+
if (stripped === body) return rulesyncRule;
|
|
64770
|
+
const source = stripControlCharacters((0, node_path.join)(toolRule.getRelativeDirPath(), toolRule.getRelativeFilePath()));
|
|
64771
|
+
warnOnceWithFallback(this.logger, `Removed the answer-language block rulesync appends from ${source} on import; it is not kept in .rulesync/rules/. Set "language" in rulesync.jsonc to keep generating it.`);
|
|
64772
|
+
return new RulesyncRule({
|
|
64773
|
+
outputRoot: rulesyncRule.getOutputRoot(),
|
|
64774
|
+
relativeDirPath: rulesyncRule.getRelativeDirPath(),
|
|
64775
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
64776
|
+
frontmatter: rulesyncRule.getFrontmatter(),
|
|
64777
|
+
body: stripped,
|
|
64778
|
+
validate: false
|
|
64779
|
+
});
|
|
64780
|
+
}
|
|
64781
|
+
/**
|
|
63458
64782
|
* Load rulesync rule files from a single source-tree's `rules/` (and
|
|
63459
64783
|
* `rules/.curated/`) subtree. `sourceTree` is the source tree itself
|
|
63460
64784
|
* (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`), NOT its parent.
|
|
@@ -63471,7 +64795,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63471
64795
|
const rulesyncOutputRoot = (0, node_path.join)(sourceTree, RULES_FEATURE_SUBDIR);
|
|
63472
64796
|
const curatedOutputRoot = (0, node_path.join)(sourceTree, CURATED_RULES_FEATURE_SUBDIR);
|
|
63473
64797
|
const [rulesDirExists, curatedDirExists] = await Promise.all([directoryExistsStrict(rulesyncOutputRoot), directoryExistsStrict(curatedOutputRoot)]);
|
|
63474
|
-
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([rulesDirExists ? findFilesByGlobs(
|
|
64798
|
+
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([rulesDirExists ? findFilesByGlobs("**/*.md", { cwd: rulesyncOutputRoot }) : [], curatedDirExists ? findFilesByGlobs("**/*.md", { cwd: curatedOutputRoot }) : []]);
|
|
63475
64799
|
const files = [.../* @__PURE__ */ new Set([...discoveredFiles, ...discoveredCuratedFiles])];
|
|
63476
64800
|
const localFiles = files.filter((file) => !(0, node_path.relative)(rulesyncOutputRoot, file).startsWith(`.curated${node_path.sep}`));
|
|
63477
64801
|
const localRelativePathsByIdentity = groupSpellingsByCaseFoldedIdentity(localFiles.map((file) => (0, node_path.relative)(rulesyncOutputRoot, file)));
|
|
@@ -63508,7 +64832,8 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63508
64832
|
const rule = await RulesyncRule.fromFile({
|
|
63509
64833
|
outputRoot: treeParent,
|
|
63510
64834
|
relativeDirPath: treeRulesDirPath,
|
|
63511
|
-
relativeFilePath: sourceRelativeFilePath
|
|
64835
|
+
relativeFilePath: sourceRelativeFilePath,
|
|
64836
|
+
deriveSubprojectPathFromGlobs: this.deriveSubprojectPathFromGlobs
|
|
63512
64837
|
});
|
|
63513
64838
|
if (sourceRelativeFilePath === relativeFilePath) return rule;
|
|
63514
64839
|
return new RulesyncRule({
|
|
@@ -63657,10 +64982,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63657
64982
|
* Resolved once, up front, so the two blocks that need it do not depend
|
|
63658
64983
|
* on each other's evaluation order.
|
|
63659
64984
|
*/
|
|
63660
|
-
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs((
|
|
64985
|
+
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs(rootRelativeGlob(settablePaths.root.relativeDirPath ?? ".", settablePaths.root.relativeFilePath), { cwd: this.outputRoot }) : [];
|
|
63661
64986
|
const rootToolRules = await (async () => {
|
|
63662
64987
|
if (!settablePaths.root) return [];
|
|
63663
|
-
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => (
|
|
64988
|
+
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => rootRelativeGlob(alt.relativeDirPath, alt.relativeFilePath), this.outputRoot);
|
|
63664
64989
|
if (forDeletion) return buildDeletionRulesFromPaths(uniqueRootFilePaths);
|
|
63665
64990
|
return await buildImportRulesFromPaths(uniqueRootFilePaths);
|
|
63666
64991
|
})();
|
|
@@ -63669,12 +64994,9 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63669
64994
|
if (this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
|
|
63670
64995
|
const fileName = factory.meta.localRootFileName;
|
|
63671
64996
|
const filePaths = await (async () => {
|
|
63672
|
-
if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
|
|
63673
|
-
outputRoot: this.outputRoot,
|
|
63674
|
-
fileName
|
|
63675
|
-
}));
|
|
64997
|
+
if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({ fileName }), { cwd: this.outputRoot });
|
|
63676
64998
|
if (!settablePaths.root) return [];
|
|
63677
|
-
return await findFilesWithFallback(await findFilesByGlobs((
|
|
64999
|
+
return await findFilesWithFallback(await findFilesByGlobs(rootRelativeGlob(settablePaths.root.relativeDirPath ?? ".", fileName), { cwd: this.outputRoot }), settablePaths.alternativeRoots, (alt) => rootRelativeGlob(alt.relativeDirPath, fileName), this.outputRoot);
|
|
63678
65000
|
})();
|
|
63679
65001
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
63680
65002
|
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
@@ -63697,15 +65019,15 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63697
65019
|
const rootMirrorDeletionRules = await (async () => {
|
|
63698
65020
|
const rootMirror = factory.class.getRootMirror?.();
|
|
63699
65021
|
if (!forDeletion || this.global || !rootMirror) return [];
|
|
63700
|
-
const { primaryGlob, mirrorGlob } = rootMirror.getMirrorDeletionGlobs(
|
|
63701
|
-
if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
|
|
63702
|
-
const mirrorPaths = await findFilesByGlobs(mirrorGlob);
|
|
65022
|
+
const { primaryGlob, mirrorGlob } = rootMirror.getMirrorDeletionGlobs();
|
|
65023
|
+
if ((await findFilesByGlobs(primaryGlob, { cwd: this.outputRoot })).length === 0) return [];
|
|
65024
|
+
const mirrorPaths = await findFilesByGlobs(mirrorGlob, { cwd: this.outputRoot });
|
|
63703
65025
|
return buildDeletionRulesFromPaths(mirrorPaths);
|
|
63704
65026
|
})();
|
|
63705
65027
|
const extraFixedToolRules = await (async () => {
|
|
63706
65028
|
const extraFiles = factory.class.getExtraFixedFiles?.({ global: this.global });
|
|
63707
65029
|
if (!extraFiles || extraFiles.length === 0) return [];
|
|
63708
|
-
const filePaths = await findFilesByGlobs(extraFiles.map((file) => (
|
|
65030
|
+
const filePaths = await findFilesByGlobs(extraFiles.map((file) => rootRelativeGlob(file.relativeDirPath, file.relativeFilePath)), { cwd: this.outputRoot });
|
|
63709
65031
|
if (filePaths.length === 0) return [];
|
|
63710
65032
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
63711
65033
|
return await Promise.all(filePaths.map((filePath) => {
|
|
@@ -63724,9 +65046,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63724
65046
|
})();
|
|
63725
65047
|
this.logger.debug(`Found ${extraFixedToolRules.length} extra fixed tool rule files`);
|
|
63726
65048
|
const nestedToolRules = await (async () => {
|
|
63727
|
-
const patterns = this.global ? void 0 : factory.class.getNestedFilePatterns?.(
|
|
65049
|
+
const patterns = this.global ? void 0 : factory.class.getNestedFilePatterns?.();
|
|
63728
65050
|
if (forDeletion || !patterns || patterns.include.length === 0) return [];
|
|
63729
65051
|
const matchedPaths = await findFilesByGlobs(patterns.include, {
|
|
65052
|
+
cwd: this.outputRoot,
|
|
63730
65053
|
type: "file",
|
|
63731
65054
|
followSymbolicLinks: false,
|
|
63732
65055
|
ignore: patterns.ignore
|
|
@@ -63757,7 +65080,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63757
65080
|
const scannedPaths = [];
|
|
63758
65081
|
const skippedPaths = [];
|
|
63759
65082
|
for (const importOnlyRoot of importOnlyRoots) {
|
|
63760
|
-
const matchedPaths = await findFilesByGlobs((
|
|
65083
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
65084
|
+
cwd: this.outputRoot,
|
|
65085
|
+
type: "file"
|
|
65086
|
+
});
|
|
63761
65087
|
if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
|
|
63762
65088
|
skippedPaths.push(...matchedPaths);
|
|
63763
65089
|
continue;
|
|
@@ -63776,7 +65102,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63776
65102
|
const nonRootToolRules = await (async () => {
|
|
63777
65103
|
if (!settablePaths.nonRoot) return [];
|
|
63778
65104
|
const nonRootOutputRoot = (0, node_path.join)(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
|
|
63779
|
-
const nonRootFilePaths = await findFilesByGlobs(
|
|
65105
|
+
const nonRootFilePaths = await findFilesByGlobs(`**/*.${factory.meta.extension}`, { cwd: nonRootOutputRoot });
|
|
63780
65106
|
if (forDeletion) return buildDeletionRulesFromPaths(nonRootFilePaths, {
|
|
63781
65107
|
outputRootOverride: nonRootOutputRoot,
|
|
63782
65108
|
relativeDirPathOverride: settablePaths.nonRoot.relativeDirPath
|
|
@@ -63919,7 +65245,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
|
|
|
63919
65245
|
* `.rulesync/` files to disk. Rulesync file instances live in memory only.
|
|
63920
65246
|
*/
|
|
63921
65247
|
async function convertFromTool(params) {
|
|
63922
|
-
|
|
65248
|
+
resetRunWarningState();
|
|
63923
65249
|
const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
|
|
63924
65250
|
if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
|
|
63925
65251
|
const ctx = params;
|
|
@@ -64008,7 +65334,8 @@ function buildRulesStrategy(ctx) {
|
|
|
64008
65334
|
toolTarget,
|
|
64009
65335
|
global,
|
|
64010
65336
|
dryRun,
|
|
64011
|
-
logger
|
|
65337
|
+
logger,
|
|
65338
|
+
language: config.getLanguage()
|
|
64012
65339
|
}),
|
|
64013
65340
|
loadSource: (p) => p.loadToolFiles(),
|
|
64014
65341
|
toRulesync: (p, files) => p.convertToolFilesToRulesyncFiles(files),
|
|
@@ -64398,6 +65725,9 @@ function createOrphanSweepPlan() {
|
|
|
64398
65725
|
const resolved = (0, node_path.resolve)(path);
|
|
64399
65726
|
return generatedPaths.has(resolved) || isInsideGeneratedTree(resolved);
|
|
64400
65727
|
},
|
|
65728
|
+
isGeneratedExactly({ path }) {
|
|
65729
|
+
return generatedPaths.has((0, node_path.resolve)(path));
|
|
65730
|
+
},
|
|
64401
65731
|
rejectClaimed({ items, getPath }) {
|
|
64402
65732
|
return items.filter((item) => !plan.isGenerated({ path: getPath(item) }));
|
|
64403
65733
|
},
|
|
@@ -64631,13 +65961,18 @@ async function processDirFeatureGeneration(params) {
|
|
|
64631
65961
|
getPath: (d) => d.getDirPath()
|
|
64632
65962
|
}), toolDirs);
|
|
64633
65963
|
const existingFlatFiles = await processor.loadToolFlatFilesToDelete();
|
|
64634
|
-
|
|
65964
|
+
const orphanFileCount = await processor.removeOrphanFlatFiles({
|
|
64635
65965
|
existingFlatFiles: sweepPlan.rejectClaimed({
|
|
64636
65966
|
items: existingFlatFiles,
|
|
64637
65967
|
getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
|
|
64638
65968
|
}),
|
|
64639
65969
|
generatedDirs: toolDirs
|
|
64640
|
-
})
|
|
65970
|
+
});
|
|
65971
|
+
const orphanInDirCount = await processor.removeOrphanFilesInAiDirs({
|
|
65972
|
+
generatedDirs: toolDirs,
|
|
65973
|
+
isClaimed: (path) => sweepPlan.isGeneratedExactly({ path })
|
|
65974
|
+
});
|
|
65975
|
+
return orphanDirCount + orphanFileCount + orphanInDirCount > 0;
|
|
64641
65976
|
} });
|
|
64642
65977
|
return {
|
|
64643
65978
|
count: totalCount,
|
|
@@ -64956,7 +66291,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
64956
66291
|
async function generate(params) {
|
|
64957
66292
|
const { config, logger } = params;
|
|
64958
66293
|
resetRootShadowingWarnings({ logger });
|
|
64959
|
-
|
|
66294
|
+
resetRunWarningState();
|
|
64960
66295
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
64961
66296
|
toolTarget,
|
|
64962
66297
|
outputRoot
|
|
@@ -65116,6 +66451,8 @@ async function generateRulesCore(params) {
|
|
|
65116
66451
|
simulateCommands: config.getSimulateCommands(),
|
|
65117
66452
|
simulateSubagents: config.getSimulateSubagents(),
|
|
65118
66453
|
simulateSkills: config.getSimulateSkills(),
|
|
66454
|
+
language: config.getLanguage(),
|
|
66455
|
+
deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
|
|
65119
66456
|
skills,
|
|
65120
66457
|
featureOptions: config.getFeatureOptions(toolTarget, "rules"),
|
|
65121
66458
|
dryRun: config.isPreviewMode(),
|
|
@@ -65566,7 +66903,7 @@ function getToolOutputRoot({ config, tool }) {
|
|
|
65566
66903
|
*/
|
|
65567
66904
|
async function importFromTool(params) {
|
|
65568
66905
|
const { config, tool, logger } = params;
|
|
65569
|
-
|
|
66906
|
+
resetRunWarningState();
|
|
65570
66907
|
await assertPluginRootSafe({
|
|
65571
66908
|
toolTarget: tool,
|
|
65572
66909
|
outputRoot: getToolOutputRoot({
|
|
@@ -66020,6 +67357,12 @@ Object.defineProperty(exports, "DEPRECATED_FEATURE_REPLACEMENTS", {
|
|
|
66020
67357
|
return DEPRECATED_FEATURE_REPLACEMENTS;
|
|
66021
67358
|
}
|
|
66022
67359
|
});
|
|
67360
|
+
Object.defineProperty(exports, "ELLIPSIS_WIDTH", {
|
|
67361
|
+
enumerable: true,
|
|
67362
|
+
get: function() {
|
|
67363
|
+
return ELLIPSIS_WIDTH;
|
|
67364
|
+
}
|
|
67365
|
+
});
|
|
66023
67366
|
Object.defineProperty(exports, "ErrorCodes", {
|
|
66024
67367
|
enumerable: true,
|
|
66025
67368
|
get: function() {
|
|
@@ -66392,6 +67735,12 @@ Object.defineProperty(exports, "__toESM", {
|
|
|
66392
67735
|
return __toESM;
|
|
66393
67736
|
}
|
|
66394
67737
|
});
|
|
67738
|
+
Object.defineProperty(exports, "applyFileMode", {
|
|
67739
|
+
enumerable: true,
|
|
67740
|
+
get: function() {
|
|
67741
|
+
return applyFileMode;
|
|
67742
|
+
}
|
|
67743
|
+
});
|
|
66395
67744
|
Object.defineProperty(exports, "assertDirectoryIfExists", {
|
|
66396
67745
|
enumerable: true,
|
|
66397
67746
|
get: function() {
|
|
@@ -66605,7 +67954,7 @@ Object.defineProperty(exports, "parseCommaSeparatedList", {
|
|
|
66605
67954
|
Object.defineProperty(exports, "parseJsonc", {
|
|
66606
67955
|
enumerable: true,
|
|
66607
67956
|
get: function() {
|
|
66608
|
-
return parseJsonc$
|
|
67957
|
+
return parseJsonc$6;
|
|
66609
67958
|
}
|
|
66610
67959
|
});
|
|
66611
67960
|
Object.defineProperty(exports, "pathEscapesRoot", {
|
|
@@ -66614,6 +67963,12 @@ Object.defineProperty(exports, "pathEscapesRoot", {
|
|
|
66614
67963
|
return pathEscapesRoot;
|
|
66615
67964
|
}
|
|
66616
67965
|
});
|
|
67966
|
+
Object.defineProperty(exports, "quoteForLog", {
|
|
67967
|
+
enumerable: true,
|
|
67968
|
+
get: function() {
|
|
67969
|
+
return quoteForLog;
|
|
67970
|
+
}
|
|
67971
|
+
});
|
|
66617
67972
|
Object.defineProperty(exports, "readFileContent", {
|
|
66618
67973
|
enumerable: true,
|
|
66619
67974
|
get: function() {
|
|
@@ -66656,10 +68011,10 @@ Object.defineProperty(exports, "removeTempDirectory", {
|
|
|
66656
68011
|
return removeTempDirectory;
|
|
66657
68012
|
}
|
|
66658
68013
|
});
|
|
66659
|
-
Object.defineProperty(exports, "
|
|
68014
|
+
Object.defineProperty(exports, "resetRunWarningState", {
|
|
66660
68015
|
enumerable: true,
|
|
66661
68016
|
get: function() {
|
|
66662
|
-
return
|
|
68017
|
+
return resetRunWarningState;
|
|
66663
68018
|
}
|
|
66664
68019
|
});
|
|
66665
68020
|
Object.defineProperty(exports, "resolveEffectiveInputRoots", {
|