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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
|
|
3
|
-
import {
|
|
3
|
+
import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
5
|
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
6
|
+
import { constants } from "node:fs";
|
|
6
7
|
import os from "node:os";
|
|
7
8
|
import { intersection, kebabCase, uniq } from "es-toolkit";
|
|
8
9
|
import { globbySync, isGitIgnoredSync } from "globby";
|
|
@@ -11,7 +12,6 @@ import { format, isDeepStrictEqual } from "node:util";
|
|
|
11
12
|
import matter from "gray-matter";
|
|
12
13
|
import { YAMLException, dump, load } from "js-yaml";
|
|
13
14
|
import { omit } from "es-toolkit/object";
|
|
14
|
-
import { constants } from "node:fs";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import * as smolToml from "smol-toml";
|
|
17
17
|
import { parse as parse$1, stringify } from "smol-toml";
|
|
@@ -38,6 +38,26 @@ function stripControlCharacters(text) {
|
|
|
38
38
|
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
+
* Strips the control characters from `text` and then JSON-quotes it, which is
|
|
42
|
+
* the form a remote-derived name — a fetched path, a directory name read off
|
|
43
|
+
* the disk, a branch name chosen by a remote repository — takes in a log line.
|
|
44
|
+
*
|
|
45
|
+
* The two steps do different jobs, and neither is enough on its own. The
|
|
46
|
+
* quoting delimits the untrusted text, so a reader can tell where the name
|
|
47
|
+
* ends and the diagnostic resumes, and it escapes the quotes and backslashes
|
|
48
|
+
* that would blur that edge. But `JSON.stringify` escapes the C0 controls and
|
|
49
|
+
* nothing else: the C1 range, the 8-bit CSI introducer among it, and the bidi
|
|
50
|
+
* overrides pass through it intact, and any one of them reaches the terminal
|
|
51
|
+
* with its power to forge a line or reorder one. The strip is what takes those
|
|
52
|
+
* out. It runs first so that the quoting sees only printable text and the line
|
|
53
|
+
* carries no escaped control characters the reader has to decode — what could
|
|
54
|
+
* do harm is gone rather than spelled out — and so that the rationale for the
|
|
55
|
+
* order lives here, once, rather than at every call site.
|
|
56
|
+
*/
|
|
57
|
+
function quoteForLog(text) {
|
|
58
|
+
return JSON.stringify(stripControlCharacters(text));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
41
61
|
* Removes every control character from `text` except the line feed, so a
|
|
42
62
|
* message written to be read over several lines still is.
|
|
43
63
|
*
|
|
@@ -903,9 +923,9 @@ function toPosixPath(p) {
|
|
|
903
923
|
return p.replace(/\\/g, "/");
|
|
904
924
|
}
|
|
905
925
|
function checkPathTraversal({ relativePath, intendedRootDir }) {
|
|
906
|
-
if (relativePath.split(/[/\\]/).includes("..")) throw new Error(`Path traversal detected: ${
|
|
926
|
+
if (relativePath.split(/[/\\]/).includes("..")) throw new Error(`Path traversal detected: ${quoteForLog(relativePath)}`);
|
|
907
927
|
const resolved = resolve(intendedRootDir, relativePath);
|
|
908
|
-
if (relative(intendedRootDir, resolved).startsWith("..") || resolve(resolved) !== resolved) throw new Error(`Path traversal detected: ${
|
|
928
|
+
if (relative(intendedRootDir, resolved).startsWith("..") || resolve(resolved) !== resolved) throw new Error(`Path traversal detected: ${quoteForLog(relativePath)}`);
|
|
909
929
|
}
|
|
910
930
|
/**
|
|
911
931
|
* Resolves a path relative to a base directory, handling both absolute and relative paths
|
|
@@ -965,24 +985,60 @@ async function writeFileContent(filepath, content) {
|
|
|
965
985
|
* Apply a POSIX mode to an existing file. Windows has no executable bit and
|
|
966
986
|
* `chmod` there only toggles the read-only flag, so the call is skipped rather
|
|
967
987
|
* than writing a mode the platform cannot honor.
|
|
988
|
+
*
|
|
989
|
+
* The mode goes through a handle opened with `O_NOFOLLOW`, so a symbolic link
|
|
990
|
+
* standing where the file should be is left alone rather than having the mode
|
|
991
|
+
* land on whatever it points at -- possibly outside the output tree -- and the
|
|
992
|
+
* file whose mode changes is the very one that was opened, with no path to
|
|
993
|
+
* swap between the check and the `chmod`. The write side refuses to read
|
|
994
|
+
* through links; the mode side refuses to write through them. A link is
|
|
995
|
+
* skipped silently, like Windows; a file that does not exist still throws.
|
|
968
996
|
*/
|
|
969
997
|
async function applyFileMode(filepath, mode) {
|
|
970
998
|
if (process.platform === "win32") return;
|
|
971
|
-
await
|
|
999
|
+
const fileHandle = await openNotFollowingLinks(filepath);
|
|
1000
|
+
if (fileHandle === void 0) return;
|
|
1001
|
+
try {
|
|
1002
|
+
await fileHandle.chmod(mode);
|
|
1003
|
+
} finally {
|
|
1004
|
+
await fileHandle.close();
|
|
1005
|
+
}
|
|
972
1006
|
}
|
|
973
1007
|
/**
|
|
974
1008
|
* Restore an executable bit that went missing (interrupted run, a copy that
|
|
975
1009
|
* dropped the mode). A file whose mode is merely stricter than `mode` — the
|
|
976
|
-
* user chose 0700 over 0755 — is left alone
|
|
1010
|
+
* user chose 0700 over 0755 — is left alone, and so is a symbolic link or a
|
|
1011
|
+
* file that cannot be opened: this repairs, it never creates.
|
|
977
1012
|
*/
|
|
978
1013
|
async function restoreMissingExecutableBit(filepath, mode) {
|
|
979
1014
|
if (process.platform === "win32") return;
|
|
1015
|
+
let fileHandle;
|
|
980
1016
|
try {
|
|
981
|
-
|
|
1017
|
+
fileHandle = await openNotFollowingLinks(filepath);
|
|
982
1018
|
} catch {
|
|
983
1019
|
return;
|
|
984
1020
|
}
|
|
985
|
-
|
|
1021
|
+
if (fileHandle === void 0) return;
|
|
1022
|
+
try {
|
|
1023
|
+
if (((await fileHandle.stat()).mode & 73) !== 0) return;
|
|
1024
|
+
await fileHandle.chmod(mode);
|
|
1025
|
+
} finally {
|
|
1026
|
+
await fileHandle.close();
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Open a file for its mode without following a symbolic link at the path.
|
|
1031
|
+
* Returns `undefined` when the path is a link (`O_NOFOLLOW` fails with
|
|
1032
|
+
* `ELOOP`, or `EMLINK` on some BSDs); any other failure is the caller's.
|
|
1033
|
+
*/
|
|
1034
|
+
async function openNotFollowingLinks(filepath) {
|
|
1035
|
+
try {
|
|
1036
|
+
return await open(filepath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const code = error.code;
|
|
1039
|
+
if (code === "ELOOP" || code === "EMLINK") return;
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
986
1042
|
}
|
|
987
1043
|
async function writeFileBuffer(filepath, buffer) {
|
|
988
1044
|
await ensureDir(dirname(filepath));
|
|
@@ -1193,6 +1249,19 @@ function posixRelativePathEscapesRoot(relativePath) {
|
|
|
1193
1249
|
return relativePath === ".." || relativePath.startsWith("../") || posix.isAbsolute(relativePath);
|
|
1194
1250
|
}
|
|
1195
1251
|
/**
|
|
1252
|
+
* Whether the file `targetPath` really denotes sits outside `rootPath`. Unlike
|
|
1253
|
+
* `pathEscapesRoot`, which reads a path as it is spelled, this resolves every link
|
|
1254
|
+
* on both sides first, so a name that sits inside the root but is a link pointing
|
|
1255
|
+
* out of it is reported as the escape it is. Both sides fall back to their literal
|
|
1256
|
+
* path when they cannot be resolved, which reports an escape rather than hiding one.
|
|
1257
|
+
*/
|
|
1258
|
+
async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
1259
|
+
return posixRelativePathEscapesRoot(await resolvedRelativePath({
|
|
1260
|
+
rootPath,
|
|
1261
|
+
targetPath
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1196
1265
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1197
1266
|
*
|
|
1198
1267
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -1202,9 +1271,17 @@ function posixRelativePathEscapesRoot(relativePath) {
|
|
|
1202
1271
|
* what lets the comparison hold for a path that is not itself resolved: a glob
|
|
1203
1272
|
* rooted at a directory that is a link of its own gives every candidate the
|
|
1204
1273
|
* same unresolved prefix, and only the segments below it decide.
|
|
1274
|
+
*
|
|
1275
|
+
* `filePath` is spelled the way the platform reported it and `identity` is
|
|
1276
|
+
* posix-separated, so the two are split on both separators: on Windows a
|
|
1277
|
+
* backslash-separated path then shares its segments with the posix identity
|
|
1278
|
+
* without being rewritten first, and on a posix platform a backslash inside a
|
|
1279
|
+
* name is split the same way on both sides, which keeps the count symmetric.
|
|
1280
|
+
*
|
|
1281
|
+
* Exported for its tests, which need a Windows-style spelling on any platform.
|
|
1205
1282
|
*/
|
|
1206
1283
|
function sharedTrailingSegments(filePath, identity) {
|
|
1207
|
-
const left = splitPathSegments(
|
|
1284
|
+
const left = splitPathSegments(filePath);
|
|
1208
1285
|
const right = splitPathSegments(identity);
|
|
1209
1286
|
let shared = 0;
|
|
1210
1287
|
while (shared < left.length && shared < right.length && left[left.length - 1 - shared] === right[right.length - 1 - shared]) shared++;
|
|
@@ -1232,7 +1309,7 @@ function chooseRepresentative(candidates, identity) {
|
|
|
1232
1309
|
});
|
|
1233
1310
|
}
|
|
1234
1311
|
async function findFilesByGlobs(globs, options = {}) {
|
|
1235
|
-
const { type = "all", followSymbolicLinks = true, ignore, dot = false } = options;
|
|
1312
|
+
const { cwd, type = "all", followSymbolicLinks = true, ignore, dot = false } = options;
|
|
1236
1313
|
const globbyOptions = type === "file" ? {
|
|
1237
1314
|
onlyFiles: true,
|
|
1238
1315
|
onlyDirectories: false
|
|
@@ -1246,6 +1323,7 @@ async function findFilesByGlobs(globs, options = {}) {
|
|
|
1246
1323
|
const normalizedGlobs = Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/");
|
|
1247
1324
|
const results = globbySync(normalizedGlobs, {
|
|
1248
1325
|
absolute: true,
|
|
1326
|
+
cwd,
|
|
1249
1327
|
followSymbolicLinks,
|
|
1250
1328
|
dot,
|
|
1251
1329
|
...ignore ? { ignore: ignore.map((pattern) => pattern.replaceAll("\\", "/")) } : {},
|
|
@@ -1318,14 +1396,16 @@ async function listEntryNames(params) {
|
|
|
1318
1396
|
*/
|
|
1319
1397
|
async function dedupeNamesByFileIdentity(params) {
|
|
1320
1398
|
const { dirPath, entries } = params;
|
|
1321
|
-
const
|
|
1399
|
+
const identifiedEntries = await mapWithConcurrency({
|
|
1322
1400
|
items: entries,
|
|
1323
1401
|
limit: ENTRY_CLASSIFY_CONCURRENCY,
|
|
1324
|
-
mapper: async (entry) =>
|
|
1402
|
+
mapper: async (entry) => ({
|
|
1403
|
+
entry,
|
|
1404
|
+
identity: await realFileIdentity(join(dirPath, entry.name))
|
|
1405
|
+
})
|
|
1325
1406
|
});
|
|
1326
1407
|
const entriesByIdentity = /* @__PURE__ */ new Map();
|
|
1327
|
-
for (const
|
|
1328
|
-
const identity = identities[index] ?? nativePathToPosix(join(dirPath, entry.name));
|
|
1408
|
+
for (const { entry, identity } of identifiedEntries) {
|
|
1329
1409
|
const group = entriesByIdentity.get(identity);
|
|
1330
1410
|
if (group === void 0) entriesByIdentity.set(identity, [entry]);
|
|
1331
1411
|
else group.push(entry);
|
|
@@ -1624,43 +1704,76 @@ var CLIError = class extends Error {
|
|
|
1624
1704
|
};
|
|
1625
1705
|
//#endregion
|
|
1626
1706
|
//#region src/utils/warned-once.ts
|
|
1707
|
+
function createRunWarningState() {
|
|
1708
|
+
return {
|
|
1709
|
+
messages: /* @__PURE__ */ new Set(),
|
|
1710
|
+
carriedFilesIncomplete: false
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1627
1713
|
/**
|
|
1628
|
-
* The
|
|
1714
|
+
* The state of a run that opened no scope of its own.
|
|
1629
1715
|
* This lives in its own module, importing nothing of rulesync's, so the vitest
|
|
1630
1716
|
* setup file can clear it between tests without pulling `logger.js` into every
|
|
1631
1717
|
* test's module graph (which would defeat the module mocks some of those tests
|
|
1632
1718
|
* install).
|
|
1633
1719
|
*/
|
|
1634
|
-
const
|
|
1720
|
+
const processWideState = createRunWarningState();
|
|
1635
1721
|
/**
|
|
1636
|
-
* The
|
|
1722
|
+
* The state an operation that opened its own scope uses instead.
|
|
1637
1723
|
*
|
|
1638
1724
|
* The MCP server does not serialize requests, so two runs can be in flight at
|
|
1639
1725
|
* once. Sharing one set between them would let the first run spend the token
|
|
1640
1726
|
* for a message and leave the second one's result silent about a diagnostic
|
|
1641
1727
|
* that applies to it just as much. A scope gives each run its own bookkeeping.
|
|
1642
1728
|
*/
|
|
1643
|
-
const
|
|
1644
|
-
function
|
|
1645
|
-
return
|
|
1729
|
+
const scopedState = new AsyncLocalStorage();
|
|
1730
|
+
function currentState() {
|
|
1731
|
+
return scopedState.getStore() ?? processWideState;
|
|
1646
1732
|
}
|
|
1647
1733
|
/** Whether `message` has not been emitted yet; records it when it has not. */
|
|
1648
1734
|
function claimWarnOnce(message) {
|
|
1649
|
-
const messages =
|
|
1735
|
+
const { messages } = currentState();
|
|
1650
1736
|
if (messages.has(message)) return false;
|
|
1651
1737
|
messages.add(message);
|
|
1652
1738
|
return true;
|
|
1653
1739
|
}
|
|
1654
|
-
/**
|
|
1655
|
-
|
|
1656
|
-
|
|
1740
|
+
/**
|
|
1741
|
+
* Record that this run could not read everything a directory carries -- a file
|
|
1742
|
+
* it could not open, a walk that hit one of its bounds, a subtree it was denied.
|
|
1743
|
+
*
|
|
1744
|
+
* Kept here, beside the once-per-run messages, because it shares their lifetime
|
|
1745
|
+
* exactly: it is set at the moment such a shortfall is warned about, and cleared
|
|
1746
|
+
* when the next run resets its warnings.
|
|
1747
|
+
*
|
|
1748
|
+
* Deliberate refusals are not shortfalls. A hidden entry, a credential-shaped
|
|
1749
|
+
* name, a link into a pseudo-filesystem: those are files Rulesync never carries,
|
|
1750
|
+
* on every run, and a run that leaves them out has read its source in full.
|
|
1751
|
+
*/
|
|
1752
|
+
function recordIncompleteCarriedFiles() {
|
|
1753
|
+
currentState().carriedFilesIncomplete = true;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Whether {@link recordIncompleteCarriedFiles} fired in this run.
|
|
1757
|
+
*
|
|
1758
|
+
* A caller that deletes what a run did not write has to ask: a run holding an
|
|
1759
|
+
* incomplete picture of its source cannot tell a stale file from one whose
|
|
1760
|
+
* source it merely failed to read.
|
|
1761
|
+
*/
|
|
1762
|
+
function hasIncompleteCarriedFiles() {
|
|
1763
|
+
return currentState().carriedFilesIncomplete;
|
|
1764
|
+
}
|
|
1765
|
+
/** Forget what was already reported, so the next run starts silent. */
|
|
1766
|
+
function resetRunWarningState() {
|
|
1767
|
+
const state = currentState();
|
|
1768
|
+
state.messages.clear();
|
|
1769
|
+
state.carriedFilesIncomplete = false;
|
|
1657
1770
|
}
|
|
1658
1771
|
/**
|
|
1659
1772
|
* Run `operation` with its own once-per-run bookkeeping, so a concurrent run
|
|
1660
1773
|
* neither spends its tokens nor clears its record.
|
|
1661
1774
|
*/
|
|
1662
1775
|
async function withWarnOnceScope(operation) {
|
|
1663
|
-
return await
|
|
1776
|
+
return await scopedState.run(createRunWarningState(), operation);
|
|
1664
1777
|
}
|
|
1665
1778
|
//#endregion
|
|
1666
1779
|
//#region src/utils/logger.ts
|
|
@@ -2034,6 +2147,148 @@ var WarningCollectingLogger = class extends ConsoleLogger {
|
|
|
2034
2147
|
}
|
|
2035
2148
|
};
|
|
2036
2149
|
//#endregion
|
|
2150
|
+
//#region src/types/language.ts
|
|
2151
|
+
/**
|
|
2152
|
+
* Response languages the root `language` key of `rulesync.jsonc` accepts.
|
|
2153
|
+
*
|
|
2154
|
+
* BCP 47-style codes: a bare ISO 639-1 code where one language name is
|
|
2155
|
+
* unambiguous, and a region-qualified code where the written form differs by
|
|
2156
|
+
* region (`zh-CN` / `zh-TW`, `pt-BR`). Widening the list is a non-breaking
|
|
2157
|
+
* change; respelling an existing code is not, so the code style is fixed here.
|
|
2158
|
+
*/
|
|
2159
|
+
const LANGUAGE_CODES = [
|
|
2160
|
+
"en",
|
|
2161
|
+
"ja",
|
|
2162
|
+
"zh-CN",
|
|
2163
|
+
"zh-TW",
|
|
2164
|
+
"ko",
|
|
2165
|
+
"fr",
|
|
2166
|
+
"de",
|
|
2167
|
+
"es",
|
|
2168
|
+
"pt-BR",
|
|
2169
|
+
"ru"
|
|
2170
|
+
];
|
|
2171
|
+
const LanguageSchema = z.enum(LANGUAGE_CODES);
|
|
2172
|
+
const LANGUAGE_DISPLAY = {
|
|
2173
|
+
en: {
|
|
2174
|
+
name: "English",
|
|
2175
|
+
claudecode: "english"
|
|
2176
|
+
},
|
|
2177
|
+
ja: {
|
|
2178
|
+
name: "Japanese",
|
|
2179
|
+
claudecode: "japanese"
|
|
2180
|
+
},
|
|
2181
|
+
"zh-CN": {
|
|
2182
|
+
name: "Simplified Chinese",
|
|
2183
|
+
claudecode: "simplified chinese"
|
|
2184
|
+
},
|
|
2185
|
+
"zh-TW": {
|
|
2186
|
+
name: "Traditional Chinese",
|
|
2187
|
+
claudecode: "traditional chinese"
|
|
2188
|
+
},
|
|
2189
|
+
ko: {
|
|
2190
|
+
name: "Korean",
|
|
2191
|
+
claudecode: "korean"
|
|
2192
|
+
},
|
|
2193
|
+
fr: {
|
|
2194
|
+
name: "French",
|
|
2195
|
+
claudecode: "french"
|
|
2196
|
+
},
|
|
2197
|
+
de: {
|
|
2198
|
+
name: "German",
|
|
2199
|
+
claudecode: "german"
|
|
2200
|
+
},
|
|
2201
|
+
es: {
|
|
2202
|
+
name: "Spanish",
|
|
2203
|
+
claudecode: "spanish"
|
|
2204
|
+
},
|
|
2205
|
+
"pt-BR": {
|
|
2206
|
+
name: "Brazilian Portuguese",
|
|
2207
|
+
claudecode: "brazilian portuguese"
|
|
2208
|
+
},
|
|
2209
|
+
ru: {
|
|
2210
|
+
name: "Russian",
|
|
2211
|
+
claudecode: "russian"
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
/** English display name of a language, as used in the appended prompt. */
|
|
2215
|
+
function getLanguageName(language) {
|
|
2216
|
+
return LANGUAGE_DISPLAY[language].name;
|
|
2217
|
+
}
|
|
2218
|
+
/** The value Claude Code's `language` settings key expects for a language. */
|
|
2219
|
+
function getClaudecodeLanguageValue(language) {
|
|
2220
|
+
return LANGUAGE_DISPLAY[language].claudecode;
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* The one sentence rulesync appends to generated root rule files when
|
|
2224
|
+
* `language` is set. Every supported language — `en` included — emits it, so
|
|
2225
|
+
* the instruction is explicit rather than implied by the absence of a block.
|
|
2226
|
+
*/
|
|
2227
|
+
function buildLanguageInstruction(language) {
|
|
2228
|
+
return `You must always answer in ${getLanguageName(language)}. On the other hand, reasoning (thinking) should be in English to improve token efficiency.`;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Separator that opens the appended block. A thematic break makes the
|
|
2232
|
+
* concatenation visible, so a reader can tell the sentence was appended by
|
|
2233
|
+
* rulesync rather than authored as part of the rules.
|
|
2234
|
+
*/
|
|
2235
|
+
const LANGUAGE_BLOCK_SEPARATOR = "---";
|
|
2236
|
+
/**
|
|
2237
|
+
* Append the language block to a rule body: a blank line, the separator, a
|
|
2238
|
+
* blank line, then the instruction. A body that is empty (or whitespace only)
|
|
2239
|
+
* gets the instruction alone, because a file that starts with `---` reads as
|
|
2240
|
+
* the opening of a frontmatter block.
|
|
2241
|
+
*/
|
|
2242
|
+
function appendLanguageBlock({ content, language }) {
|
|
2243
|
+
const instruction = buildLanguageInstruction(language);
|
|
2244
|
+
const body = content.trimEnd();
|
|
2245
|
+
if (body.length === 0) return instruction;
|
|
2246
|
+
return `${body}\n\n${LANGUAGE_BLOCK_SEPARATOR}\n\n${instruction}`;
|
|
2247
|
+
}
|
|
2248
|
+
const INSTRUCTIONS = LANGUAGE_CODES.map((code) => buildLanguageInstruction(code));
|
|
2249
|
+
const isBlank = (value) => value === " " || value === " ";
|
|
2250
|
+
/**
|
|
2251
|
+
* Remove one trailing block exactly as {@link appendLanguageBlock} writes it,
|
|
2252
|
+
* for any supported language: the separator on its own line, blank space
|
|
2253
|
+
* with at least one line break, the instruction, trailing whitespace. Only
|
|
2254
|
+
* the end of the body is inspected, so a sentence quoted mid-file is left
|
|
2255
|
+
* alone. Returns the body unchanged when no block closes it.
|
|
2256
|
+
*
|
|
2257
|
+
* Walks the body from its end with string operations rather than a regular
|
|
2258
|
+
* expression: an unanchored pattern over optional blank lines backtracks in
|
|
2259
|
+
* polynomial time, and a rule file padded with a few hundred thousand
|
|
2260
|
+
* trailing newlines would hang `rulesync import` on it.
|
|
2261
|
+
*/
|
|
2262
|
+
function stripOneLanguageBlock(body) {
|
|
2263
|
+
const trimmed = body.trimEnd();
|
|
2264
|
+
const instruction = INSTRUCTIONS.find((candidate) => trimmed.endsWith(candidate));
|
|
2265
|
+
if (instruction === void 0) return body;
|
|
2266
|
+
const beforeInstruction = trimmed.slice(0, trimmed.length - instruction.length);
|
|
2267
|
+
const beforeGap = beforeInstruction.trimEnd();
|
|
2268
|
+
if (beforeGap.length === 0) return "";
|
|
2269
|
+
if (!beforeInstruction.slice(beforeGap.length).includes("\n")) return body;
|
|
2270
|
+
if (!beforeGap.endsWith(LANGUAGE_BLOCK_SEPARATOR)) return body;
|
|
2271
|
+
let lineStart = beforeGap.length - 3;
|
|
2272
|
+
while (lineStart > 0 && isBlank(beforeGap.charAt(lineStart - 1))) lineStart -= 1;
|
|
2273
|
+
if (lineStart > 0 && beforeGap.charAt(lineStart - 1) !== "\n") return body;
|
|
2274
|
+
return body.slice(0, lineStart).trimEnd();
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Remove the trailing language block(s) from an imported rule body so that
|
|
2278
|
+
* `rulesync import` followed by `rulesync generate` does not stack a second
|
|
2279
|
+
* copy. Repeats until nothing more comes off, so a file that already carries
|
|
2280
|
+
* two stacked blocks (generated twice by an older flow, say) comes back
|
|
2281
|
+
* clean. Returns the body unchanged when no block is present.
|
|
2282
|
+
*/
|
|
2283
|
+
function stripLanguageBlock(body) {
|
|
2284
|
+
let current = body;
|
|
2285
|
+
for (;;) {
|
|
2286
|
+
const next = stripOneLanguageBlock(current);
|
|
2287
|
+
if (next === current) return current;
|
|
2288
|
+
current = next;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
//#endregion
|
|
2037
2292
|
//#region src/utils/validation.ts
|
|
2038
2293
|
/**
|
|
2039
2294
|
* Shared validation utilities for input sanitization.
|
|
@@ -2105,7 +2360,15 @@ const ConfigParamsSchema = z.object({
|
|
|
2105
2360
|
simulateCommands: optional(z.boolean()),
|
|
2106
2361
|
simulateSubagents: optional(z.boolean()),
|
|
2107
2362
|
simulateSkills: optional(z.boolean()),
|
|
2363
|
+
deriveSubprojectPathFromGlobs: optional(z.boolean()),
|
|
2108
2364
|
flattenedCommandNaming: optional(FlattenedCommandNamingSchema),
|
|
2365
|
+
/**
|
|
2366
|
+
* Response language the generated rules steer the AI toward. Config-file
|
|
2367
|
+
* only (no CLI flag): it is a property of the project, not of one run.
|
|
2368
|
+
* Absent means "say nothing about language", which is why `en` is a real
|
|
2369
|
+
* value rather than the default.
|
|
2370
|
+
*/
|
|
2371
|
+
language: optional(LanguageSchema),
|
|
2109
2372
|
gitignoreTargetsOnly: optional(z.boolean()),
|
|
2110
2373
|
gitignoreDestination: optional(GitignoreDestinationSchema),
|
|
2111
2374
|
dryRun: optional(z.boolean()),
|
|
@@ -2245,7 +2508,9 @@ var Config = class Config {
|
|
|
2245
2508
|
simulateCommands;
|
|
2246
2509
|
simulateSubagents;
|
|
2247
2510
|
simulateSkills;
|
|
2511
|
+
deriveSubprojectPathFromGlobs;
|
|
2248
2512
|
flattenedCommandNaming;
|
|
2513
|
+
language;
|
|
2249
2514
|
gitignoreTargetsOnly;
|
|
2250
2515
|
gitignoreDestination;
|
|
2251
2516
|
dryRun;
|
|
@@ -2269,7 +2534,7 @@ var Config = class Config {
|
|
|
2269
2534
|
inputRoots;
|
|
2270
2535
|
configFilePath;
|
|
2271
2536
|
sources;
|
|
2272
|
-
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
2537
|
+
constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, deriveSubprojectPathFromGlobs, flattenedCommandNaming, language, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, inputRoots, configFilePath, sources, configFileTargets }) {
|
|
2273
2538
|
assertTargetsFeaturesExclusive({
|
|
2274
2539
|
targets,
|
|
2275
2540
|
features
|
|
@@ -2301,7 +2566,9 @@ var Config = class Config {
|
|
|
2301
2566
|
this.simulateCommands = simulateCommands ?? false;
|
|
2302
2567
|
this.simulateSubagents = simulateSubagents ?? false;
|
|
2303
2568
|
this.simulateSkills = simulateSkills ?? false;
|
|
2569
|
+
this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs ?? false;
|
|
2304
2570
|
this.flattenedCommandNaming = flattenedCommandNaming ?? "basename";
|
|
2571
|
+
this.language = language;
|
|
2305
2572
|
this.gitignoreTargetsOnly = gitignoreTargetsOnly ?? true;
|
|
2306
2573
|
this.gitignoreDestination = gitignoreDestination ?? "gitignore";
|
|
2307
2574
|
this.dryRun = dryRun ?? false;
|
|
@@ -2480,12 +2747,22 @@ var Config = class Config {
|
|
|
2480
2747
|
getFlattenedCommandNaming() {
|
|
2481
2748
|
return this.flattenedCommandNaming;
|
|
2482
2749
|
}
|
|
2750
|
+
/**
|
|
2751
|
+
* The configured response language, or `undefined` when `rulesync.jsonc`
|
|
2752
|
+
* does not set one — in which case generation leaves language alone.
|
|
2753
|
+
*/
|
|
2754
|
+
getLanguage() {
|
|
2755
|
+
return this.language;
|
|
2756
|
+
}
|
|
2483
2757
|
getSimulateSubagents() {
|
|
2484
2758
|
return this.simulateSubagents;
|
|
2485
2759
|
}
|
|
2486
2760
|
getSimulateSkills() {
|
|
2487
2761
|
return this.simulateSkills;
|
|
2488
2762
|
}
|
|
2763
|
+
getDeriveSubprojectPathFromGlobs() {
|
|
2764
|
+
return this.deriveSubprojectPathFromGlobs;
|
|
2765
|
+
}
|
|
2489
2766
|
getGitignoreTargetsOnly() {
|
|
2490
2767
|
return this.gitignoreTargetsOnly;
|
|
2491
2768
|
}
|
|
@@ -2546,6 +2823,7 @@ const getDefaults = () => ({
|
|
|
2546
2823
|
simulateCommands: false,
|
|
2547
2824
|
simulateSubagents: false,
|
|
2548
2825
|
simulateSkills: false,
|
|
2826
|
+
deriveSubprojectPathFromGlobs: false,
|
|
2549
2827
|
flattenedCommandNaming: "basename",
|
|
2550
2828
|
gitignoreTargetsOnly: true,
|
|
2551
2829
|
gitignoreDestination: "gitignore",
|
|
@@ -2593,7 +2871,9 @@ const mergeConfigs = (baseConfig, localConfig) => {
|
|
|
2593
2871
|
simulateCommands: localConfig.simulateCommands ?? baseConfig.simulateCommands,
|
|
2594
2872
|
simulateSubagents: localConfig.simulateSubagents ?? baseConfig.simulateSubagents,
|
|
2595
2873
|
simulateSkills: localConfig.simulateSkills ?? baseConfig.simulateSkills,
|
|
2874
|
+
deriveSubprojectPathFromGlobs: localConfig.deriveSubprojectPathFromGlobs ?? baseConfig.deriveSubprojectPathFromGlobs,
|
|
2596
2875
|
flattenedCommandNaming: localConfig.flattenedCommandNaming ?? baseConfig.flattenedCommandNaming,
|
|
2876
|
+
language: localConfig.language ?? baseConfig.language,
|
|
2597
2877
|
gitignoreTargetsOnly: localConfig.gitignoreTargetsOnly ?? baseConfig.gitignoreTargetsOnly,
|
|
2598
2878
|
gitignoreDestination: localConfig.gitignoreDestination ?? baseConfig.gitignoreDestination,
|
|
2599
2879
|
dryRun: localConfig.dryRun ?? baseConfig.dryRun,
|
|
@@ -2715,7 +2995,7 @@ function resolveEffectiveInputRoots({ cliInputRoot, cliInputRoots, configByFile,
|
|
|
2715
2995
|
};
|
|
2716
2996
|
}
|
|
2717
2997
|
var ConfigResolver = class {
|
|
2718
|
-
static async resolve({ targets, features, verbose, delete: isDelete, outputRoots, configPath = getDefaults().configPath, global, silent, simulateCommands, simulateSubagents, simulateSkills, gitignoreTargetsOnly, dryRun, check, gitignoreDestination, inputRoot, inputRoots }, { logger } = {}) {
|
|
2998
|
+
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 } = {}) {
|
|
2719
2999
|
const cwd = resolve(process.cwd());
|
|
2720
3000
|
assertInputRootFieldsExclusive({
|
|
2721
3001
|
inputRoot,
|
|
@@ -2814,6 +3094,11 @@ var ConfigResolver = class {
|
|
|
2814
3094
|
file: configByFile.simulateSkills,
|
|
2815
3095
|
fallback: getDefaults().simulateSkills
|
|
2816
3096
|
}),
|
|
3097
|
+
deriveSubprojectPathFromGlobs: pick({
|
|
3098
|
+
cli: deriveSubprojectPathFromGlobs,
|
|
3099
|
+
file: configByFile.deriveSubprojectPathFromGlobs,
|
|
3100
|
+
fallback: getDefaults().deriveSubprojectPathFromGlobs
|
|
3101
|
+
}),
|
|
2817
3102
|
gitignoreTargetsOnly: pick({
|
|
2818
3103
|
cli: gitignoreTargetsOnly,
|
|
2819
3104
|
file: configByFile.gitignoreTargetsOnly,
|
|
@@ -2838,6 +3123,7 @@ var ConfigResolver = class {
|
|
|
2838
3123
|
configFilePath: validatedConfigPath,
|
|
2839
3124
|
sources: configByFile.sources ?? getDefaults().sources,
|
|
2840
3125
|
flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
|
|
3126
|
+
language: configByFile.language,
|
|
2841
3127
|
configFileTargets: extractConfigFileTargets(configByFile.targets)
|
|
2842
3128
|
});
|
|
2843
3129
|
}
|
|
@@ -7241,7 +7527,80 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
|
7241
7527
|
hermesagent: "hermes"
|
|
7242
7528
|
};
|
|
7243
7529
|
//#endregion
|
|
7530
|
+
//#region src/utils/glob-static-prefix.ts
|
|
7531
|
+
/**
|
|
7532
|
+
* Characters that make a path segment a pattern rather than a literal name.
|
|
7533
|
+
*
|
|
7534
|
+
* `(` `)` and `!` are included because extglob syntax (`+(a|b)`, `!(x)`) uses
|
|
7535
|
+
* them; treating a segment that carries one as static could name a directory
|
|
7536
|
+
* that no matched file actually lives under.
|
|
7537
|
+
*/
|
|
7538
|
+
const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
|
|
7539
|
+
/**
|
|
7540
|
+
* The directory every file matched by `glob` lives under, or `undefined` when
|
|
7541
|
+
* the pattern pins down no such directory.
|
|
7542
|
+
*
|
|
7543
|
+
* The result is the longest leading run of POSIX path segments that contain no
|
|
7544
|
+
* glob metacharacter, minus the final segment when the pattern ends in one that
|
|
7545
|
+
* is static: `packages/api/**\/*.ts` yields `packages/api`, while
|
|
7546
|
+
* `packages/api/README.md` names a file and yields `packages/api` as well, and
|
|
7547
|
+
* a bare `README.md` yields nothing. A leading `./` is stripped. The following
|
|
7548
|
+
* patterns are rejected outright because the directory they would name is
|
|
7549
|
+
* either not inside the output root or not a single directory at all:
|
|
7550
|
+
*
|
|
7551
|
+
* - negation patterns (`!packages/**`), which exclude rather than select;
|
|
7552
|
+
* - absolute paths (`/etc/**`, `C:/x/**`) and backslash-separated patterns;
|
|
7553
|
+
* - patterns with a `..` segment anywhere;
|
|
7554
|
+
* - patterns with a brace expansion or any other metacharacter in their first
|
|
7555
|
+
* segment (`{a,b}/**`), which have no static prefix.
|
|
7556
|
+
*
|
|
7557
|
+
* Brace expansion elsewhere ends the prefix without rejecting the pattern:
|
|
7558
|
+
* `packages/{api,web}/**` yields `packages`, the directory both alternatives
|
|
7559
|
+
* share, exactly as `packages/*\/**` would.
|
|
7560
|
+
*/
|
|
7561
|
+
function getGlobStaticPrefix(glob) {
|
|
7562
|
+
if (glob.startsWith("!") || glob.includes("\\")) return;
|
|
7563
|
+
let pattern = glob;
|
|
7564
|
+
while (pattern.startsWith("./")) pattern = pattern.slice(2);
|
|
7565
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern)) return;
|
|
7566
|
+
const segments = pattern.split("/");
|
|
7567
|
+
if (segments.includes("..")) return;
|
|
7568
|
+
const lastIndex = segments.length - 1;
|
|
7569
|
+
const prefix = [];
|
|
7570
|
+
for (const [index, segment] of segments.entries()) {
|
|
7571
|
+
if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
|
|
7572
|
+
if (segment === ".") continue;
|
|
7573
|
+
if (index === lastIndex) break;
|
|
7574
|
+
prefix.push(segment);
|
|
7575
|
+
}
|
|
7576
|
+
return prefix.length > 0 ? prefix.join("/") : void 0;
|
|
7577
|
+
}
|
|
7578
|
+
/**
|
|
7579
|
+
* The single directory every one of `globs` scopes, or `undefined` when the
|
|
7580
|
+
* list is empty, any pattern yields no prefix, or the patterns disagree.
|
|
7581
|
+
*
|
|
7582
|
+
* Several globs derive a directory only when each of them yields the same one:
|
|
7583
|
+
* picking the first pattern's prefix, or the common ancestor of all of them,
|
|
7584
|
+
* would place the rule somewhere the author never named.
|
|
7585
|
+
*/
|
|
7586
|
+
function getGlobsStaticPrefix(globs) {
|
|
7587
|
+
let shared;
|
|
7588
|
+
for (const glob of globs) {
|
|
7589
|
+
const prefix = getGlobStaticPrefix(glob);
|
|
7590
|
+
if (prefix === void 0 || shared !== void 0 && prefix !== shared) return;
|
|
7591
|
+
shared = prefix;
|
|
7592
|
+
}
|
|
7593
|
+
return shared;
|
|
7594
|
+
}
|
|
7595
|
+
//#endregion
|
|
7244
7596
|
//#region src/features/rules/rulesync-rule.ts
|
|
7597
|
+
/**
|
|
7598
|
+
* The `agentsmd.subprojectPath` value that asks for the path to be derived from
|
|
7599
|
+
* the rule's `globs` (see {@link resolveSubprojectPath}). It is a request, not a
|
|
7600
|
+
* path: the constructor replaces it with the derived directory, or drops it,
|
|
7601
|
+
* before any consumer reads the frontmatter.
|
|
7602
|
+
*/
|
|
7603
|
+
const AUTO_SUBPROJECT_PATH = "auto";
|
|
7245
7604
|
const RulesyncRuleFrontmatterSchema = z.object({
|
|
7246
7605
|
root: z.optional(z.boolean()),
|
|
7247
7606
|
localRoot: z.optional(z.boolean()),
|
|
@@ -7293,10 +7652,90 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
7293
7652
|
facet: z.optional(z.enum(["policies", "output-contracts"]))
|
|
7294
7653
|
}))
|
|
7295
7654
|
});
|
|
7655
|
+
/**
|
|
7656
|
+
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
7657
|
+
* that no target has to know how it came about:
|
|
7658
|
+
*
|
|
7659
|
+
* 1. an explicit directory in the frontmatter wins, and an explicit `""` is an
|
|
7660
|
+
* opt-out: the rule keeps its default placement and nothing is derived;
|
|
7661
|
+
* 2. otherwise, when the rule says `"auto"` or `deriveFromGlobs` is on, the
|
|
7662
|
+
* directory the rule's `globs` share (see `getGlobsStaticPrefix`);
|
|
7663
|
+
* 3. otherwise none, which keeps the rule in the target's modular directory.
|
|
7664
|
+
*
|
|
7665
|
+
* A root rule never nests, so it never derives. When a derivation yields
|
|
7666
|
+
* nothing the rule falls back to step 3, and only a rule that asked with
|
|
7667
|
+
* `"auto"` is told, once: it named a placement it did not get, so a warning
|
|
7668
|
+
* names the file to fix (an error would stop every other rule from
|
|
7669
|
+
* generating). The config option applies to every non-root rule, most of
|
|
7670
|
+
* which are general guidance whose globs, if any, were written as activation
|
|
7671
|
+
* hints (`["src/**\/*.ts", "test/**\/*.ts"]`) rather than as a directory;
|
|
7672
|
+
* warning about each of those on every generate would drown out real ones,
|
|
7673
|
+
* so config-driven derivation falls back silently.
|
|
7674
|
+
*/
|
|
7675
|
+
function resolveSubprojectPath({ frontmatter, deriveFromGlobs, rulePath }) {
|
|
7676
|
+
const authored = frontmatter.agentsmd?.subprojectPath;
|
|
7677
|
+
if (authored === "") return;
|
|
7678
|
+
if (typeof authored === "string" && authored !== "auto") return authored;
|
|
7679
|
+
const requested = authored === AUTO_SUBPROJECT_PATH;
|
|
7680
|
+
if (!requested && !deriveFromGlobs) return;
|
|
7681
|
+
if (frontmatter.root) {
|
|
7682
|
+
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.`);
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
const globs = Array.isArray(frontmatter.globs) ? frontmatter.globs : [];
|
|
7686
|
+
const derived = getGlobsStaticPrefix(globs);
|
|
7687
|
+
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.`);
|
|
7688
|
+
return derived;
|
|
7689
|
+
}
|
|
7690
|
+
/**
|
|
7691
|
+
* `frontmatter` with `agentsmd.subprojectPath` replaced by its resolved value,
|
|
7692
|
+
* or removed when there is none, so the `"auto"` request never reaches a
|
|
7693
|
+
* consumer as if it were a directory name. An `agentsmd` block that held
|
|
7694
|
+
* nothing but the request goes with it, so a consumer sees the same shape it
|
|
7695
|
+
* would for a rule that never mentioned `agentsmd`. An authored `""` is left
|
|
7696
|
+
* as written: every consumer already reads it as "no nesting".
|
|
7697
|
+
*/
|
|
7698
|
+
function withResolvedSubprojectPath({ frontmatter, deriveFromGlobs, rulePath }) {
|
|
7699
|
+
const authored = frontmatter.agentsmd?.subprojectPath;
|
|
7700
|
+
const resolved = resolveSubprojectPath({
|
|
7701
|
+
frontmatter,
|
|
7702
|
+
deriveFromGlobs,
|
|
7703
|
+
rulePath
|
|
7704
|
+
});
|
|
7705
|
+
if (resolved === authored || resolved === void 0 && authored !== "auto") return frontmatter;
|
|
7706
|
+
const { subprojectPath: _authored, ...agentsmd } = frontmatter.agentsmd ?? {};
|
|
7707
|
+
if (resolved !== void 0) return {
|
|
7708
|
+
...frontmatter,
|
|
7709
|
+
agentsmd: {
|
|
7710
|
+
...agentsmd,
|
|
7711
|
+
subprojectPath: resolved
|
|
7712
|
+
}
|
|
7713
|
+
};
|
|
7714
|
+
if (Object.keys(agentsmd).length === 0) {
|
|
7715
|
+
const { agentsmd: _empty, ...rest } = frontmatter;
|
|
7716
|
+
return rest;
|
|
7717
|
+
}
|
|
7718
|
+
return {
|
|
7719
|
+
...frontmatter,
|
|
7720
|
+
agentsmd
|
|
7721
|
+
};
|
|
7722
|
+
}
|
|
7296
7723
|
var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
7724
|
+
/**
|
|
7725
|
+
* The frontmatter consumers read. It differs from what the file says in one
|
|
7726
|
+
* place: `agentsmd.subprojectPath` holds the resolved directory (see
|
|
7727
|
+
* `resolveSubprojectPath`), while `authoredFrontmatter` and
|
|
7728
|
+
* `getFileContent()` keep the authored value, so a rule written back out
|
|
7729
|
+
* still says `"auto"`.
|
|
7730
|
+
*/
|
|
7297
7731
|
frontmatter;
|
|
7732
|
+
/**
|
|
7733
|
+
* The frontmatter as written, after schema defaults but before
|
|
7734
|
+
* `agentsmd.subprojectPath` resolution: what `getFileContent()` serializes.
|
|
7735
|
+
*/
|
|
7736
|
+
authoredFrontmatter;
|
|
7298
7737
|
body;
|
|
7299
|
-
constructor({ frontmatter, body, ...rest }) {
|
|
7738
|
+
constructor({ frontmatter, body, deriveSubprojectPathFromGlobs = false, ...rest }) {
|
|
7300
7739
|
const parseResult = RulesyncRuleFrontmatterSchema.safeParse(frontmatter);
|
|
7301
7740
|
if (!parseResult.success && rest.validate !== false) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(parseResult.error)}`);
|
|
7302
7741
|
const parsedFrontmatter = parseResult.success ? parseResult.data : {
|
|
@@ -7307,7 +7746,12 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7307
7746
|
...rest,
|
|
7308
7747
|
fileContent: stringifyFrontmatter(body, parsedFrontmatter)
|
|
7309
7748
|
});
|
|
7310
|
-
this.
|
|
7749
|
+
this.authoredFrontmatter = parsedFrontmatter;
|
|
7750
|
+
this.frontmatter = withResolvedSubprojectPath({
|
|
7751
|
+
frontmatter: parsedFrontmatter,
|
|
7752
|
+
deriveFromGlobs: deriveSubprojectPathFromGlobs,
|
|
7753
|
+
rulePath: join(rest.relativeDirPath, rest.relativeFilePath)
|
|
7754
|
+
});
|
|
7311
7755
|
this.body = body;
|
|
7312
7756
|
}
|
|
7313
7757
|
static getSettablePaths() {
|
|
@@ -7316,9 +7760,23 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7316
7760
|
legacy: { relativeDirPath: RULESYNC_RELATIVE_DIR_PATH }
|
|
7317
7761
|
};
|
|
7318
7762
|
}
|
|
7763
|
+
/**
|
|
7764
|
+
* The frontmatter to act on: `agentsmd.subprojectPath` is the resolved
|
|
7765
|
+
* placement, never `"auto"`.
|
|
7766
|
+
*/
|
|
7319
7767
|
getFrontmatter() {
|
|
7320
7768
|
return this.frontmatter;
|
|
7321
7769
|
}
|
|
7770
|
+
/**
|
|
7771
|
+
* The frontmatter as the file states it, `agentsmd.subprojectPath: "auto"`
|
|
7772
|
+
* included. This is the view to hand back to whoever edits the file (the
|
|
7773
|
+
* MCP rule tools): returning the resolved placement instead would make a
|
|
7774
|
+
* get → edit → put round trip hardcode the derived directory, or drop the
|
|
7775
|
+
* request when nothing could be derived.
|
|
7776
|
+
*/
|
|
7777
|
+
getAuthoredFrontmatter() {
|
|
7778
|
+
return this.authoredFrontmatter;
|
|
7779
|
+
}
|
|
7322
7780
|
validate() {
|
|
7323
7781
|
if (!this.frontmatter) return {
|
|
7324
7782
|
success: true,
|
|
@@ -7334,7 +7792,7 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7334
7792
|
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
7335
7793
|
};
|
|
7336
7794
|
}
|
|
7337
|
-
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
7795
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, deriveSubprojectPathFromGlobs = false }) {
|
|
7338
7796
|
const dirPath = relativeDirPath ?? this.getSettablePaths().recommended.relativeDirPath;
|
|
7339
7797
|
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
7340
7798
|
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
@@ -7353,7 +7811,8 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
7353
7811
|
relativeFilePath,
|
|
7354
7812
|
frontmatter: validatedFrontmatter,
|
|
7355
7813
|
body: content.trim(),
|
|
7356
|
-
validate
|
|
7814
|
+
validate,
|
|
7815
|
+
deriveSubprojectPathFromGlobs
|
|
7357
7816
|
});
|
|
7358
7817
|
}
|
|
7359
7818
|
getBody() {
|
|
@@ -7381,6 +7840,21 @@ function containsPathSeparator(name) {
|
|
|
7381
7840
|
return name.includes("/") || name.includes("\\");
|
|
7382
7841
|
}
|
|
7383
7842
|
/**
|
|
7843
|
+
* The permission bits worth carrying from a source file: only an executable
|
|
7844
|
+
* one has a mode the copy must keep, and only its read and execute bits plus
|
|
7845
|
+
* the owner's write bit are kept. Group and world write bits are dropped the
|
|
7846
|
+
* way a umask would drop them, so a source sitting on a mount that reports
|
|
7847
|
+
* everything as 0777 does not turn the generated copy world-writable; setuid,
|
|
7848
|
+
* setgid and sticky are never carried. Windows reports no executable bit, so
|
|
7849
|
+
* nothing is carried there and the copy takes the platform default, as it
|
|
7850
|
+
* always has.
|
|
7851
|
+
*/
|
|
7852
|
+
function carriedFileMode(mode) {
|
|
7853
|
+
const permissionBits = mode & 493;
|
|
7854
|
+
if ((permissionBits & 73) === 0) return {};
|
|
7855
|
+
return { fileMode: permissionBits };
|
|
7856
|
+
}
|
|
7857
|
+
/**
|
|
7384
7858
|
* Directories that hold credentials. Excluding these protects something, so
|
|
7385
7859
|
* their exclusion is reported rather than silent.
|
|
7386
7860
|
*/
|
|
@@ -8094,6 +8568,7 @@ var AiDir = class AiDir {
|
|
|
8094
8568
|
* files says so rather than generating a directory that is quietly short.
|
|
8095
8569
|
*/
|
|
8096
8570
|
static warnOnCarriedWalkLimits({ reportedDirPath, truncations, unreadablePaths }) {
|
|
8571
|
+
if (truncations.size > 0 || unreadablePaths.length > 0) recordIncompleteCarriedFiles();
|
|
8097
8572
|
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.`);
|
|
8098
8573
|
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.`);
|
|
8099
8574
|
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.`);
|
|
@@ -8192,12 +8667,15 @@ var AiDir = class AiDir {
|
|
|
8192
8667
|
try {
|
|
8193
8668
|
fileHandle = await open(classifiedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
8194
8669
|
} catch (error) {
|
|
8670
|
+
recordIncompleteCarriedFiles();
|
|
8195
8671
|
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
8196
8672
|
continue;
|
|
8197
8673
|
}
|
|
8198
8674
|
try {
|
|
8199
|
-
const
|
|
8675
|
+
const fileStats = await fileHandle.stat();
|
|
8676
|
+
const fileSize = fileStats.size;
|
|
8200
8677
|
if (carriedBytes + fileSize > 104857600) {
|
|
8678
|
+
recordIncompleteCarriedFiles();
|
|
8201
8679
|
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.`);
|
|
8202
8680
|
break;
|
|
8203
8681
|
}
|
|
@@ -8205,9 +8683,11 @@ var AiDir = class AiDir {
|
|
|
8205
8683
|
carriedBytes += fileBuffer.byteLength;
|
|
8206
8684
|
files.push({
|
|
8207
8685
|
relativeFilePathToDirPath: relative(dirPath, filePath),
|
|
8208
|
-
fileBuffer
|
|
8686
|
+
fileBuffer,
|
|
8687
|
+
...carriedFileMode(fileStats.mode)
|
|
8209
8688
|
});
|
|
8210
8689
|
} catch (error) {
|
|
8690
|
+
recordIncompleteCarriedFiles();
|
|
8211
8691
|
warnOnceWithFallback(void 0, `Not carrying ${stripControlCharacters(toPosixPath(filePath))}: ${stripControlCharacters(formatError(error))}.`);
|
|
8212
8692
|
} finally {
|
|
8213
8693
|
await fileHandle.close();
|
|
@@ -8240,6 +8720,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8240
8720
|
targets: z._default(RulesyncTargetsSchema, ["*"]),
|
|
8241
8721
|
"disable-model-invocation": z.optional(z.boolean()),
|
|
8242
8722
|
"user-invocable": z.optional(z.boolean()),
|
|
8723
|
+
license: z.optional(z.string()),
|
|
8724
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
8725
|
+
metadata: z.optional(z.looseObject({})),
|
|
8243
8726
|
claudecode: z.optional(z.looseObject({
|
|
8244
8727
|
when_to_use: z.optional(z.string()),
|
|
8245
8728
|
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
@@ -8289,7 +8772,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8289
8772
|
kilo: z.optional(z.looseObject({
|
|
8290
8773
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8291
8774
|
license: z.optional(z.string()),
|
|
8292
|
-
compatibility: z.optional(z.looseObject({})),
|
|
8775
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
8293
8776
|
metadata: z.optional(z.looseObject({}))
|
|
8294
8777
|
})),
|
|
8295
8778
|
kiro: z.optional(z.looseObject({
|
|
@@ -8666,6 +9149,61 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
8666
9149
|
function resolveUserInvocable({ rootFrontmatter, section }) {
|
|
8667
9150
|
return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
|
|
8668
9151
|
}
|
|
9152
|
+
/**
|
|
9153
|
+
* Resolve the effective `license` value for a tool skill.
|
|
9154
|
+
*
|
|
9155
|
+
* The rulesync skill frontmatter exposes a root-level `license` default that
|
|
9156
|
+
* applies to every tool modelling the Agent Skills standard field; the list of
|
|
9157
|
+
* those tools lives in `docs/reference/file-formats.md`. Each tool's own
|
|
9158
|
+
* section may override that default with a per-target value. A defined section
|
|
9159
|
+
* value always wins over the root default.
|
|
9160
|
+
*
|
|
9161
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9162
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9163
|
+
*
|
|
9164
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9165
|
+
*/
|
|
9166
|
+
function resolveLicense({ rootFrontmatter, section }) {
|
|
9167
|
+
return section?.license ?? rootFrontmatter.license;
|
|
9168
|
+
}
|
|
9169
|
+
/**
|
|
9170
|
+
* Resolve the effective `compatibility` value for a tool skill.
|
|
9171
|
+
*
|
|
9172
|
+
* The rulesync skill frontmatter exposes a root-level `compatibility` default
|
|
9173
|
+
* that applies to every tool modelling the Agent Skills standard field; the
|
|
9174
|
+
* list of those tools lives in `docs/reference/file-formats.md`. Each tool's
|
|
9175
|
+
* own section may override that default with a per-target value. A defined
|
|
9176
|
+
* section value always wins over the root default.
|
|
9177
|
+
*
|
|
9178
|
+
* The root value keeps the rulesync shape (a string, or the legacy object
|
|
9179
|
+
* form); any per-target normalization — such as the Agent Skills spec's string
|
|
9180
|
+
* coercion — stays with the adapter.
|
|
9181
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9182
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9183
|
+
*
|
|
9184
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9185
|
+
*/
|
|
9186
|
+
function resolveCompatibility({ rootFrontmatter, section }) {
|
|
9187
|
+
return section?.compatibility ?? rootFrontmatter.compatibility;
|
|
9188
|
+
}
|
|
9189
|
+
/**
|
|
9190
|
+
* Resolve the effective `metadata` value for a tool skill.
|
|
9191
|
+
*
|
|
9192
|
+
* The rulesync skill frontmatter exposes a root-level `metadata` default that
|
|
9193
|
+
* applies to every tool modelling the Agent Skills standard field; the list of
|
|
9194
|
+
* those tools lives in `docs/reference/file-formats.md`. Each tool's own
|
|
9195
|
+
* section may override that default with a per-target value. A defined section
|
|
9196
|
+
* value always wins over the root default; the two maps are never merged key
|
|
9197
|
+
* by key.
|
|
9198
|
+
*
|
|
9199
|
+
* The section value type is generic because `factorydroid` deliberately types
|
|
9200
|
+
* the packaging fields as `unknown` (Droid never validates them).
|
|
9201
|
+
*
|
|
9202
|
+
* @returns The resolved value, or `undefined` when neither value is set.
|
|
9203
|
+
*/
|
|
9204
|
+
function resolveMetadata({ rootFrontmatter, section }) {
|
|
9205
|
+
return section?.metadata ?? rootFrontmatter.metadata;
|
|
9206
|
+
}
|
|
8669
9207
|
//#endregion
|
|
8670
9208
|
//#region src/constants/augmentcode-paths.ts
|
|
8671
9209
|
const AUGMENTCODE_DIR = ".augment";
|
|
@@ -10610,6 +11148,40 @@ var RovodevCheck = class extends AggregatedToolCheck {
|
|
|
10610
11148
|
}
|
|
10611
11149
|
};
|
|
10612
11150
|
//#endregion
|
|
11151
|
+
//#region src/constants/claudecode-paths.ts
|
|
11152
|
+
/**
|
|
11153
|
+
* Claude Code configuration-layout conventions.
|
|
11154
|
+
*
|
|
11155
|
+
* Single source of truth for where Claude Code expects its files
|
|
11156
|
+
* (directories, file names, scope-specific paths). Every feature module
|
|
11157
|
+
* (rules, commands, skills, subagents, ignore, mcp, permissions, hooks)
|
|
11158
|
+
* and the gitignore entry registry import from here, so a change in the
|
|
11159
|
+
* Claude Code conventions is a change to this file only.
|
|
11160
|
+
*/
|
|
11161
|
+
/** Root directory for Claude Code configuration, relative to the scope root. */
|
|
11162
|
+
const CLAUDECODE_DIR = ".claude";
|
|
11163
|
+
const CLAUDECODE_RULE_FILE_NAME = "CLAUDE.md";
|
|
11164
|
+
const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
|
|
11165
|
+
/** Modular rules directory name under `.claude/` (current format). */
|
|
11166
|
+
const CLAUDECODE_RULES_DIR_NAME = "rules";
|
|
11167
|
+
/** Memories directory name under `.claude/` (legacy format). */
|
|
11168
|
+
const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
|
|
11169
|
+
const CLAUDECODE_COMMANDS_DIR_PATH = join(CLAUDECODE_DIR, "commands");
|
|
11170
|
+
const CLAUDECODE_AGENTS_DIR_PATH = join(CLAUDECODE_DIR, "agents");
|
|
11171
|
+
const CLAUDECODE_SKILLS_DIR_PATH = join(CLAUDECODE_DIR, "skills");
|
|
11172
|
+
const CLAUDECODE_SCHEDULED_TASKS_DIR_PATH = join(CLAUDECODE_DIR, "scheduled-tasks");
|
|
11173
|
+
const CLAUDECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
11174
|
+
const CLAUDECODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
11175
|
+
/**
|
|
11176
|
+
* The published JSON Schema for both settings files (SchemaStore). Written as
|
|
11177
|
+
* the `$schema` key of every settings file rulesync generates, when the file
|
|
11178
|
+
* does not already state one, so editors offer completion, hover
|
|
11179
|
+
* documentation and validation for the keys around the ones rulesync writes.
|
|
11180
|
+
*/
|
|
11181
|
+
const CLAUDECODE_SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
|
|
11182
|
+
const CLAUDECODE_MCP_FILE_NAME = ".mcp.json";
|
|
11183
|
+
const CLAUDECODE_GLOBAL_MCP_FILE_NAME = ".claude.json";
|
|
11184
|
+
//#endregion
|
|
10613
11185
|
//#region src/constants/codexcli-paths.ts
|
|
10614
11186
|
const CODEXCLI_DIR = ".codex";
|
|
10615
11187
|
const CODEXCLI_PROMPTS_DIR_PATH = join(CODEXCLI_DIR, "prompts");
|
|
@@ -10630,6 +11202,52 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
10630
11202
|
"tui"
|
|
10631
11203
|
];
|
|
10632
11204
|
//#endregion
|
|
11205
|
+
//#region src/utils/quote-value.ts
|
|
11206
|
+
/**
|
|
11207
|
+
* How much of a value read off disk a diagnostic quotes.
|
|
11208
|
+
*
|
|
11209
|
+
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
11210
|
+
* offending value so the reader can find it, but the values these warnings
|
|
11211
|
+
* quote come from files rulesync did not write — a tool's own settings, a
|
|
11212
|
+
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
11213
|
+
* no longer stop at a terminal: they travel into a `--json` document another
|
|
11214
|
+
* program parses and into an MCP result an agent reads as context. A command
|
|
11215
|
+
* line or a header is the shape most likely to carry a credential, and a long
|
|
11216
|
+
* value is the shape most likely to carry instructions aimed at the agent.
|
|
11217
|
+
*/
|
|
11218
|
+
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
11219
|
+
/**
|
|
11220
|
+
* A short, quotable rendering of a value for a diagnostic.
|
|
11221
|
+
*
|
|
11222
|
+
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
11223
|
+
* crafted one read as a second line; stripped of the control characters
|
|
11224
|
+
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
11225
|
+
* bidirectional overrides); and truncated.
|
|
11226
|
+
*/
|
|
11227
|
+
function quoteValueForWarning(value) {
|
|
11228
|
+
return truncateText({
|
|
11229
|
+
text: stripControlCharacters(serialize(value)),
|
|
11230
|
+
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
11231
|
+
suffix: "…(truncated)"
|
|
11232
|
+
});
|
|
11233
|
+
}
|
|
11234
|
+
function serialize(value) {
|
|
11235
|
+
try {
|
|
11236
|
+
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
11237
|
+
} catch {
|
|
11238
|
+
return `[unserializable ${typeof value}]`;
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11241
|
+
/**
|
|
11242
|
+
* Strip the control characters out of every string before `JSON.stringify`
|
|
11243
|
+
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
11244
|
+
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
11245
|
+
* over the output can recognize as a control character again.
|
|
11246
|
+
*/
|
|
11247
|
+
function stripStrings(_key, value) {
|
|
11248
|
+
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
11249
|
+
}
|
|
11250
|
+
//#endregion
|
|
10633
11251
|
//#region src/features/shared/shared-config-gateway.ts
|
|
10634
11252
|
/**
|
|
10635
11253
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
@@ -10765,39 +11383,35 @@ function detectJsoncFormattingOptions({ text, root }) {
|
|
|
10765
11383
|
eol
|
|
10766
11384
|
};
|
|
10767
11385
|
}
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
* it said before. For an owned key that is a silent ownership failure: a
|
|
10778
|
-
* `deny` rulesync just wrote would sit above the `allow` the tool reads.
|
|
10779
|
-
* - `__proto__`, `constructor` or `prototype`. None survives into the parsed
|
|
10780
|
-
* document — a nested one is dropped, a root-level `__proto__` replaces the
|
|
10781
|
-
* root's prototype — so an edit-based write would find no difference to
|
|
10782
|
-
* apply and leave the key in the file.
|
|
10783
|
-
*
|
|
10784
|
-
* The whole-document writer resolves duplicates last-wins and drops pollution
|
|
10785
|
-
* keys, which is what it has always done, so those files go to it.
|
|
10786
|
-
*/
|
|
10787
|
-
function statesUneditableKeys(node) {
|
|
10788
|
-
if (node.type === "array") return (node.children ?? []).some((child) => statesUneditableKeys(child));
|
|
10789
|
-
if (node.type !== "object") return false;
|
|
11386
|
+
function uneditableKeyOf(node) {
|
|
11387
|
+
if (node.type === "array") {
|
|
11388
|
+
for (const child of node.children ?? []) {
|
|
11389
|
+
const found = uneditableKeyOf(child);
|
|
11390
|
+
if (found !== void 0) return found;
|
|
11391
|
+
}
|
|
11392
|
+
return;
|
|
11393
|
+
}
|
|
11394
|
+
if (node.type !== "object") return void 0;
|
|
10790
11395
|
const seen = /* @__PURE__ */ new Set();
|
|
10791
11396
|
for (const property of node.children ?? []) {
|
|
10792
11397
|
const key = property.children?.[0]?.value;
|
|
10793
11398
|
if (typeof key === "string") {
|
|
10794
|
-
if (
|
|
11399
|
+
if (isPrototypePollutionKey(key)) return {
|
|
11400
|
+
kind: "prototype-pollution",
|
|
11401
|
+
key
|
|
11402
|
+
};
|
|
11403
|
+
if (seen.has(key)) return {
|
|
11404
|
+
kind: "duplicate",
|
|
11405
|
+
key
|
|
11406
|
+
};
|
|
10795
11407
|
seen.add(key);
|
|
10796
11408
|
}
|
|
10797
11409
|
const value = property.children?.[1];
|
|
10798
|
-
if (value !== void 0
|
|
11410
|
+
if (value !== void 0) {
|
|
11411
|
+
const found = uneditableKeyOf(value);
|
|
11412
|
+
if (found !== void 0) return found;
|
|
11413
|
+
}
|
|
10799
11414
|
}
|
|
10800
|
-
return false;
|
|
10801
11415
|
}
|
|
10802
11416
|
/**
|
|
10803
11417
|
* The offset just past the whitespace and comments starting at `from`.
|
|
@@ -10978,7 +11592,8 @@ function notesAt({ text, from }) {
|
|
|
10978
11592
|
/**
|
|
10979
11593
|
* Detach the notes written at the point where the object at `path` will take a
|
|
10980
11594
|
* new key: after its last property (and around the comma a trailing-comma file
|
|
10981
|
-
* spells there), or just inside the `{` when it has no properties yet
|
|
11595
|
+
* spells there), or just inside the `{` when it has no properties yet or when
|
|
11596
|
+
* the key is to be written first (`at: "start"`).
|
|
10982
11597
|
*
|
|
10983
11598
|
* `modify` computes its insert from exactly that point — in front of a note
|
|
10984
11599
|
* written there — so applying the edit unchanged re-emits the note *after* the
|
|
@@ -10991,11 +11606,11 @@ function notesAt({ text, from }) {
|
|
|
10991
11606
|
*
|
|
10992
11607
|
* Returns `undefined` when there is no such note, which is the common case.
|
|
10993
11608
|
*/
|
|
10994
|
-
function
|
|
11609
|
+
function detachInsertionNote({ text, path, at }) {
|
|
10995
11610
|
const root = parseTree(text, [], { allowTrailingComma: true });
|
|
10996
11611
|
const object = root === void 0 ? void 0 : findNodeAtLocation(root, [...path]);
|
|
10997
11612
|
if (object?.type !== "object") return void 0;
|
|
10998
|
-
const property = object.children?.at(-1);
|
|
11613
|
+
const property = at === "end" ? object.children?.at(-1) : void 0;
|
|
10999
11614
|
const anchorKey = property?.children?.[0]?.value;
|
|
11000
11615
|
if (property !== void 0 && typeof anchorKey !== "string") return void 0;
|
|
11001
11616
|
const spans = notesAt({
|
|
@@ -11012,7 +11627,7 @@ function detachTrailingNote({ text, path }) {
|
|
|
11012
11627
|
};
|
|
11013
11628
|
}
|
|
11014
11629
|
/**
|
|
11015
|
-
* Put a note detached by {@link
|
|
11630
|
+
* Put a note detached by {@link detachInsertionNote} back where it was: after
|
|
11016
11631
|
* the property it describes (behind the comma the insert gave that property),
|
|
11017
11632
|
* or just inside the `{` of the object it was written in when there was no
|
|
11018
11633
|
* property to describe. Returns `undefined` if that place can no longer be
|
|
@@ -11035,16 +11650,23 @@ function reattachTrailingNote({ text, path, anchorKey, note }) {
|
|
|
11035
11650
|
return text.slice(0, cursor) + note + text.slice(cursor);
|
|
11036
11651
|
}
|
|
11037
11652
|
/**
|
|
11038
|
-
* Write `value` at `[...path, key]`, keeping the
|
|
11039
|
-
*
|
|
11040
|
-
*
|
|
11041
|
-
*
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11653
|
+
* Write `value` at `[...path, key]`, keeping the note written at the point of
|
|
11654
|
+
* insertion where its author put it (see {@link detachInsertionNote}): the
|
|
11655
|
+
* trailing note of the property the new key is appended after, or — for a
|
|
11656
|
+
* `leading` key, which `modify` places before every other property — the note
|
|
11657
|
+
* written just inside the `{`. Replacing an existing key needs none of this:
|
|
11658
|
+
* `modify` rewrites the value's own span and leaves every comment where it is.
|
|
11659
|
+
*/
|
|
11660
|
+
function insertJsoncProperty({ text, path, key, value, options, leading = false }) {
|
|
11661
|
+
const modification = leading ? {
|
|
11662
|
+
...options,
|
|
11663
|
+
getInsertionIndex: () => 0
|
|
11664
|
+
} : options;
|
|
11665
|
+
const write = (source) => applyEdits(source, modify(source, [...path, key], value, modification));
|
|
11666
|
+
const detached = detachInsertionNote({
|
|
11046
11667
|
text,
|
|
11047
|
-
path
|
|
11668
|
+
path,
|
|
11669
|
+
at: leading ? "start" : "end"
|
|
11048
11670
|
});
|
|
11049
11671
|
if (detached === void 0) return write(text);
|
|
11050
11672
|
return reattachTrailingNote({
|
|
@@ -11164,7 +11786,7 @@ function countJsoncEdits({ base, next, depth }) {
|
|
|
11164
11786
|
* large enough and enough of it changing, the product of the two is what
|
|
11165
11787
|
* {@link JSONC_EDIT_BUDGET_BYTES} keeps off this path.
|
|
11166
11788
|
*/
|
|
11167
|
-
function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
11789
|
+
function applyJsoncObjectEdits({ text, base, next, path, options, leadingKeys }) {
|
|
11168
11790
|
let result = text;
|
|
11169
11791
|
for (const [key, value] of Object.entries(next)) {
|
|
11170
11792
|
if (isPrototypePollutionKey(key)) continue;
|
|
@@ -11183,7 +11805,8 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11183
11805
|
base: previous,
|
|
11184
11806
|
next: value,
|
|
11185
11807
|
path: [...path, key],
|
|
11186
|
-
options
|
|
11808
|
+
options,
|
|
11809
|
+
leadingKeys: []
|
|
11187
11810
|
});
|
|
11188
11811
|
continue;
|
|
11189
11812
|
}
|
|
@@ -11197,7 +11820,8 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11197
11820
|
path,
|
|
11198
11821
|
key,
|
|
11199
11822
|
value,
|
|
11200
|
-
options
|
|
11823
|
+
options,
|
|
11824
|
+
leading: leadingKeys.includes(key)
|
|
11201
11825
|
});
|
|
11202
11826
|
}
|
|
11203
11827
|
for (const key of Object.keys(base)) if (!Object.hasOwn(next, key)) result = removeJsoncProperty({
|
|
@@ -11227,7 +11851,7 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11227
11851
|
* here have already decided (via `invalidRootPolicy`) that such a file is
|
|
11228
11852
|
* replaced;
|
|
11229
11853
|
* - a file stating the same key twice, or using `__proto__`, `constructor` or
|
|
11230
|
-
* `prototype` as a key (see {@link
|
|
11854
|
+
* `prototype` as a key (see {@link uneditableKeyOf});
|
|
11231
11855
|
* - a file so large, with so much of it changing, that editing it key by key
|
|
11232
11856
|
* would take longer than a user would wait (see
|
|
11233
11857
|
* {@link JSONC_EDIT_BUDGET_BYTES}), or changed keys that write more new text
|
|
@@ -11235,26 +11859,54 @@ function applyJsoncObjectEdits({ text, base, next, path, options }) {
|
|
|
11235
11859
|
* {@link JSONC_EDIT_WRITTEN_BYTES});
|
|
11236
11860
|
* - a file the editor itself refuses, which it answers with an exception
|
|
11237
11861
|
* rather than a result.
|
|
11862
|
+
*
|
|
11863
|
+
* Every one of those fallbacks except the empty file is reported once per run
|
|
11864
|
+
* through `logger` (or the fallback logger when none is given): the author's
|
|
11865
|
+
* comments are what the edit path exists to keep, and a `generate` that drops
|
|
11866
|
+
* them should say so rather than let the loss show up in the next diff.
|
|
11867
|
+
* `filePath` names the file in that warning; callers that know it should pass
|
|
11868
|
+
* it, since the once-per-run token is the message itself and two nameless
|
|
11869
|
+
* files would share one.
|
|
11870
|
+
*
|
|
11871
|
+
* `leadingKeys` names the root keys that go in front of every other property
|
|
11872
|
+
* when the edit path inserts them (the whole-document writer keeps the order
|
|
11873
|
+
* `document` states, so the caller places them there itself). A key the file
|
|
11874
|
+
* already has stays where the file put it.
|
|
11238
11875
|
*/
|
|
11239
|
-
function serializeSharedConfig({ format, document, existingContent }) {
|
|
11876
|
+
function serializeSharedConfig({ format, document, existingContent, leadingKeys = [], filePath, logger }) {
|
|
11240
11877
|
const whole = stringifySharedConfig({
|
|
11241
11878
|
format,
|
|
11242
11879
|
document
|
|
11243
11880
|
});
|
|
11244
11881
|
if (format !== "jsonc" || existingContent.trim() === "") return whole;
|
|
11882
|
+
const wholeBecause = (reason) => {
|
|
11883
|
+
warnAboutWholeJsoncRewrite({
|
|
11884
|
+
filePath,
|
|
11885
|
+
reason,
|
|
11886
|
+
logger
|
|
11887
|
+
});
|
|
11888
|
+
return whole;
|
|
11889
|
+
};
|
|
11245
11890
|
try {
|
|
11246
11891
|
const errors = [];
|
|
11247
11892
|
const root = parseTree(existingContent, errors, { allowTrailingComma: true });
|
|
11248
|
-
if (root === void 0 || errors.length > 0
|
|
11893
|
+
if (root === void 0 || errors.length > 0) return wholeBecause("it could not be parsed");
|
|
11894
|
+
if (root.type !== "object") return wholeBecause("its root is not an object");
|
|
11895
|
+
const uneditable = uneditableKeyOf(root);
|
|
11896
|
+
if (uneditable !== void 0) {
|
|
11897
|
+
const key = quoteValueForWarning(uneditable.key);
|
|
11898
|
+
return wholeBecause(uneditable.kind === "duplicate" ? `it states the key ${key} twice` : `it uses the key ${key}, which rulesync drops from every document it parses`);
|
|
11899
|
+
}
|
|
11249
11900
|
const base = sanitizeSharedConfigValue(getNodeValue(root));
|
|
11250
|
-
if (!isPlainObject$1(base)) return
|
|
11901
|
+
if (!isPlainObject$1(base)) return wholeBecause("its root is not an object");
|
|
11251
11902
|
const span = Math.max(existingContent.length, whole.length);
|
|
11252
11903
|
const cost = countJsoncEdits({
|
|
11253
11904
|
base,
|
|
11254
11905
|
next: document,
|
|
11255
11906
|
depth: 0
|
|
11256
11907
|
});
|
|
11257
|
-
if (cost.written > JSONC_EDIT_WRITTEN_BYTES
|
|
11908
|
+
if (cost.written > JSONC_EDIT_WRITTEN_BYTES) return wholeBecause("its changed keys write more new text than editing in place can afford");
|
|
11909
|
+
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");
|
|
11258
11910
|
return applyJsoncObjectEdits({
|
|
11259
11911
|
text: existingContent,
|
|
11260
11912
|
base,
|
|
@@ -11263,13 +11915,25 @@ function serializeSharedConfig({ format, document, existingContent }) {
|
|
|
11263
11915
|
options: { formattingOptions: detectJsoncFormattingOptions({
|
|
11264
11916
|
text: existingContent,
|
|
11265
11917
|
root
|
|
11266
|
-
}) }
|
|
11918
|
+
}) },
|
|
11919
|
+
leadingKeys
|
|
11267
11920
|
});
|
|
11268
|
-
} catch {
|
|
11269
|
-
return
|
|
11921
|
+
} catch (error) {
|
|
11922
|
+
return wholeBecause(`the editor refused it (${stripControlCharacters(formatError(error))})`);
|
|
11270
11923
|
}
|
|
11271
11924
|
}
|
|
11272
11925
|
/**
|
|
11926
|
+
* Report, once per run and file, that a JSONC file is being written whole
|
|
11927
|
+
* instead of edited in place. Named by `filePath` when the caller knows it;
|
|
11928
|
+
* a caller serializing text it never read from disk has no name to give.
|
|
11929
|
+
*/
|
|
11930
|
+
function warnAboutWholeJsoncRewrite({ filePath, reason, logger }) {
|
|
11931
|
+
const subject = filePath === void 0 ? "A shared JSONC config file" : quoteForLog(filePath);
|
|
11932
|
+
try {
|
|
11933
|
+
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.`);
|
|
11934
|
+
} catch {}
|
|
11935
|
+
}
|
|
11936
|
+
/**
|
|
11273
11937
|
* Shallow merge: every top-level key in `patch` replaces the base key
|
|
11274
11938
|
* wholesale; all other base keys are preserved. The policy for a feature that
|
|
11275
11939
|
* owns a fixed set of top-level keys.
|
|
@@ -11307,6 +11971,7 @@ function mergeSharedConfigDeep({ base, patch }) {
|
|
|
11307
11971
|
return result;
|
|
11308
11972
|
}
|
|
11309
11973
|
const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
|
|
11974
|
+
const CLAUDE_SETTINGS_LOCAL_SHARED_FILE_KEY = ".claude/settings.local.json";
|
|
11310
11975
|
const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
|
|
11311
11976
|
const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
|
|
11312
11977
|
const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
|
|
@@ -11416,9 +12081,28 @@ const ZCODE_CONFIG_DECLARATION = {
|
|
|
11416
12081
|
ownedKeys: ["mcp"]
|
|
11417
12082
|
} }
|
|
11418
12083
|
};
|
|
12084
|
+
/**
|
|
12085
|
+
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
12086
|
+
* and both validate against the one published schema, which the gateway
|
|
12087
|
+
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
12088
|
+
* file with fewer writers — the ignore feature's `fileMode: "local"` and the
|
|
12089
|
+
* project-scope `language` setting go there — so the shape is declared once
|
|
12090
|
+
* and the feature sets separately.
|
|
12091
|
+
*
|
|
12092
|
+
* `language` is the rules feature's: the root `language` key of
|
|
12093
|
+
* `rulesync.jsonc` is written natively for Claude Code instead of as a prompt
|
|
12094
|
+
* block in CLAUDE.md. At project scope it lands in the local file (a
|
|
12095
|
+
* per-developer preference, not a team commitment); at global scope it lands
|
|
12096
|
+
* in `~/.claude/settings.json`, because Claude Code reads no
|
|
12097
|
+
* `~/.claude/settings.local.json`.
|
|
12098
|
+
*/
|
|
12099
|
+
const CLAUDE_SETTINGS_FILE_SHAPE = {
|
|
12100
|
+
format: "json",
|
|
12101
|
+
ensuredKeys: { $schema: CLAUDECODE_SETTINGS_SCHEMA_URL }
|
|
12102
|
+
};
|
|
11419
12103
|
const SHARED_CONFIG_OWNERSHIP = {
|
|
11420
12104
|
[CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
|
|
11421
|
-
|
|
12105
|
+
...CLAUDE_SETTINGS_FILE_SHAPE,
|
|
11422
12106
|
features: {
|
|
11423
12107
|
ignore: {
|
|
11424
12108
|
kind: "custom",
|
|
@@ -11431,6 +12115,23 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
11431
12115
|
permissions: {
|
|
11432
12116
|
kind: "custom",
|
|
11433
12117
|
policyFunction: "applyPermissions"
|
|
12118
|
+
},
|
|
12119
|
+
rules: {
|
|
12120
|
+
kind: "replace-owned-keys",
|
|
12121
|
+
ownedKeys: ["language"]
|
|
12122
|
+
}
|
|
12123
|
+
}
|
|
12124
|
+
},
|
|
12125
|
+
[CLAUDE_SETTINGS_LOCAL_SHARED_FILE_KEY]: {
|
|
12126
|
+
...CLAUDE_SETTINGS_FILE_SHAPE,
|
|
12127
|
+
features: {
|
|
12128
|
+
ignore: {
|
|
12129
|
+
kind: "custom",
|
|
12130
|
+
policyFunction: "applyIgnoreReadDenies"
|
|
12131
|
+
},
|
|
12132
|
+
rules: {
|
|
12133
|
+
kind: "replace-owned-keys",
|
|
12134
|
+
ownedKeys: ["language"]
|
|
11434
12135
|
}
|
|
11435
12136
|
}
|
|
11436
12137
|
},
|
|
@@ -11872,17 +12573,69 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
11872
12573
|
}
|
|
11873
12574
|
};
|
|
11874
12575
|
/**
|
|
12576
|
+
* The keys a declaration ensures, added to `document` when it lacks them and
|
|
12577
|
+
* placed in front of every key it has. A document that states all of them —
|
|
12578
|
+
* with whatever value — comes back as it is, and no key of the document
|
|
12579
|
+
* moves: the whole-document writer emits keys in this order, and the JSONC
|
|
12580
|
+
* edit path is told which keys lead (see {@link serializeSharedConfig}).
|
|
12581
|
+
*/
|
|
12582
|
+
function withEnsuredKeys({ document, ensuredKeys }) {
|
|
12583
|
+
const missing = Object.entries(ensuredKeys).filter(([key]) => !isPrototypePollutionKey(key) && !Object.hasOwn(document, key));
|
|
12584
|
+
if (missing.length === 0) return document;
|
|
12585
|
+
return {
|
|
12586
|
+
...Object.fromEntries(missing),
|
|
12587
|
+
...document
|
|
12588
|
+
};
|
|
12589
|
+
}
|
|
12590
|
+
function serializeDeclaredSharedConfig({ declaration, document, existingContent, filePath, logger }) {
|
|
12591
|
+
const ensuredKeys = declaration.ensuredKeys ?? {};
|
|
12592
|
+
return serializeSharedConfig({
|
|
12593
|
+
format: declaration.format,
|
|
12594
|
+
document: withEnsuredKeys({
|
|
12595
|
+
document,
|
|
12596
|
+
ensuredKeys
|
|
12597
|
+
}),
|
|
12598
|
+
existingContent,
|
|
12599
|
+
leadingKeys: Object.keys(ensuredKeys),
|
|
12600
|
+
filePath,
|
|
12601
|
+
logger
|
|
12602
|
+
});
|
|
12603
|
+
}
|
|
12604
|
+
/**
|
|
12605
|
+
* Serialize a merged document back over a gateway-managed shared file, under
|
|
12606
|
+
* that file's declaration: its format, and the keys the gateway ensures on it
|
|
12607
|
+
* (see {@link SharedConfigFileDeclaration.ensuredKeys}). This is the one
|
|
12608
|
+
* write path of the gateway — {@link applySharedConfigPatch} ends in it, and a
|
|
12609
|
+
* feature whose policy is `custom` serializes its own merge through it — so a
|
|
12610
|
+
* key the gateway ensures is emitted whichever feature writes the file.
|
|
12611
|
+
* Throws when the file is undeclared. A JSONC file that had to be written
|
|
12612
|
+
* whole is warned about through `logger`, named by `filePath` when given and
|
|
12613
|
+
* by `fileKey` otherwise (see {@link serializeSharedConfig}).
|
|
12614
|
+
*/
|
|
12615
|
+
function serializeSharedConfigFile({ fileKey, document, existingContent, filePath, logger }) {
|
|
12616
|
+
const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
|
|
12617
|
+
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.`);
|
|
12618
|
+
return serializeDeclaredSharedConfig({
|
|
12619
|
+
declaration,
|
|
12620
|
+
document,
|
|
12621
|
+
existingContent,
|
|
12622
|
+
filePath: filePath ?? fileKey,
|
|
12623
|
+
logger
|
|
12624
|
+
});
|
|
12625
|
+
}
|
|
12626
|
+
/**
|
|
11875
12627
|
* Execute a feature's declared write to a gateway-managed shared file: parse
|
|
11876
12628
|
* the existing content, merge the patch under the feature's declared policy,
|
|
11877
12629
|
* and serialize it back over the existing content (see
|
|
11878
|
-
* {@link
|
|
11879
|
-
*
|
|
12630
|
+
* {@link serializeSharedConfigFile}, which adds the keys the file's declaration
|
|
12631
|
+
* ensures and keeps a JSONC file's comments and formatting outside the spans
|
|
12632
|
+
* the merge actually changed). Throws when the
|
|
11880
12633
|
* file or feature is undeclared, when a
|
|
11881
12634
|
* `replace-owned-keys` patch strays outside its owned keys, or when the
|
|
11882
12635
|
* feature's policy is `custom` (those calls go to the named policy function
|
|
11883
12636
|
* instead).
|
|
11884
12637
|
*/
|
|
11885
|
-
function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
|
|
12638
|
+
function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath, logger }) {
|
|
11886
12639
|
const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
|
|
11887
12640
|
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.`);
|
|
11888
12641
|
const policy = declaration.features[feature];
|
|
@@ -11903,10 +12656,12 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
|
|
|
11903
12656
|
patch
|
|
11904
12657
|
});
|
|
11905
12658
|
for (const [key, value] of Object.entries(patch)) if (value === void 0) delete document[key];
|
|
11906
|
-
return
|
|
11907
|
-
|
|
12659
|
+
return serializeDeclaredSharedConfig({
|
|
12660
|
+
declaration,
|
|
11908
12661
|
document,
|
|
11909
|
-
existingContent
|
|
12662
|
+
existingContent,
|
|
12663
|
+
filePath: filePath ?? fileKey,
|
|
12664
|
+
logger
|
|
11910
12665
|
});
|
|
11911
12666
|
}
|
|
11912
12667
|
const merged = mergeSharedConfigDeep({
|
|
@@ -11914,10 +12669,12 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
|
|
|
11914
12669
|
patch
|
|
11915
12670
|
});
|
|
11916
12671
|
for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
|
|
11917
|
-
return
|
|
11918
|
-
|
|
12672
|
+
return serializeDeclaredSharedConfig({
|
|
12673
|
+
declaration,
|
|
11919
12674
|
document: merged,
|
|
11920
|
-
existingContent
|
|
12675
|
+
existingContent,
|
|
12676
|
+
filePath: filePath ?? fileKey,
|
|
12677
|
+
logger
|
|
11921
12678
|
});
|
|
11922
12679
|
}
|
|
11923
12680
|
const READ_TOOL_NAME = "Read";
|
|
@@ -12461,7 +13218,10 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
12461
13218
|
const factory = this.getFactory(this.toolTarget);
|
|
12462
13219
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
12463
13220
|
const baseDir = join(this.outputRoot, paths.relativeDirPath);
|
|
12464
|
-
const checkFilePaths = await findFilesByGlobs(
|
|
13221
|
+
const checkFilePaths = await findFilesByGlobs(factory.meta.filePattern, {
|
|
13222
|
+
cwd: baseDir,
|
|
13223
|
+
followSymbolicLinks: !forDeletion
|
|
13224
|
+
});
|
|
12465
13225
|
const toRelativeFilePath = (path) => relative(baseDir, path);
|
|
12466
13226
|
if (forDeletion) {
|
|
12467
13227
|
const toolChecks = checkFilePaths.map((path) => factory.class.forDeletion({
|
|
@@ -13233,7 +13993,7 @@ var AugmentcodeCommand = class AugmentcodeCommand extends ToolCommand {
|
|
|
13233
13993
|
*/
|
|
13234
13994
|
static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
|
|
13235
13995
|
const rootDir = join(outputRoot, AUGMENTCODE_AGENTS_COMMANDS_DIR_PATH);
|
|
13236
|
-
const filePaths = await findFilesByGlobs(
|
|
13996
|
+
const filePaths = await findFilesByGlobs("**/*.md", { cwd: rootDir });
|
|
13237
13997
|
const imported = (await Promise.all(filePaths.map(async (filePath) => {
|
|
13238
13998
|
const relativePath = relative(rootDir, filePath);
|
|
13239
13999
|
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
@@ -13270,33 +14030,6 @@ var AugmentcodeCommand = class AugmentcodeCommand extends ToolCommand {
|
|
|
13270
14030
|
}
|
|
13271
14031
|
};
|
|
13272
14032
|
//#endregion
|
|
13273
|
-
//#region src/constants/claudecode-paths.ts
|
|
13274
|
-
/**
|
|
13275
|
-
* Claude Code configuration-layout conventions.
|
|
13276
|
-
*
|
|
13277
|
-
* Single source of truth for where Claude Code expects its files
|
|
13278
|
-
* (directories, file names, scope-specific paths). Every feature module
|
|
13279
|
-
* (rules, commands, skills, subagents, ignore, mcp, permissions, hooks)
|
|
13280
|
-
* and the gitignore entry registry import from here, so a change in the
|
|
13281
|
-
* Claude Code conventions is a change to this file only.
|
|
13282
|
-
*/
|
|
13283
|
-
/** Root directory for Claude Code configuration, relative to the scope root. */
|
|
13284
|
-
const CLAUDECODE_DIR = ".claude";
|
|
13285
|
-
const CLAUDECODE_RULE_FILE_NAME = "CLAUDE.md";
|
|
13286
|
-
const CLAUDECODE_LOCAL_RULE_FILE_NAME = "CLAUDE.local.md";
|
|
13287
|
-
/** Modular rules directory name under `.claude/` (current format). */
|
|
13288
|
-
const CLAUDECODE_RULES_DIR_NAME = "rules";
|
|
13289
|
-
/** Memories directory name under `.claude/` (legacy format). */
|
|
13290
|
-
const CLAUDECODE_MEMORIES_DIR_NAME = "memories";
|
|
13291
|
-
const CLAUDECODE_COMMANDS_DIR_PATH = join(CLAUDECODE_DIR, "commands");
|
|
13292
|
-
const CLAUDECODE_AGENTS_DIR_PATH = join(CLAUDECODE_DIR, "agents");
|
|
13293
|
-
const CLAUDECODE_SKILLS_DIR_PATH = join(CLAUDECODE_DIR, "skills");
|
|
13294
|
-
const CLAUDECODE_SCHEDULED_TASKS_DIR_PATH = join(CLAUDECODE_DIR, "scheduled-tasks");
|
|
13295
|
-
const CLAUDECODE_SETTINGS_FILE_NAME = "settings.json";
|
|
13296
|
-
const CLAUDECODE_SETTINGS_LOCAL_FILE_NAME = "settings.local.json";
|
|
13297
|
-
const CLAUDECODE_MCP_FILE_NAME = ".mcp.json";
|
|
13298
|
-
const CLAUDECODE_GLOBAL_MCP_FILE_NAME = ".claude.json";
|
|
13299
|
-
//#endregion
|
|
13300
14033
|
//#region src/features/commands/claudecode-command.ts
|
|
13301
14034
|
const ClaudecodeCommandFrontmatterSchema = z.looseObject({
|
|
13302
14035
|
description: z.optional(z.string()),
|
|
@@ -13944,7 +14677,7 @@ function commandSlug(relativeFilePath) {
|
|
|
13944
14677
|
* it up as a regular orphan.
|
|
13945
14678
|
*/
|
|
13946
14679
|
async function rulesyncCommandSlugExists({ inputRoots, dirName }) {
|
|
13947
|
-
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs(join(root, COMMANDS_FEATURE_SUBDIR
|
|
14680
|
+
return (await Promise.all(inputRoots.map((root) => findFilesByGlobs("**/*.md", { cwd: join(root, COMMANDS_FEATURE_SUBDIR) })))).flat().some((filePath) => commandSlug(basename(filePath)) === dirName);
|
|
13948
14681
|
}
|
|
13949
14682
|
//#endregion
|
|
13950
14683
|
//#region src/features/commands/devin-command.ts
|
|
@@ -14907,6 +15640,41 @@ function toStringMetadata(metadata) {
|
|
|
14907
15640
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
14908
15641
|
}
|
|
14909
15642
|
/**
|
|
15643
|
+
* The `agentsskills` block with the three Agent Skills standard fields resolved
|
|
15644
|
+
* against the root-level rulesync defaults (a defined section value wins).
|
|
15645
|
+
*
|
|
15646
|
+
* Every writer of an Agent Skills `SKILL.md` — the native target, Hermes Agent
|
|
15647
|
+
* and the simulated `agentsmd` — reads the block through this helper so a
|
|
15648
|
+
* root-authored value goes through exactly the same normalization and
|
|
15649
|
+
* spec-violation reporting as a section value. `allowed-tools` has no root
|
|
15650
|
+
* counterpart and is carried through untouched.
|
|
15651
|
+
*
|
|
15652
|
+
* @returns The merged block. It is empty when neither the section nor any
|
|
15653
|
+
* root-level field is set, which `toSpecConformantAgentSkillFields` emits as
|
|
15654
|
+
* nothing.
|
|
15655
|
+
*/
|
|
15656
|
+
function resolveAgentsSkillsSection(rulesyncFrontmatter) {
|
|
15657
|
+
const section = rulesyncFrontmatter.agentsskills;
|
|
15658
|
+
const license = resolveLicense({
|
|
15659
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15660
|
+
section
|
|
15661
|
+
});
|
|
15662
|
+
const compatibility = resolveCompatibility({
|
|
15663
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15664
|
+
section
|
|
15665
|
+
});
|
|
15666
|
+
const metadata = resolveMetadata({
|
|
15667
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
15668
|
+
section
|
|
15669
|
+
});
|
|
15670
|
+
return {
|
|
15671
|
+
...section,
|
|
15672
|
+
...license !== void 0 && { license },
|
|
15673
|
+
...compatibility !== void 0 && { compatibility },
|
|
15674
|
+
...metadata !== void 0 && { metadata }
|
|
15675
|
+
};
|
|
15676
|
+
}
|
|
15677
|
+
/**
|
|
14910
15678
|
* Convert the rulesync `agentsskills` block into the shapes the specification
|
|
14911
15679
|
* requires. Shared with `HermesagentSkill`, which writes the same fields to its
|
|
14912
15680
|
* own skill location, so one rulesync input can never produce two different
|
|
@@ -15054,7 +15822,7 @@ var AgentsSkillsSkill = class AgentsSkillsSkill extends ToolSkill {
|
|
|
15054
15822
|
const agentsSkillsFrontmatter = {
|
|
15055
15823
|
name: rulesyncFrontmatter.name,
|
|
15056
15824
|
description: rulesyncFrontmatter.description,
|
|
15057
|
-
...toSpecConformantAgentSkillFields(rulesyncFrontmatter
|
|
15825
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter))
|
|
15058
15826
|
};
|
|
15059
15827
|
AgentsSkillsSkill.reportSpecViolations({
|
|
15060
15828
|
outputRoot,
|
|
@@ -15162,7 +15930,7 @@ var HermesagentSkill = class HermesagentSkill extends AgentsSkillsSkill {
|
|
|
15162
15930
|
}
|
|
15163
15931
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
15164
15932
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
15165
|
-
const shared = toSpecConformantAgentSkillFields(rulesyncFrontmatter
|
|
15933
|
+
const shared = toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter), { coerceMetadata: false });
|
|
15166
15934
|
const hermes = rulesyncFrontmatter.hermesagent ?? {};
|
|
15167
15935
|
const dirName = rulesyncSkill.getDirName();
|
|
15168
15936
|
const frontmatter = {
|
|
@@ -17808,7 +18576,7 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
17808
18576
|
const treeName = basename(sourceTree);
|
|
17809
18577
|
const treeCommandsDirPath = join(treeName, COMMANDS_FEATURE_SUBDIR);
|
|
17810
18578
|
const basePath = join(sourceTree, COMMANDS_FEATURE_SUBDIR);
|
|
17811
|
-
const rulesyncCommandPaths = await directoryExistsStrict(basePath) ? await findFilesByGlobs(
|
|
18579
|
+
const rulesyncCommandPaths = await directoryExistsStrict(basePath) ? await findFilesByGlobs("**/*.md", { cwd: basePath }) : [];
|
|
17812
18580
|
return await Promise.all(rulesyncCommandPaths.map((path) => RulesyncCommand.fromFile({
|
|
17813
18581
|
outputRoot: treeParent,
|
|
17814
18582
|
relativeDirPath: treeCommandsDirPath,
|
|
@@ -17841,7 +18609,10 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
17841
18609
|
if (factory.meta.skipToolFileScan) return [];
|
|
17842
18610
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
17843
18611
|
const outputRootFull = join(this.outputRoot, paths.relativeDirPath);
|
|
17844
|
-
const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ?
|
|
18612
|
+
const commandFilePaths = await findFilesByGlobs(factory.meta.supportsSubdirectory ? `**/*.${factory.meta.extension}` : `*.${factory.meta.extension}`, {
|
|
18613
|
+
cwd: outputRootFull,
|
|
18614
|
+
followSymbolicLinks: !forDeletion
|
|
18615
|
+
});
|
|
17845
18616
|
if (forDeletion) {
|
|
17846
18617
|
const toolCommands = commandFilePaths.map((path) => factory.class.forDeletion({
|
|
17847
18618
|
outputRoot: this.outputRoot,
|
|
@@ -18190,52 +18961,6 @@ function compact(obj) {
|
|
|
18190
18961
|
return result;
|
|
18191
18962
|
}
|
|
18192
18963
|
//#endregion
|
|
18193
|
-
//#region src/utils/quote-value.ts
|
|
18194
|
-
/**
|
|
18195
|
-
* How much of a value read off disk a diagnostic quotes.
|
|
18196
|
-
*
|
|
18197
|
-
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
18198
|
-
* offending value so the reader can find it, but the values these warnings
|
|
18199
|
-
* quote come from files rulesync did not write — a tool's own settings, a
|
|
18200
|
-
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
18201
|
-
* no longer stop at a terminal: they travel into a `--json` document another
|
|
18202
|
-
* program parses and into an MCP result an agent reads as context. A command
|
|
18203
|
-
* line or a header is the shape most likely to carry a credential, and a long
|
|
18204
|
-
* value is the shape most likely to carry instructions aimed at the agent.
|
|
18205
|
-
*/
|
|
18206
|
-
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
18207
|
-
/**
|
|
18208
|
-
* A short, quotable rendering of a value for a diagnostic.
|
|
18209
|
-
*
|
|
18210
|
-
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
18211
|
-
* crafted one read as a second line; stripped of the control characters
|
|
18212
|
-
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
18213
|
-
* bidirectional overrides); and truncated.
|
|
18214
|
-
*/
|
|
18215
|
-
function quoteValueForWarning(value) {
|
|
18216
|
-
return truncateText({
|
|
18217
|
-
text: stripControlCharacters(serialize(value)),
|
|
18218
|
-
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
18219
|
-
suffix: "…(truncated)"
|
|
18220
|
-
});
|
|
18221
|
-
}
|
|
18222
|
-
function serialize(value) {
|
|
18223
|
-
try {
|
|
18224
|
-
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
18225
|
-
} catch {
|
|
18226
|
-
return `[unserializable ${typeof value}]`;
|
|
18227
|
-
}
|
|
18228
|
-
}
|
|
18229
|
-
/**
|
|
18230
|
-
* Strip the control characters out of every string before `JSON.stringify`
|
|
18231
|
-
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
18232
|
-
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
18233
|
-
* over the output can recognize as a control character again.
|
|
18234
|
-
*/
|
|
18235
|
-
function stripStrings(_key, value) {
|
|
18236
|
-
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
18237
|
-
}
|
|
18238
|
-
//#endregion
|
|
18239
18964
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
18240
18965
|
function isToolMatcherEntry(x) {
|
|
18241
18966
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -24822,6 +25547,17 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
24822
25547
|
};
|
|
24823
25548
|
}
|
|
24824
25549
|
/**
|
|
25550
|
+
* The `fileMode: "local"` twin, `.claude/settings.local.json`: a settable
|
|
25551
|
+
* path only under that option, so the default `getSettablePaths` does not
|
|
25552
|
+
* report it, and the gateway would otherwise not know the file has a writer.
|
|
25553
|
+
*/
|
|
25554
|
+
static getExtraSharedWritePaths() {
|
|
25555
|
+
return [{
|
|
25556
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
25557
|
+
relativeFilePath: CLAUDECODE_SETTINGS_LOCAL_FILE_NAME
|
|
25558
|
+
}];
|
|
25559
|
+
}
|
|
25560
|
+
/**
|
|
24825
25561
|
* ClaudecodeIgnore uses settings.json (or settings.local.json), which can
|
|
24826
25562
|
* include non-ignore settings. It should not be deleted by rulesync.
|
|
24827
25563
|
*
|
|
@@ -24864,7 +25600,11 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
24864
25600
|
outputRoot,
|
|
24865
25601
|
relativeDirPath: paths.relativeDirPath,
|
|
24866
25602
|
relativeFilePath: paths.relativeFilePath,
|
|
24867
|
-
fileContent:
|
|
25603
|
+
fileContent: serializeSharedConfigFile({
|
|
25604
|
+
fileKey: sharedConfigFileKey(paths),
|
|
25605
|
+
document: jsonValue,
|
|
25606
|
+
existingContent: existingFileContent
|
|
25607
|
+
}),
|
|
24868
25608
|
validate: true
|
|
24869
25609
|
});
|
|
24870
25610
|
}
|
|
@@ -26240,18 +26980,48 @@ var AiassistantMcp = class AiassistantMcp extends ToolMcp {
|
|
|
26240
26980
|
}
|
|
26241
26981
|
};
|
|
26242
26982
|
//#endregion
|
|
26243
|
-
//#region src/features/
|
|
26244
|
-
|
|
26245
|
-
|
|
26246
|
-
|
|
26247
|
-
|
|
26248
|
-
|
|
26249
|
-
|
|
26250
|
-
|
|
26983
|
+
//#region src/features/shared/amp-settings.ts
|
|
26984
|
+
/**
|
|
26985
|
+
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
26986
|
+
*
|
|
26987
|
+
* Shared by the MCP and permissions adapters because both read the very same
|
|
26988
|
+
* file. Two separately maintained copies drifted once already: the permissions
|
|
26989
|
+
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
26990
|
+
* which left it importing MCP servers reachable only through an injected
|
|
26991
|
+
* `__proto__`.
|
|
26992
|
+
*
|
|
26993
|
+
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
26994
|
+
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
26995
|
+
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
26996
|
+
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
26997
|
+
* never survive as own keys, at any depth.
|
|
26998
|
+
*
|
|
26999
|
+
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
27000
|
+
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
27001
|
+
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
27002
|
+
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
27003
|
+
* explaining, whereas here the surrounding settings are the user's own file
|
|
27004
|
+
* and are left alone.
|
|
27005
|
+
*
|
|
27006
|
+
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
27007
|
+
* plain-object check, so such a document parses successfully with the key
|
|
27008
|
+
* stripped rather than failing that check. That is deliberate — dropping the
|
|
27009
|
+
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
27010
|
+
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
27011
|
+
*/
|
|
27012
|
+
function parseAmpSettings({ fileContent }) {
|
|
27013
|
+
let parsed;
|
|
27014
|
+
try {
|
|
27015
|
+
parsed = parseJsonc(fileContent || "{}");
|
|
27016
|
+
} catch (error) {
|
|
27017
|
+
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
26251
27018
|
}
|
|
26252
27019
|
if (!isPlainObject$1(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
26253
27020
|
return parsed;
|
|
26254
27021
|
}
|
|
27022
|
+
//#endregion
|
|
27023
|
+
//#region src/features/mcp/amp-mcp.ts
|
|
27024
|
+
const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
|
|
26255
27025
|
function filterMcpServers(mcpServers) {
|
|
26256
27026
|
const filtered = {};
|
|
26257
27027
|
if (!isRecord$1(mcpServers)) return filtered;
|
|
@@ -26270,7 +27040,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26270
27040
|
json;
|
|
26271
27041
|
constructor(params) {
|
|
26272
27042
|
super(params);
|
|
26273
|
-
this.json =
|
|
27043
|
+
this.json = parseAmpSettings({ fileContent: this.fileContent });
|
|
26274
27044
|
}
|
|
26275
27045
|
getJson() {
|
|
26276
27046
|
return structuredClone(this.json);
|
|
@@ -26314,7 +27084,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26314
27084
|
const basePaths = this.getSettablePaths({ global });
|
|
26315
27085
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
26316
27086
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
26317
|
-
const json = fileContent ?
|
|
27087
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
26318
27088
|
const mcpServers = json[AMP_MCP_SERVERS_KEY];
|
|
26319
27089
|
const newJson = {
|
|
26320
27090
|
...json,
|
|
@@ -26329,7 +27099,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26329
27099
|
global
|
|
26330
27100
|
});
|
|
26331
27101
|
}
|
|
26332
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
27102
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
26333
27103
|
const basePaths = this.getSettablePaths({ global });
|
|
26334
27104
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
26335
27105
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
@@ -26342,7 +27112,8 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26342
27112
|
feature: "mcp",
|
|
26343
27113
|
existingContent: fileContent ?? "",
|
|
26344
27114
|
patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
|
|
26345
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
27115
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
27116
|
+
logger
|
|
26346
27117
|
}),
|
|
26347
27118
|
validate,
|
|
26348
27119
|
global
|
|
@@ -26355,17 +27126,13 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26355
27126
|
validate() {
|
|
26356
27127
|
let json;
|
|
26357
27128
|
try {
|
|
26358
|
-
json =
|
|
27129
|
+
json = parseAmpSettings({ fileContent: this.fileContent });
|
|
26359
27130
|
} catch (error) {
|
|
26360
27131
|
return {
|
|
26361
27132
|
success: false,
|
|
26362
27133
|
error: error instanceof Error ? error : new Error(String(error))
|
|
26363
27134
|
};
|
|
26364
27135
|
}
|
|
26365
|
-
for (const key of Object.keys(json)) if (isPrototypePollutionKey(key)) return {
|
|
26366
|
-
success: false,
|
|
26367
|
-
error: /* @__PURE__ */ new Error(`Prototype pollution key "${key}" is not allowed`)
|
|
26368
|
-
};
|
|
26369
27136
|
const mcpServers = json[AMP_MCP_SERVERS_KEY];
|
|
26370
27137
|
if (mcpServers === void 0) return {
|
|
26371
27138
|
success: true,
|
|
@@ -26375,20 +27142,10 @@ var AmpMcp = class AmpMcp extends ToolMcp {
|
|
|
26375
27142
|
success: false,
|
|
26376
27143
|
error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
|
|
26377
27144
|
};
|
|
26378
|
-
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
26379
|
-
|
|
26380
|
-
|
|
26381
|
-
|
|
26382
|
-
};
|
|
26383
|
-
if (!isRecord$1(serverConfig)) return {
|
|
26384
|
-
success: false,
|
|
26385
|
-
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
26386
|
-
};
|
|
26387
|
-
for (const key of Object.keys(serverConfig)) if (isPrototypePollutionKey(key)) return {
|
|
26388
|
-
success: false,
|
|
26389
|
-
error: /* @__PURE__ */ new Error(`Config key "${key}" in server "${serverName}" is a prototype pollution key and is not allowed`)
|
|
26390
|
-
};
|
|
26391
|
-
}
|
|
27145
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) if (!isRecord$1(serverConfig)) return {
|
|
27146
|
+
success: false,
|
|
27147
|
+
error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
|
|
27148
|
+
};
|
|
26392
27149
|
return {
|
|
26393
27150
|
success: true,
|
|
26394
27151
|
error: null
|
|
@@ -27340,7 +28097,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
27340
28097
|
validate
|
|
27341
28098
|
});
|
|
27342
28099
|
}
|
|
27343
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
|
|
28100
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
27344
28101
|
const paths = this.getSettablePaths();
|
|
27345
28102
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
27346
28103
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
@@ -27353,7 +28110,8 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
27353
28110
|
feature: "mcp",
|
|
27354
28111
|
existingContent,
|
|
27355
28112
|
patch: { servers: rulesyncMcp.getMcpServers() },
|
|
27356
|
-
filePath
|
|
28113
|
+
filePath,
|
|
28114
|
+
logger
|
|
27357
28115
|
}),
|
|
27358
28116
|
validate
|
|
27359
28117
|
});
|
|
@@ -29484,7 +30242,8 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29484
30242
|
mcp: convertedMcp,
|
|
29485
30243
|
tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
|
|
29486
30244
|
},
|
|
29487
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
30245
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
30246
|
+
logger
|
|
29488
30247
|
}),
|
|
29489
30248
|
validate
|
|
29490
30249
|
});
|
|
@@ -29502,7 +30261,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29502
30261
|
*
|
|
29503
30262
|
* @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
|
|
29504
30263
|
*/
|
|
29505
|
-
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
|
|
30264
|
+
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false, logger }) {
|
|
29506
30265
|
const basePaths = this.getSettablePaths({ global });
|
|
29507
30266
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
29508
30267
|
let fileContent = null;
|
|
@@ -29529,7 +30288,8 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
29529
30288
|
feature: "rules",
|
|
29530
30289
|
existingContent: fileContent ?? "",
|
|
29531
30290
|
patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
|
|
29532
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
30291
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
30292
|
+
logger
|
|
29533
30293
|
}),
|
|
29534
30294
|
validate
|
|
29535
30295
|
});
|
|
@@ -30549,7 +31309,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30549
31309
|
mcp: convertedMcp,
|
|
30550
31310
|
tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
|
|
30551
31311
|
},
|
|
30552
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
31312
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
31313
|
+
logger
|
|
30553
31314
|
}),
|
|
30554
31315
|
validate
|
|
30555
31316
|
});
|
|
@@ -30570,7 +31331,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30570
31331
|
* @see https://opencode.ai/docs/rules/
|
|
30571
31332
|
* @see https://opencode.ai/docs/config/
|
|
30572
31333
|
*/
|
|
30573
|
-
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
|
|
31334
|
+
static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false, logger }) {
|
|
30574
31335
|
const basePaths = this.getSettablePaths({ global });
|
|
30575
31336
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
30576
31337
|
const configDirPrefix = `${toPosixPath(basePaths.relativeDirPath).replace(/\/+$/, "")}/`;
|
|
@@ -30602,7 +31363,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
30602
31363
|
feature: "rules",
|
|
30603
31364
|
existingContent: fileContent ?? "",
|
|
30604
31365
|
patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
|
|
30605
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
31366
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
31367
|
+
logger
|
|
30606
31368
|
}),
|
|
30607
31369
|
validate
|
|
30608
31370
|
});
|
|
@@ -33090,16 +33852,6 @@ function isCanonicalAmpEntry(entry) {
|
|
|
33090
33852
|
const keys = Object.keys(matches);
|
|
33091
33853
|
return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
|
|
33092
33854
|
}
|
|
33093
|
-
function parseAmpSettings(fileContent) {
|
|
33094
|
-
const errors = [];
|
|
33095
|
-
const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
|
|
33096
|
-
if (errors.length > 0) {
|
|
33097
|
-
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
33098
|
-
throw new Error(`Failed to parse Amp settings: ${details}`);
|
|
33099
|
-
}
|
|
33100
|
-
if (!isPlainObject$1(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
33101
|
-
return parsed;
|
|
33102
|
-
}
|
|
33103
33855
|
function toDisableList(value) {
|
|
33104
33856
|
if (!Array.isArray(value)) return [];
|
|
33105
33857
|
return value.filter((entry) => typeof entry === "string");
|
|
@@ -33215,7 +33967,7 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33215
33967
|
const basePaths = AmpPermissions.getSettablePaths({ global });
|
|
33216
33968
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
33217
33969
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
33218
|
-
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
33970
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
33219
33971
|
const newJson = {
|
|
33220
33972
|
...json,
|
|
33221
33973
|
[AMP_TOOLS_DISABLE_KEY]: toDisableList(json[AMP_TOOLS_DISABLE_KEY])
|
|
@@ -33228,11 +33980,11 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33228
33980
|
validate
|
|
33229
33981
|
});
|
|
33230
33982
|
}
|
|
33231
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
33983
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
|
|
33232
33984
|
const basePaths = AmpPermissions.getSettablePaths({ global });
|
|
33233
33985
|
const jsonDir = join(outputRoot, basePaths.relativeDirPath);
|
|
33234
33986
|
const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
|
|
33235
|
-
const json = fileContent ? parseAmpSettings(fileContent) : {};
|
|
33987
|
+
const json = fileContent ? parseAmpSettings({ fileContent }) : {};
|
|
33236
33988
|
const config = rulesyncPermissions.getJson();
|
|
33237
33989
|
const { disable, permissions } = convertRulesyncToAmp(config);
|
|
33238
33990
|
const override = config.amp;
|
|
@@ -33259,13 +34011,14 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33259
34011
|
feature: "permissions",
|
|
33260
34012
|
existingContent: fileContent ?? "",
|
|
33261
34013
|
patch,
|
|
33262
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
34014
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
34015
|
+
logger
|
|
33263
34016
|
}),
|
|
33264
34017
|
validate: true
|
|
33265
34018
|
});
|
|
33266
34019
|
}
|
|
33267
34020
|
toRulesyncPermissions() {
|
|
33268
|
-
const json = parseAmpSettings(this.getFileContent());
|
|
34021
|
+
const json = parseAmpSettings({ fileContent: this.getFileContent() });
|
|
33269
34022
|
const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
|
|
33270
34023
|
const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
|
|
33271
34024
|
const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
|
|
@@ -33285,7 +34038,7 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
|
|
|
33285
34038
|
}
|
|
33286
34039
|
validate() {
|
|
33287
34040
|
try {
|
|
33288
|
-
const json = parseAmpSettings(this.fileContent);
|
|
34041
|
+
const json = parseAmpSettings({ fileContent: this.fileContent });
|
|
33289
34042
|
const disable = json[AMP_TOOLS_DISABLE_KEY];
|
|
33290
34043
|
if (disable !== void 0 && !Array.isArray(disable)) return {
|
|
33291
34044
|
success: false,
|
|
@@ -35335,15 +36088,15 @@ const CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS = Object.entries(SHARED_CONFIG_OWNE
|
|
|
35335
36088
|
* passthrough must not carry. `permissions` and `sandbox` have their own merge
|
|
35336
36089
|
* branches (the managed `allow`/`ask`/`deny` arrays and the scope filtering
|
|
35337
36090
|
* respectively), `permission` is rulesync's own canonical tool-scoped block
|
|
35338
|
-
* rather than a settings key,
|
|
35339
|
-
*
|
|
35340
|
-
* shared file.
|
|
36091
|
+
* rather than a settings key, the keys the gateway ensures on the file
|
|
36092
|
+
* (`$schema`) are editor pointers rather than Claude Code settings, and the
|
|
36093
|
+
* rest belong to the other features writing this shared file.
|
|
35341
36094
|
*/
|
|
35342
36095
|
const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
35343
36096
|
"permission",
|
|
35344
36097
|
"permissions",
|
|
35345
36098
|
"sandbox",
|
|
35346
|
-
"
|
|
36099
|
+
...Object.keys(SHARED_CONFIG_OWNERSHIP[".claude/settings.json"]?.ensuredKeys ?? {}),
|
|
35347
36100
|
...CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS
|
|
35348
36101
|
]);
|
|
35349
36102
|
/**
|
|
@@ -35715,7 +36468,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
35715
36468
|
deny,
|
|
35716
36469
|
logger
|
|
35717
36470
|
});
|
|
35718
|
-
const fileContent =
|
|
36471
|
+
const fileContent = serializeSharedConfigFile({
|
|
36472
|
+
fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
|
|
36473
|
+
document: merged,
|
|
36474
|
+
existingContent
|
|
36475
|
+
});
|
|
35719
36476
|
return new ClaudecodePermissions({
|
|
35720
36477
|
outputRoot,
|
|
35721
36478
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -36742,7 +37499,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
36742
37499
|
validate
|
|
36743
37500
|
});
|
|
36744
37501
|
}
|
|
36745
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
|
|
37502
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger }) {
|
|
36746
37503
|
const paths = CopilotPermissions.getSettablePaths();
|
|
36747
37504
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
36748
37505
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
@@ -36762,7 +37519,8 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
36762
37519
|
feature: "permissions",
|
|
36763
37520
|
existingContent,
|
|
36764
37521
|
patch,
|
|
36765
|
-
filePath
|
|
37522
|
+
filePath,
|
|
37523
|
+
logger
|
|
36766
37524
|
}),
|
|
36767
37525
|
validate: true
|
|
36768
37526
|
});
|
|
@@ -37482,6 +38240,18 @@ var DeepagentsPermissions = class DeepagentsPermissions extends ToolPermissions
|
|
|
37482
38240
|
isDeletable() {
|
|
37483
38241
|
return false;
|
|
37484
38242
|
}
|
|
38243
|
+
/**
|
|
38244
|
+
* `config.toml` is dcode's file, not one rulesync owns: rulesync merges into
|
|
38245
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
38246
|
+
* nothing. When no rule maps, `[shell]` is dropped and
|
|
38247
|
+
* `smolToml.stringify({})` leaves a lone newline, which would otherwise be
|
|
38248
|
+
* written as a fresh `~/.deepagents/config.toml` that says nothing. An
|
|
38249
|
+
* existing file is still rewritten as before, so user content is never
|
|
38250
|
+
* dropped — the skip only applies when there is no file yet.
|
|
38251
|
+
*/
|
|
38252
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
38253
|
+
return true;
|
|
38254
|
+
}
|
|
37485
38255
|
static getSettablePaths(_options) {
|
|
37486
38256
|
return {
|
|
37487
38257
|
relativeDirPath: DEEPAGENTS_DIR,
|
|
@@ -40414,7 +41184,8 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
40414
41184
|
feature: "permissions",
|
|
40415
41185
|
existingContent: fileContent ?? "",
|
|
40416
41186
|
patch: { permission },
|
|
40417
|
-
filePath: join(jsonDir, relativeFilePath)
|
|
41187
|
+
filePath: join(jsonDir, relativeFilePath),
|
|
41188
|
+
logger
|
|
40418
41189
|
}),
|
|
40419
41190
|
validate: true
|
|
40420
41191
|
});
|
|
@@ -42539,18 +43310,20 @@ function pickSecurityPolicies(source, report) {
|
|
|
42539
43310
|
* not emoji at all are named beside it: the angle brackets, the trigrams and
|
|
42540
43311
|
* digrams, and the hexagrams of U+4DC0–U+4DFF.
|
|
42541
43312
|
*
|
|
42542
|
-
* The ranges are the standard
|
|
42543
|
-
*
|
|
42544
|
-
*
|
|
42545
|
-
*
|
|
42546
|
-
*
|
|
42547
|
-
*
|
|
42548
|
-
*
|
|
42549
|
-
*
|
|
42550
|
-
*
|
|
42551
|
-
*
|
|
42552
|
-
*
|
|
42553
|
-
*
|
|
43313
|
+
* The ranges are the standard blocks rather than the full `East_Asian_Width`
|
|
43314
|
+
* property, because the wide characters sit in blocks and a block is coarse in
|
|
43315
|
+
* the safe direction: the whole of U+1F000–U+1FAFF is counted as wide, which
|
|
43316
|
+
* overstates the few narrow symbols in it, because overstating a width shortens
|
|
43317
|
+
* a label that did not need it while understating one lets a label wrap. That
|
|
43318
|
+
* is also why an emoji built from a chain of joiners is counted per component:
|
|
43319
|
+
* two columns each rather than the two the whole chain draws.
|
|
43320
|
+
*
|
|
43321
|
+
* The East Asian Ambiguous class is counted at two columns as well, by the
|
|
43322
|
+
* table below this one rather than by these ranges, and that table is the
|
|
43323
|
+
* whole of its property: the class is scattered among the Neutral characters
|
|
43324
|
+
* rather than gathered in blocks, so a block list over it would either miss
|
|
43325
|
+
* members or take in characters that are not wide anywhere. The reason for
|
|
43326
|
+
* counting it, and how the table is kept in step with Unicode, is given there.
|
|
42554
43327
|
*
|
|
42555
43328
|
* Two of the ranges are here for a narrower reason: a name the skill prompt can
|
|
42556
43329
|
* offer may not be counted narrower here than the prompt's own renderer counts
|
|
@@ -42588,6 +43361,64 @@ function pickSecurityPolicies(source, report) {
|
|
|
42588
43361
|
*/
|
|
42589
43362
|
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;
|
|
42590
43363
|
/**
|
|
43364
|
+
* The East Asian Ambiguous class of UAX #11: the characters a terminal draws
|
|
43365
|
+
* one column wide under a Latin font and two columns wide where it is set to
|
|
43366
|
+
* draw the ambiguous class wide, which is a common setting in CJK locales. The
|
|
43367
|
+
* box-drawing characters, the geometric shapes, the Greek and Cyrillic
|
|
43368
|
+
* alphabets, and the accented letters of the Latin-1 supplement are all here.
|
|
43369
|
+
*
|
|
43370
|
+
* Counted at two columns, which is the wider of the two answers, because the
|
|
43371
|
+
* two ways of being wrong are not the same size. Overstating a width shortens
|
|
43372
|
+
* a label that did not need it: a name carrying a Greek letter or an accented
|
|
43373
|
+
* letter is cut a little earlier on a Latin terminal than it had to be.
|
|
43374
|
+
* Understating one lets a forged row wrap: a name of sixty box-drawing
|
|
43375
|
+
* characters and `● pdf-tools` measures 71 columns at one column apiece,
|
|
43376
|
+
* inside the prompt's budget, and draws at 132 in a terminal that draws the
|
|
43377
|
+
* class wide, so the terminal breaks the row itself and puts `● pdf-tools` at
|
|
43378
|
+
* the left margin of a continuation line that carries no pointer and no
|
|
43379
|
+
* checkbox.
|
|
43380
|
+
*
|
|
43381
|
+
* The prompt's own renderer, `fast-string-width`, counts the class at one
|
|
43382
|
+
* column and never sees the wrap coming either; the terminal is what breaks
|
|
43383
|
+
* the row, and only the budget can keep the row short enough not to be broken.
|
|
43384
|
+
*
|
|
43385
|
+
* One width model rather than two — a wide one for bounding an untrusted name
|
|
43386
|
+
* and a narrow one for laying out the tool's own text — because the second
|
|
43387
|
+
* model would buy a column or two of alignment in the tool's own output at the
|
|
43388
|
+
* price of two measurements that have to be kept from being confused for each
|
|
43389
|
+
* other. Where this is used to lay out text of the tool's own, the cost of the
|
|
43390
|
+
* single model is a line cut a little earlier than it needed to be.
|
|
43391
|
+
*
|
|
43392
|
+
* The ranges are the `East_Asian_Width=A` property of Unicode 17.0, taken from
|
|
43393
|
+
* the table `get-east-asian-width` 1.6.0 carries and merged where two are
|
|
43394
|
+
* adjacent. The table is written out here rather than read from the package so
|
|
43395
|
+
* that the one lookup adds no dependency of its own, and the drift test in
|
|
43396
|
+
* `display-width.test.ts` is what keeps it honest: it loads the package through
|
|
43397
|
+
* the prompt renderer that already depends on it, walks every code point, and
|
|
43398
|
+
* fails when this table and the property disagree in either direction. When
|
|
43399
|
+
* the renderer's copy moves to a newer Unicode, that test is what fails, and
|
|
43400
|
+
* the table is regenerated from the new property — its ranges, marks aside,
|
|
43401
|
+
* merged where adjacent and written as escapes — with the version named here.
|
|
43402
|
+
*
|
|
43403
|
+
* Three stretches of the property are left out because they never reach this
|
|
43404
|
+
* pattern: the combining diacritical marks of U+0300–U+036F and the variation
|
|
43405
|
+
* selectors of U+FE00–U+FE0F and U+E0100–U+E01EF are marks by category and are
|
|
43406
|
+
* counted by the mark rule before the width of a character is looked up.
|
|
43407
|
+
* Leaving them out is also what keeps the class from holding a combining
|
|
43408
|
+
* character, which `no-misleading-character-class` is there to catch, and they
|
|
43409
|
+
* are the one exception the drift test allows. U+00AD SOFT HYPHEN never reaches
|
|
43410
|
+
* this pattern either — it is a format character, and the zero-width rule
|
|
43411
|
+
* counts it at nothing first — but it is kept, as the property has it, so that
|
|
43412
|
+
* the test holds the table to the property exactly rather than to a list of
|
|
43413
|
+
* exceptions. The private use areas are in it, as the property says they are: a
|
|
43414
|
+
* name is free to carry them and a terminal is free to draw them wide.
|
|
43415
|
+
*
|
|
43416
|
+
* Escapes rather than the characters themselves, for the reason given above.
|
|
43417
|
+
* Exported for the drift test alone; the width of a string is asked for
|
|
43418
|
+
* through `displayWidthOf`.
|
|
43419
|
+
*/
|
|
43420
|
+
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;
|
|
43421
|
+
/**
|
|
42591
43422
|
* U+FE0F VARIATION SELECTOR-16, which takes no width of its own but asks the
|
|
42592
43423
|
* character before it to be drawn as an emoji — that is, in two columns rather
|
|
42593
43424
|
* than one. Counting it as a column of its own is how that promotion is paid
|
|
@@ -42595,7 +43426,12 @@ const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|
|
|
|
42595
43426
|
* draws rather than the one the heart alone would.
|
|
42596
43427
|
*/
|
|
42597
43428
|
const EMOJI_PRESENTATION_SELECTOR = "️";
|
|
42598
|
-
/**
|
|
43429
|
+
/**
|
|
43430
|
+
* Combining marks are drawn on top of the character before them, not beside it.
|
|
43431
|
+
*
|
|
43432
|
+
* Exported for the drift test on the ambiguous table, which has to leave out
|
|
43433
|
+
* exactly the characters this rule catches first.
|
|
43434
|
+
*/
|
|
42599
43435
|
const COMBINING_MARK_PATTERN = /[\p{Mn}\p{Me}]/u;
|
|
42600
43436
|
/** The characters that take no width at all, marks aside. */
|
|
42601
43437
|
const ZERO_WIDTH_CHARACTERS_PATTERN = /[\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
|
|
@@ -42641,16 +43477,28 @@ function isCombiningMark(character) {
|
|
|
42641
43477
|
return character !== EMOJI_PRESENTATION_SELECTOR && COMBINING_MARK_PATTERN.test(character);
|
|
42642
43478
|
}
|
|
42643
43479
|
/**
|
|
43480
|
+
* A lone surrogate, which is no character at all: a string can carry one — a
|
|
43481
|
+
* `"\ud800"` escape in JSON is enough — and the encoder that writes stdout
|
|
43482
|
+
* replaces it with U+FFFD on the way out, so the replacement character is what
|
|
43483
|
+
* the terminal draws and what has to be measured. U+FFFD is East Asian
|
|
43484
|
+
* Ambiguous, so the difference is a column apiece: 72 of them measured as
|
|
43485
|
+
* themselves fit a 72-column budget and are drawn in 144.
|
|
43486
|
+
*/
|
|
43487
|
+
const LONE_SURROGATE_PATTERN = /\p{Cs}/u;
|
|
43488
|
+
/** What the stdout encoder writes in place of a lone surrogate. */
|
|
43489
|
+
const REPLACEMENT_CHARACTER = "�";
|
|
43490
|
+
/**
|
|
42644
43491
|
* The width of one character, given how many marks already sit on the character
|
|
42645
43492
|
* before it.
|
|
42646
43493
|
*/
|
|
42647
43494
|
function widthInContext(params) {
|
|
42648
|
-
const {
|
|
43495
|
+
const { precedingMarks } = params;
|
|
43496
|
+
const character = LONE_SURROGATE_PATTERN.test(params.character) ? REPLACEMENT_CHARACTER : params.character;
|
|
42649
43497
|
if (character === EMOJI_PRESENTATION_SELECTOR) return 1;
|
|
42650
43498
|
if (isCombiningMark(character)) return precedingMarks < FREE_MARKS_PER_CHARACTER ? 0 : 1;
|
|
42651
43499
|
if (RENDERER_COUNTED_JOINERS.test(character)) return 1;
|
|
42652
43500
|
if (ZERO_WIDTH_CHARACTERS_PATTERN.test(character)) return 0;
|
|
42653
|
-
return WIDE_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
43501
|
+
return WIDE_CHARACTERS_PATTERN.test(character) || AMBIGUOUS_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
42654
43502
|
}
|
|
42655
43503
|
/**
|
|
42656
43504
|
* How many terminal columns a string occupies.
|
|
@@ -42674,6 +43522,20 @@ function displayWidthOf(text) {
|
|
|
42674
43522
|
/** The mark a cut string ends in. */
|
|
42675
43523
|
const SHORTENING_ELLIPSIS = "…";
|
|
42676
43524
|
/**
|
|
43525
|
+
* The columns a cut string is drawn in at the very least: what is left when
|
|
43526
|
+
* the budget has room for the mark of the cut and nothing before it. Two, since
|
|
43527
|
+
* the ellipsis is itself East Asian Ambiguous and is measured the way every
|
|
43528
|
+
* other such character is, so that a shortened label is not wider than it was
|
|
43529
|
+
* measured to be on the terminal the measurement is for.
|
|
43530
|
+
*
|
|
43531
|
+
* Exported because a caller composing several shortened pieces into one line
|
|
43532
|
+
* has to leave room for it to decide which of them gives way, and the width of
|
|
43533
|
+
* the mark is this module's business rather than something to be counted again
|
|
43534
|
+
* at the other end. Leaving room for it is not what bounds the line: cutting
|
|
43535
|
+
* the composed line is.
|
|
43536
|
+
*/
|
|
43537
|
+
const ELLIPSIS_WIDTH = displayWidthOf(SHORTENING_ELLIPSIS);
|
|
43538
|
+
/**
|
|
42677
43539
|
* Cut `text` down to at most `budget` columns, marking the cut with an ellipsis.
|
|
42678
43540
|
*
|
|
42679
43541
|
* The ellipsis is paid for out of the budget rather than added on top of it, so
|
|
@@ -42684,7 +43546,7 @@ const SHORTENING_ELLIPSIS = "…";
|
|
|
42684
43546
|
function shortenToWidth(params) {
|
|
42685
43547
|
const { text, budget } = params;
|
|
42686
43548
|
if (displayWidthOf(text) <= budget) return text;
|
|
42687
|
-
const target = budget -
|
|
43549
|
+
const target = budget - ELLIPSIS_WIDTH;
|
|
42688
43550
|
let width = 0;
|
|
42689
43551
|
let marks = 0;
|
|
42690
43552
|
const kept = [];
|
|
@@ -45622,7 +46484,7 @@ var AgentsmdSkill = class AgentsmdSkill extends SimulatedSkill {
|
|
|
45622
46484
|
const relativeDirPath = this.getSettablePaths().relativeDirPath;
|
|
45623
46485
|
const frontmatter = {
|
|
45624
46486
|
...defaults.frontmatter,
|
|
45625
|
-
...toSpecConformantAgentSkillFields(params.rulesyncSkill.getFrontmatter()
|
|
46487
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(params.rulesyncSkill.getFrontmatter()))
|
|
45626
46488
|
};
|
|
45627
46489
|
AgentsSkillsSkill.reportSpecViolations({
|
|
45628
46490
|
outputRoot: params.outputRoot ?? process.cwd(),
|
|
@@ -45748,13 +46610,25 @@ var RovodevSkill = class RovodevSkill extends ToolSkill {
|
|
|
45748
46610
|
const settablePaths = RovodevSkill.getSettablePaths({ global });
|
|
45749
46611
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
45750
46612
|
const rovodevSection = rulesyncFrontmatter.rovodev;
|
|
46613
|
+
const license = resolveLicense({
|
|
46614
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46615
|
+
section: rovodevSection
|
|
46616
|
+
});
|
|
46617
|
+
const compatibility = resolveCompatibility({
|
|
46618
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46619
|
+
section: rovodevSection
|
|
46620
|
+
});
|
|
46621
|
+
const metadata = resolveMetadata({
|
|
46622
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
46623
|
+
section: rovodevSection
|
|
46624
|
+
});
|
|
45751
46625
|
const rovodevFrontmatter = {
|
|
45752
46626
|
name: rulesyncFrontmatter.name,
|
|
45753
46627
|
description: rulesyncFrontmatter.description,
|
|
45754
46628
|
...rovodevSection?.["allowed-tools"] !== void 0 && { "allowed-tools": rovodevSection["allowed-tools"] },
|
|
45755
|
-
...
|
|
45756
|
-
...
|
|
45757
|
-
...
|
|
46629
|
+
...license !== void 0 && { license },
|
|
46630
|
+
...compatibility !== void 0 && { compatibility },
|
|
46631
|
+
...metadata !== void 0 && { metadata }
|
|
45758
46632
|
};
|
|
45759
46633
|
return new RovodevSkill({
|
|
45760
46634
|
outputRoot,
|
|
@@ -45936,7 +46810,12 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45936
46810
|
composed: file.composed
|
|
45937
46811
|
})) dirHasChanges = true;
|
|
45938
46812
|
}
|
|
45939
|
-
if (!dirHasChanges)
|
|
46813
|
+
if (!dirHasChanges) {
|
|
46814
|
+
if (!this.dryRun) {
|
|
46815
|
+
for (const file of otherFiles) if (file.fileMode !== void 0) await restoreMissingExecutableBit(join(dirPath, file.relativeFilePathToDirPath), file.fileMode);
|
|
46816
|
+
}
|
|
46817
|
+
continue;
|
|
46818
|
+
}
|
|
45940
46819
|
const relativeDir = aiDir.getRelativePathFromCwd();
|
|
45941
46820
|
if (this.dryRun) {
|
|
45942
46821
|
this.logger.info(`[DRY RUN] Would create directory: ${stripControlCharacters(dirPath)}`);
|
|
@@ -45955,7 +46834,9 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45955
46834
|
changedPaths.push(join(relativeDir, mainFile.name));
|
|
45956
46835
|
}
|
|
45957
46836
|
for (const file of otherFiles) {
|
|
45958
|
-
|
|
46837
|
+
const filePath = join(dirPath, file.relativeFilePathToDirPath);
|
|
46838
|
+
await writeFileBuffer(filePath, file.fileBuffer);
|
|
46839
|
+
if (file.fileMode !== void 0) await applyFileMode(filePath, file.fileMode);
|
|
45959
46840
|
changedPaths.push(join(relativeDir, file.relativeFilePathToDirPath));
|
|
45960
46841
|
}
|
|
45961
46842
|
}
|
|
@@ -45973,7 +46854,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45973
46854
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
45974
46855
|
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
45975
46856
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
45976
|
-
const quotedOutputRoot =
|
|
46857
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
45977
46858
|
for (const aiDir of existingDirs) {
|
|
45978
46859
|
const dirPath = aiDir.getDirPath();
|
|
45979
46860
|
const { verdict, root } = locateInOwnRoot({
|
|
@@ -45981,14 +46862,14 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
45981
46862
|
dirPath,
|
|
45982
46863
|
outputRoot: this.outputRoot
|
|
45983
46864
|
});
|
|
45984
|
-
const quotedDirPath =
|
|
45985
|
-
const quotedRoot =
|
|
46865
|
+
const quotedDirPath = quoteForLog(dirPath);
|
|
46866
|
+
const quotedRoot = quoteForLog(root);
|
|
45986
46867
|
if (verdict === "root-outside") {
|
|
45987
46868
|
this.logger.warn(`Refusing to delete ${quotedDirPath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
45988
46869
|
continue;
|
|
45989
46870
|
}
|
|
45990
46871
|
if (!aiDir.ownsDirTree()) {
|
|
45991
|
-
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${
|
|
46872
|
+
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${quoteForLog(aiDir.getDirName())}: ${quotedDirPath} is a shared root, not a directory of its own`);
|
|
45992
46873
|
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`);
|
|
45993
46874
|
continue;
|
|
45994
46875
|
}
|
|
@@ -46004,6 +46885,126 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46004
46885
|
});
|
|
46005
46886
|
}
|
|
46006
46887
|
/**
|
|
46888
|
+
* Remove the files left inside a directory this run still generates, but
|
|
46889
|
+
* which the run no longer writes.
|
|
46890
|
+
*
|
|
46891
|
+
* The directory sweep above cannot see them: it removes a directory that no
|
|
46892
|
+
* longer corresponds to any generated entry, and never looks inside one that
|
|
46893
|
+
* does. So deleting a companion file from a source directory that is
|
|
46894
|
+
* otherwise kept left the generated copy in place — and, because change
|
|
46895
|
+
* detection compares only the files the run will write, the run reported
|
|
46896
|
+
* itself up to date while an agent went on reading the stale file. The same
|
|
46897
|
+
* gap left any file that was never rulesync's sitting inside a directory the
|
|
46898
|
+
* user now believes rulesync owns.
|
|
46899
|
+
*
|
|
46900
|
+
* Only a directory that owns its whole tree is swept, and only when this run
|
|
46901
|
+
* generated it. That is the same claim the directory sweep already acts on,
|
|
46902
|
+
* one level down: a directory whose entry disappears is deleted outright,
|
|
46903
|
+
* companion files and all, so a file inside one whose entry is still here and
|
|
46904
|
+
* which no source produces is stale by exactly the same reasoning.
|
|
46905
|
+
*
|
|
46906
|
+
* Two kinds of file are left alone:
|
|
46907
|
+
*
|
|
46908
|
+
* - **Hidden entries.** The loader does carry a hidden companion — a
|
|
46909
|
+
* `.env.example` beside the script that reads it is skill content — so a
|
|
46910
|
+
* hidden file here may be rulesync's. But a hidden name is also where a
|
|
46911
|
+
* user's own files live: a `.gitkeep`, a `.env` with real values in it. The
|
|
46912
|
+
* sweep cannot tell the two apart by name, and deleting the user's is the
|
|
46913
|
+
* worse mistake, so a stale hidden companion is the one leftover it
|
|
46914
|
+
* knowingly keeps.
|
|
46915
|
+
* - **Symbolic links**, which the writer never creates. The walk neither
|
|
46916
|
+
* follows nor reports them, so a link is never removed and never resolved
|
|
46917
|
+
* into a deletion somewhere outside the tree.
|
|
46918
|
+
*
|
|
46919
|
+
* Nothing is swept at all by a run that could not read its sources in full.
|
|
46920
|
+
* `AiDir` drops a companion file it cannot open, and stops short of a subtree
|
|
46921
|
+
* it is denied or that runs past one of its bounds -- warning each time, but
|
|
46922
|
+
* carrying on, because a skill that is short one file is still worth writing.
|
|
46923
|
+
* The output copy of such a file is then indistinguishable here from a file
|
|
46924
|
+
* whose source was deleted, and the wrong guess deletes something the next
|
|
46925
|
+
* readable run would put straight back. So a shortfall anywhere in the run
|
|
46926
|
+
* calls the whole sweep off: it is the sweeps that are optional, not the
|
|
46927
|
+
* files.
|
|
46928
|
+
*
|
|
46929
|
+
* @param isClaimed - Whether some other target or feature in this run wrote
|
|
46930
|
+
* this exact path. A shared output root -- `.agents/skills/`, written by
|
|
46931
|
+
* several targets at once -- is a directory whose entry here lists only
|
|
46932
|
+
* *this* target's files, so without the run's own record a sibling's fresh
|
|
46933
|
+
* output reads as an orphan. Asked per path rather than per tree: a tree
|
|
46934
|
+
* claim covers the directory this sweep is looking inside of, and would
|
|
46935
|
+
* answer yes to every file in it.
|
|
46936
|
+
*/
|
|
46937
|
+
async removeOrphanFilesInAiDirs({ generatedDirs, isClaimed }) {
|
|
46938
|
+
if (hasIncompleteCarriedFiles()) {
|
|
46939
|
+
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.");
|
|
46940
|
+
return 0;
|
|
46941
|
+
}
|
|
46942
|
+
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46943
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46944
|
+
for (const aiDir of generatedDirs) {
|
|
46945
|
+
const dirPath = aiDir.getDirPath();
|
|
46946
|
+
const { verdict, root } = locateInOwnRoot({
|
|
46947
|
+
aiDir,
|
|
46948
|
+
dirPath,
|
|
46949
|
+
outputRoot: this.outputRoot
|
|
46950
|
+
});
|
|
46951
|
+
const quotedDirPath = quoteForLog(dirPath);
|
|
46952
|
+
const quotedRoot = quoteForLog(root);
|
|
46953
|
+
if (verdict === "root-outside") {
|
|
46954
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
46955
|
+
continue;
|
|
46956
|
+
}
|
|
46957
|
+
if (!aiDir.ownsDirTree()) {
|
|
46958
|
+
if (verdict === "equal") this.logger.debug(`Skipping orphan sweep for ${quoteForLog(aiDir.getDirName())}: ${quotedDirPath} is a shared root, not a directory of its own`);
|
|
46959
|
+
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`);
|
|
46960
|
+
continue;
|
|
46961
|
+
}
|
|
46962
|
+
if (verdict !== "inside") {
|
|
46963
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: it is not a directory inside ${quotedRoot}, the root it was found in`);
|
|
46964
|
+
continue;
|
|
46965
|
+
}
|
|
46966
|
+
try {
|
|
46967
|
+
await assertWritablePathInsideRoot({
|
|
46968
|
+
rootPath: this.outputRoot,
|
|
46969
|
+
targetPath: dirPath
|
|
46970
|
+
});
|
|
46971
|
+
} catch (error) {
|
|
46972
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
46973
|
+
continue;
|
|
46974
|
+
}
|
|
46975
|
+
const generatedNames = /* @__PURE__ */ new Set();
|
|
46976
|
+
const mainFile = aiDir.getMainFile();
|
|
46977
|
+
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
46978
|
+
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
46979
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => name.toLowerCase()));
|
|
46980
|
+
let existingNames;
|
|
46981
|
+
try {
|
|
46982
|
+
existingNames = await listFilePathsRecursively(dirPath, {
|
|
46983
|
+
followSymbolicLinks: false,
|
|
46984
|
+
includeHidden: false
|
|
46985
|
+
});
|
|
46986
|
+
} catch (error) {
|
|
46987
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
46988
|
+
continue;
|
|
46989
|
+
}
|
|
46990
|
+
for (const existingName of existingNames) {
|
|
46991
|
+
const posixName = toPosixPath(existingName);
|
|
46992
|
+
if (generatedNames.has(posixName)) continue;
|
|
46993
|
+
const filePath = join(dirPath, existingName);
|
|
46994
|
+
if (generatedNamesFolded.has(posixName.toLowerCase())) {
|
|
46995
|
+
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`);
|
|
46996
|
+
continue;
|
|
46997
|
+
}
|
|
46998
|
+
if (isClaimed(filePath)) continue;
|
|
46999
|
+
orphanPaths.add(filePath);
|
|
47000
|
+
}
|
|
47001
|
+
}
|
|
47002
|
+
return await this.deleteOrphanPaths({
|
|
47003
|
+
paths: orphanPaths,
|
|
47004
|
+
kind: "file"
|
|
47005
|
+
});
|
|
47006
|
+
}
|
|
47007
|
+
/**
|
|
46007
47008
|
* Delete the paths the sweeps decided on, or report what a real run would
|
|
46008
47009
|
* have deleted. Shared by both halves so the dry-run wording, the quoting of
|
|
46009
47010
|
* a name that came off disk, and the count they return stay one behavior
|
|
@@ -46011,7 +47012,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46011
47012
|
*/
|
|
46012
47013
|
async deleteOrphanPaths({ paths, kind }) {
|
|
46013
47014
|
for (const targetPath of paths) {
|
|
46014
|
-
const loggedPath =
|
|
47015
|
+
const loggedPath = quoteForLog(targetPath);
|
|
46015
47016
|
if (this.dryRun) this.logger.info(`[DRY RUN] Would delete ${kind}: ${loggedPath}`);
|
|
46016
47017
|
else {
|
|
46017
47018
|
await (kind === "directory" ? removeDirectory(targetPath) : removeFile(targetPath));
|
|
@@ -46048,11 +47049,11 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46048
47049
|
}
|
|
46049
47050
|
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath.toLowerCase()));
|
|
46050
47051
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46051
|
-
const quotedOutputRoot =
|
|
47052
|
+
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46052
47053
|
for (const aiDir of existingFlatFiles) {
|
|
46053
47054
|
const filePath = aiDir.getFlatFilePath();
|
|
46054
47055
|
if (filePath === void 0) {
|
|
46055
|
-
this.logger.warn(`Refusing to sweep ${
|
|
47056
|
+
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`);
|
|
46056
47057
|
continue;
|
|
46057
47058
|
}
|
|
46058
47059
|
const dirPath = aiDir.getDirPath();
|
|
@@ -46061,8 +47062,8 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46061
47062
|
dirPath,
|
|
46062
47063
|
outputRoot: this.outputRoot
|
|
46063
47064
|
});
|
|
46064
|
-
const quotedFilePath =
|
|
46065
|
-
const quotedRoot =
|
|
47065
|
+
const quotedFilePath = quoteForLog(filePath);
|
|
47066
|
+
const quotedRoot = quoteForLog(root);
|
|
46066
47067
|
if (verdict === "root-outside") {
|
|
46067
47068
|
this.logger.warn(`Refusing to delete ${quotedFilePath}: the root ${quotedRoot} it was found in is not inside ${quotedOutputRoot}, the directory this run writes to`);
|
|
46068
47069
|
continue;
|
|
@@ -46804,9 +47805,9 @@ async function checkNestedSkillsRoot({ outputRoot, dirPath }) {
|
|
|
46804
47805
|
if (posixRelativePathEscapesRoot(realRelativeDirPath)) return { reason: "it resolves outside the project." };
|
|
46805
47806
|
const segments = realRelativeDirPath.split("/");
|
|
46806
47807
|
const aboveTailSegments = segments.slice(0, -CLAUDECODE_SKILLS_DIR_SEGMENTS.length);
|
|
46807
|
-
if (segments.slice(-CLAUDECODE_SKILLS_DIR_SEGMENTS.length).join("/") !== CLAUDECODE_SKILLS_DIR_POSIX_PATH) return { reason: `it resolves to ${realRelativeDirPath === "" ? "the project root" :
|
|
47808
|
+
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.` };
|
|
46808
47809
|
const excludedSegment = excludedNestedScanSegment(aboveTailSegments);
|
|
46809
|
-
if (excludedSegment !== void 0) return { reason: `it resolves inside ${
|
|
47810
|
+
if (excludedSegment !== void 0) return { reason: `it resolves inside ${quoteForLog(excludedSegment)}, which the nested scan excludes.` };
|
|
46810
47811
|
return { realRelativeDirPath };
|
|
46811
47812
|
}
|
|
46812
47813
|
const ClaudecodeSkillFrontmatterSchema = z.looseObject({
|
|
@@ -46857,9 +47858,18 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
|
|
|
46857
47858
|
"disable-model-invocation": resolvedDisableModelInvocation,
|
|
46858
47859
|
"user-invocable": resolvedUserInvocable,
|
|
46859
47860
|
paths: section.paths,
|
|
46860
|
-
license:
|
|
46861
|
-
|
|
46862
|
-
|
|
47861
|
+
license: resolveLicense({
|
|
47862
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47863
|
+
section
|
|
47864
|
+
}),
|
|
47865
|
+
compatibility: resolveCompatibility({
|
|
47866
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47867
|
+
section
|
|
47868
|
+
}),
|
|
47869
|
+
metadata: resolveMetadata({
|
|
47870
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
47871
|
+
section
|
|
47872
|
+
})
|
|
46863
47873
|
};
|
|
46864
47874
|
const frontmatter = {
|
|
46865
47875
|
name: rulesyncFrontmatter.name,
|
|
@@ -47059,21 +48069,25 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
47059
48069
|
*/
|
|
47060
48070
|
static async getConfiguredImportRoots({ outputRoot, global = false, logger }) {
|
|
47061
48071
|
if (global) return [];
|
|
47062
|
-
const
|
|
48072
|
+
const skillsDirPath = toPosixPath(CLAUDECODE_SKILLS_DIR_PATH);
|
|
47063
48073
|
const filteredDirPaths = filterOutPathsInGitIgnoredDirectories({
|
|
47064
48074
|
rootDir: outputRoot,
|
|
47065
|
-
filePaths: await findFilesByGlobs([
|
|
48075
|
+
filePaths: await findFilesByGlobs([`*/**/${skillsDirPath}`], {
|
|
48076
|
+
cwd: outputRoot,
|
|
47066
48077
|
type: "dir",
|
|
47067
48078
|
followSymbolicLinks: false,
|
|
47068
48079
|
ignore: [
|
|
47069
|
-
|
|
47070
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
47071
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
48080
|
+
`**/.*/**/${skillsDirPath}`,
|
|
48081
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
48082
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
47072
48083
|
]
|
|
47073
48084
|
})
|
|
47074
48085
|
}).toSorted();
|
|
47075
48086
|
const roots = [];
|
|
47076
|
-
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH
|
|
48087
|
+
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH, await resolvedRelativePath({
|
|
48088
|
+
rootPath: outputRoot,
|
|
48089
|
+
targetPath: join(outputRoot, CLAUDECODE_SKILLS_DIR_PATH)
|
|
48090
|
+
})]);
|
|
47077
48091
|
for (const dirPath of filteredDirPaths) {
|
|
47078
48092
|
const scannedDirPath = resolve(dirPath);
|
|
47079
48093
|
const check = await checkNestedSkillsRoot({
|
|
@@ -47081,7 +48095,7 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
47081
48095
|
dirPath: scannedDirPath
|
|
47082
48096
|
});
|
|
47083
48097
|
if ("reason" in check) {
|
|
47084
|
-
logger?.warn(`Skipping the nested Claude Code skills directory ${
|
|
48098
|
+
logger?.warn(`Skipping the nested Claude Code skills directory ${quoteForLog(scannedDirPath)}: ${check.reason} Its skills are not imported.`);
|
|
47085
48099
|
continue;
|
|
47086
48100
|
}
|
|
47087
48101
|
if (seenRealRelativeDirPaths.has(check.realRelativeDirPath)) continue;
|
|
@@ -47668,11 +48682,16 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
|
|
|
47668
48682
|
rootFrontmatter: rulesyncFrontmatter,
|
|
47669
48683
|
section: copilotSection
|
|
47670
48684
|
});
|
|
47671
|
-
const
|
|
48685
|
+
const license = resolveLicense({
|
|
48686
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
48687
|
+
section: copilotSection
|
|
48688
|
+
});
|
|
48689
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, license: _license, ...copilotFields } = copilotSection ?? {};
|
|
47672
48690
|
const copilotFrontmatter = {
|
|
47673
48691
|
...copilotFields,
|
|
47674
48692
|
name: rulesyncFrontmatter.name,
|
|
47675
48693
|
description: rulesyncFrontmatter.description,
|
|
48694
|
+
...license !== void 0 && { license },
|
|
47676
48695
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
47677
48696
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
47678
48697
|
};
|
|
@@ -47823,11 +48842,16 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
47823
48842
|
rootFrontmatter: rulesyncFrontmatter,
|
|
47824
48843
|
section: copilotcliSection
|
|
47825
48844
|
});
|
|
47826
|
-
const
|
|
48845
|
+
const license = resolveLicense({
|
|
48846
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
48847
|
+
section: copilotcliSection
|
|
48848
|
+
});
|
|
48849
|
+
const { "user-invocable": _userInvocable, "disable-model-invocation": _disableModelInvocation, license: _license, ...copilotcliFields } = copilotcliSection ?? {};
|
|
47827
48850
|
const copilotcliFrontmatter = {
|
|
47828
48851
|
...copilotcliFields,
|
|
47829
48852
|
name: rulesyncFrontmatter.name,
|
|
47830
48853
|
description: rulesyncFrontmatter.description,
|
|
48854
|
+
...license !== void 0 && { license },
|
|
47831
48855
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
47832
48856
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
47833
48857
|
};
|
|
@@ -47978,13 +49002,17 @@ var CursorSkill = class CursorSkill extends ToolSkill {
|
|
|
47978
49002
|
rootFrontmatter: rulesyncFrontmatter,
|
|
47979
49003
|
section: cursorSection
|
|
47980
49004
|
});
|
|
49005
|
+
const metadata = resolveMetadata({
|
|
49006
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49007
|
+
section: cursorSection
|
|
49008
|
+
});
|
|
47981
49009
|
const cursorFrontmatter = {
|
|
47982
49010
|
name: rulesyncFrontmatter.name,
|
|
47983
49011
|
description: rulesyncFrontmatter.description,
|
|
47984
49012
|
...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
|
|
47985
49013
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
47986
49014
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
47987
|
-
...
|
|
49015
|
+
...metadata !== void 0 && { metadata }
|
|
47988
49016
|
};
|
|
47989
49017
|
return new CursorSkill({
|
|
47990
49018
|
outputRoot,
|
|
@@ -48125,13 +49153,25 @@ var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
|
48125
49153
|
const deepagentsSection = rulesyncFrontmatter.deepagents;
|
|
48126
49154
|
const allowedTools = deepagentsSection?.["allowed-tools"];
|
|
48127
49155
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
49156
|
+
const license = resolveLicense({
|
|
49157
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49158
|
+
section: deepagentsSection
|
|
49159
|
+
});
|
|
49160
|
+
const compatibility = resolveCompatibility({
|
|
49161
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49162
|
+
section: deepagentsSection
|
|
49163
|
+
});
|
|
49164
|
+
const metadata = resolveMetadata({
|
|
49165
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49166
|
+
section: deepagentsSection
|
|
49167
|
+
});
|
|
48128
49168
|
const deepagentsFrontmatter = {
|
|
48129
49169
|
name: rulesyncFrontmatter.name,
|
|
48130
49170
|
description: rulesyncFrontmatter.description,
|
|
48131
49171
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
48132
|
-
...
|
|
48133
|
-
...
|
|
48134
|
-
...
|
|
49172
|
+
...license !== void 0 && { license },
|
|
49173
|
+
...compatibility !== void 0 && { compatibility },
|
|
49174
|
+
...metadata !== void 0 && { metadata }
|
|
48135
49175
|
};
|
|
48136
49176
|
return new DeepagentsSkill({
|
|
48137
49177
|
outputRoot,
|
|
@@ -48483,11 +49523,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
48483
49523
|
rootFrontmatter: rulesyncFrontmatter,
|
|
48484
49524
|
section: factorydroidSection
|
|
48485
49525
|
});
|
|
49526
|
+
const license = resolveLicense({
|
|
49527
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49528
|
+
section: factorydroidSection
|
|
49529
|
+
});
|
|
49530
|
+
const compatibility = resolveCompatibility({
|
|
49531
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49532
|
+
section: factorydroidSection
|
|
49533
|
+
});
|
|
49534
|
+
const metadata = resolveMetadata({
|
|
49535
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49536
|
+
section: factorydroidSection
|
|
49537
|
+
});
|
|
48486
49538
|
const { name: _sectionName, description: _sectionDescription, ...section } = factorydroidSection ?? {};
|
|
48487
49539
|
const factorydroidFrontmatter = {
|
|
48488
49540
|
name: rulesyncFrontmatter.name,
|
|
48489
49541
|
description: rulesyncFrontmatter.description,
|
|
48490
49542
|
...section,
|
|
49543
|
+
...license !== void 0 && { license },
|
|
49544
|
+
...compatibility !== void 0 && { compatibility },
|
|
49545
|
+
...metadata !== void 0 && { metadata },
|
|
48491
49546
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
48492
49547
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable }
|
|
48493
49548
|
};
|
|
@@ -49079,7 +50134,7 @@ const KiloSkillFrontmatterSchema = z.looseObject({
|
|
|
49079
50134
|
name: z.string(),
|
|
49080
50135
|
description: z.string(),
|
|
49081
50136
|
license: z.optional(z.string()),
|
|
49082
|
-
compatibility: z.optional(z.looseObject({})),
|
|
50137
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
49083
50138
|
metadata: z.optional(z.looseObject({})),
|
|
49084
50139
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
49085
50140
|
});
|
|
@@ -49154,13 +50209,25 @@ var KiloSkill = class KiloSkill extends ToolSkill {
|
|
|
49154
50209
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49155
50210
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49156
50211
|
const kiloSection = rulesyncFrontmatter.kilo;
|
|
50212
|
+
const license = resolveLicense({
|
|
50213
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50214
|
+
section: kiloSection
|
|
50215
|
+
});
|
|
50216
|
+
const compatibility = resolveCompatibility({
|
|
50217
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50218
|
+
section: kiloSection
|
|
50219
|
+
});
|
|
50220
|
+
const metadata = resolveMetadata({
|
|
50221
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50222
|
+
section: kiloSection
|
|
50223
|
+
});
|
|
49157
50224
|
const kiloFrontmatter = {
|
|
49158
50225
|
name: rulesyncFrontmatter.name,
|
|
49159
50226
|
description: rulesyncFrontmatter.description,
|
|
49160
50227
|
...kiloSection?.["allowed-tools"] !== void 0 && { "allowed-tools": kiloSection["allowed-tools"] },
|
|
49161
|
-
...
|
|
49162
|
-
...
|
|
49163
|
-
...
|
|
50228
|
+
...license !== void 0 && { license },
|
|
50229
|
+
...compatibility !== void 0 && { compatibility },
|
|
50230
|
+
...metadata !== void 0 && { metadata }
|
|
49164
50231
|
};
|
|
49165
50232
|
const settablePaths = KiloSkill.getSettablePaths({ global });
|
|
49166
50233
|
return new KiloSkill({
|
|
@@ -49492,10 +50559,26 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
49492
50559
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49493
50560
|
const settablePaths = KiroSkill.getSettablePaths({ global });
|
|
49494
50561
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
50562
|
+
const kiroSection = rulesyncFrontmatter.kiro;
|
|
50563
|
+
const license = resolveLicense({
|
|
50564
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50565
|
+
section: kiroSection
|
|
50566
|
+
});
|
|
50567
|
+
const compatibility = resolveCompatibility({
|
|
50568
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50569
|
+
section: kiroSection
|
|
50570
|
+
});
|
|
50571
|
+
const metadata = resolveMetadata({
|
|
50572
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50573
|
+
section: kiroSection
|
|
50574
|
+
});
|
|
49495
50575
|
const kiroFrontmatter = {
|
|
49496
|
-
...
|
|
50576
|
+
...kiroSection,
|
|
49497
50577
|
name: rulesyncFrontmatter.name,
|
|
49498
|
-
description: rulesyncFrontmatter.description
|
|
50578
|
+
description: rulesyncFrontmatter.description,
|
|
50579
|
+
...license !== void 0 && { license },
|
|
50580
|
+
...compatibility !== void 0 && { compatibility },
|
|
50581
|
+
...metadata !== void 0 && { metadata }
|
|
49499
50582
|
};
|
|
49500
50583
|
return new KiroSkill({
|
|
49501
50584
|
outputRoot,
|
|
@@ -49729,15 +50812,6 @@ const OpenCodeSkillFrontmatterSchema = z.looseObject({
|
|
|
49729
50812
|
metadata: z.optional(z.looseObject({})),
|
|
49730
50813
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
49731
50814
|
});
|
|
49732
|
-
/**
|
|
49733
|
-
* Reads a top-level `compatibility` value from rulesync frontmatter, accepting
|
|
49734
|
-
* both the documented string form (e.g. `compatibility: opencode`) and the
|
|
49735
|
-
* legacy object form. Returns `undefined` for any other shape.
|
|
49736
|
-
*/
|
|
49737
|
-
function readTopLevelCompatibility(value) {
|
|
49738
|
-
if (typeof value === "string") return value;
|
|
49739
|
-
if (typeof value === "object" && value !== null) return value;
|
|
49740
|
-
}
|
|
49741
50815
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
49742
50816
|
constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49743
50817
|
super({
|
|
@@ -49773,9 +50847,16 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49773
50847
|
* configured path is read but never generated into. `skills.urls` is a
|
|
49774
50848
|
* remote-fetch surface and is out of scope for a file-based generator.
|
|
49775
50849
|
*
|
|
49776
|
-
* Absolute paths and paths escaping the
|
|
49777
|
-
* root is joined onto
|
|
49778
|
-
* a project config should be able to ask for.
|
|
50850
|
+
* Absolute paths and paths escaping the config directory are dropped — an
|
|
50851
|
+
* import root is joined onto that directory, and reaching outside it is not
|
|
50852
|
+
* something a project config should be able to ask for. A path is judged by
|
|
50853
|
+
* where it resolves, not by how it is spelled: a relative name that is a
|
|
50854
|
+
* symbolic link pointing out of the directory is an escape all the same and
|
|
50855
|
+
* is dropped too. A path that does not exist has nothing to resolve and is
|
|
50856
|
+
* compared as spelled against the resolved config directory, so it survives
|
|
50857
|
+
* only when that directory resolves to its own spelling and is dropped when
|
|
50858
|
+
* the directory is itself reached through a link. Either way it yields no
|
|
50859
|
+
* skills — the scan finds no directory under it.
|
|
49779
50860
|
*
|
|
49780
50861
|
* @see https://opencode.ai/config.json
|
|
49781
50862
|
*/
|
|
@@ -49789,7 +50870,12 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49789
50870
|
outputRoot,
|
|
49790
50871
|
global
|
|
49791
50872
|
});
|
|
49792
|
-
|
|
50873
|
+
const lexicallyContained = skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !isAbsolute(candidate) && !normalize(candidate).startsWith(".."));
|
|
50874
|
+
const escapes = await Promise.all(lexicallyContained.map((candidate) => resolvedPathEscapesRoot({
|
|
50875
|
+
rootPath: configDir,
|
|
50876
|
+
targetPath: join(configDir, candidate)
|
|
50877
|
+
})));
|
|
50878
|
+
return lexicallyContained.filter((_, index) => !escapes[index]).map((relativeDirPath) => ({
|
|
49793
50879
|
outputRoot: configDir,
|
|
49794
50880
|
relativeDirPath
|
|
49795
50881
|
}));
|
|
@@ -49843,13 +50929,18 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
|
49843
50929
|
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49844
50930
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49845
50931
|
const opencodeSection = rulesyncFrontmatter.opencode;
|
|
49846
|
-
const
|
|
49847
|
-
|
|
49848
|
-
|
|
49849
|
-
|
|
49850
|
-
const
|
|
49851
|
-
|
|
49852
|
-
|
|
50932
|
+
const license = resolveLicense({
|
|
50933
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50934
|
+
section: opencodeSection
|
|
50935
|
+
});
|
|
50936
|
+
const compatibility = resolveCompatibility({
|
|
50937
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50938
|
+
section: opencodeSection
|
|
50939
|
+
});
|
|
50940
|
+
const metadata = resolveMetadata({
|
|
50941
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
50942
|
+
section: opencodeSection
|
|
50943
|
+
});
|
|
49853
50944
|
const opencodeFrontmatter = {
|
|
49854
50945
|
name: rulesyncFrontmatter.name,
|
|
49855
50946
|
description: rulesyncFrontmatter.description,
|
|
@@ -50018,11 +51109,26 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
50018
51109
|
});
|
|
50019
51110
|
const { "allowed-tools": allowedTools, ...piSectionRest } = piSection ?? {};
|
|
50020
51111
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
51112
|
+
const license = resolveLicense({
|
|
51113
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51114
|
+
section: piSection
|
|
51115
|
+
});
|
|
51116
|
+
const compatibility = resolveCompatibility({
|
|
51117
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51118
|
+
section: piSection
|
|
51119
|
+
});
|
|
51120
|
+
const metadata = resolveMetadata({
|
|
51121
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51122
|
+
section: piSection
|
|
51123
|
+
});
|
|
50021
51124
|
const piFrontmatter = {
|
|
50022
51125
|
name: rulesyncFrontmatter.name,
|
|
50023
51126
|
description: rulesyncFrontmatter.description,
|
|
50024
51127
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
50025
51128
|
...piSectionRest,
|
|
51129
|
+
...license !== void 0 && { license },
|
|
51130
|
+
...compatibility !== void 0 && { compatibility },
|
|
51131
|
+
...metadata !== void 0 && { metadata },
|
|
50026
51132
|
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation }
|
|
50027
51133
|
};
|
|
50028
51134
|
return new PiSkill({
|
|
@@ -50504,11 +51610,26 @@ var ReplitSkill = class ReplitSkill extends ToolSkill {
|
|
|
50504
51610
|
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
50505
51611
|
const { "allowed-tools": allowedTools, ...replitSection } = rulesyncFrontmatter.replit ?? {};
|
|
50506
51612
|
const allowedToolsString = Array.isArray(allowedTools) ? allowedTools.join(" ") : allowedTools;
|
|
51613
|
+
const license = resolveLicense({
|
|
51614
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51615
|
+
section: replitSection
|
|
51616
|
+
});
|
|
51617
|
+
const compatibility = resolveCompatibility({
|
|
51618
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51619
|
+
section: replitSection
|
|
51620
|
+
});
|
|
51621
|
+
const metadata = resolveMetadata({
|
|
51622
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51623
|
+
section: replitSection
|
|
51624
|
+
});
|
|
50507
51625
|
const replitFrontmatter = {
|
|
50508
51626
|
name: rulesyncFrontmatter.name,
|
|
50509
51627
|
description: rulesyncFrontmatter.description,
|
|
50510
51628
|
...allowedToolsString && { "allowed-tools": allowedToolsString },
|
|
50511
|
-
...replitSection
|
|
51629
|
+
...replitSection,
|
|
51630
|
+
...license !== void 0 && { license },
|
|
51631
|
+
...compatibility !== void 0 && { compatibility },
|
|
51632
|
+
...metadata !== void 0 && { metadata }
|
|
50512
51633
|
};
|
|
50513
51634
|
return new ReplitSkill({
|
|
50514
51635
|
outputRoot,
|
|
@@ -50861,30 +51982,24 @@ const VibeSkillFrontmatterSchema = z.looseObject({
|
|
|
50861
51982
|
"user-invocable": z.optional(z.boolean()),
|
|
50862
51983
|
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
|
|
50863
51984
|
});
|
|
50864
|
-
/** Resolve the top-level `license` field, if present as a string. */
|
|
50865
|
-
function resolveTopLevelLicense(looseTopLevel) {
|
|
50866
|
-
return typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
|
|
50867
|
-
}
|
|
50868
|
-
/** Resolve the top-level `compatibility` field (string or object), if present. */
|
|
50869
|
-
function resolveTopLevelCompatibility(looseTopLevel) {
|
|
50870
|
-
const value = looseTopLevel.compatibility;
|
|
50871
|
-
if (typeof value === "string" || typeof value === "object" && value !== null) return value;
|
|
50872
|
-
}
|
|
50873
|
-
/** Resolve the top-level `metadata` field (object), if present. */
|
|
50874
|
-
function resolveTopLevelMetadata(looseTopLevel) {
|
|
50875
|
-
const value = looseTopLevel.metadata;
|
|
50876
|
-
return typeof value === "object" && value !== null ? value : void 0;
|
|
50877
|
-
}
|
|
50878
51985
|
/**
|
|
50879
51986
|
* Build the Vibe frontmatter from a rulesync skill frontmatter, preferring the
|
|
50880
|
-
* dedicated `vibe` section over
|
|
51987
|
+
* dedicated `vibe` section over the shared root-level fields.
|
|
50881
51988
|
*/
|
|
50882
51989
|
function buildVibeFrontmatter(rulesyncFrontmatter) {
|
|
50883
51990
|
const vibeSection = rulesyncFrontmatter.vibe;
|
|
50884
|
-
const
|
|
50885
|
-
|
|
50886
|
-
|
|
50887
|
-
|
|
51991
|
+
const license = resolveLicense({
|
|
51992
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51993
|
+
section: vibeSection
|
|
51994
|
+
});
|
|
51995
|
+
const compatibility = resolveCompatibility({
|
|
51996
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
51997
|
+
section: vibeSection
|
|
51998
|
+
});
|
|
51999
|
+
const metadata = resolveMetadata({
|
|
52000
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
52001
|
+
section: vibeSection
|
|
52002
|
+
});
|
|
50888
52003
|
const resolvedUserInvocable = resolveUserInvocable({
|
|
50889
52004
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50890
52005
|
section: vibeSection
|
|
@@ -50892,9 +52007,9 @@ function buildVibeFrontmatter(rulesyncFrontmatter) {
|
|
|
50892
52007
|
return {
|
|
50893
52008
|
name: rulesyncFrontmatter.name,
|
|
50894
52009
|
description: rulesyncFrontmatter.description,
|
|
50895
|
-
...
|
|
50896
|
-
...
|
|
50897
|
-
...
|
|
52010
|
+
...license !== void 0 && { license },
|
|
52011
|
+
...compatibility !== void 0 && { compatibility },
|
|
52012
|
+
...metadata !== void 0 && { metadata },
|
|
50898
52013
|
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
50899
52014
|
...vibeSection?.["allowed-tools"] !== void 0 && { "allowed-tools": vibeSection["allowed-tools"] }
|
|
50900
52015
|
};
|
|
@@ -51874,7 +52989,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
51874
52989
|
global: this.global
|
|
51875
52990
|
});
|
|
51876
52991
|
if (dirWriteBlockReason !== void 0 && dirWriteBlockReason !== null) {
|
|
51877
|
-
this.logger.warn(`Skipping skill ${
|
|
52992
|
+
this.logger.warn(`Skipping skill ${quoteForLog(dirName)} for '${this.toolTarget}': ${dirWriteBlockReason}`);
|
|
51878
52993
|
return null;
|
|
51879
52994
|
}
|
|
51880
52995
|
return factory.class.fromRulesyncSkill({
|
|
@@ -52030,7 +53145,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52030
53145
|
};
|
|
52031
53146
|
} catch (error) {
|
|
52032
53147
|
if (!isLenientRoot) throw error;
|
|
52033
|
-
this.logger.warn(`Skipping ${
|
|
53148
|
+
this.logger.warn(`Skipping ${quoteForLog(sourcePath)}: ` + stripControlCharacters(formatError(error)));
|
|
52034
53149
|
return null;
|
|
52035
53150
|
}
|
|
52036
53151
|
}))).filter((loaded) => loaded !== null);
|
|
@@ -52061,7 +53176,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52061
53176
|
};
|
|
52062
53177
|
} catch (error) {
|
|
52063
53178
|
if (!isLenientRoot) throw error;
|
|
52064
|
-
this.logger.warn(`Skipping ${
|
|
53179
|
+
this.logger.warn(`Skipping ${quoteForLog(sourcePath)}: ` + stripControlCharacters(formatError(error)));
|
|
52065
53180
|
return null;
|
|
52066
53181
|
}
|
|
52067
53182
|
}))).filter((loaded) => loaded !== null);
|
|
@@ -52089,7 +53204,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
52089
53204
|
return names.filter((name) => {
|
|
52090
53205
|
if (isAddressableSkillName(kind === "file" ? basename(name, ".md") : name)) return true;
|
|
52091
53206
|
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.";
|
|
52092
|
-
warnOnceWithFallback(this.logger, `Skipping ${
|
|
53207
|
+
warnOnceWithFallback(this.logger, `Skipping ${quoteForLog(join(dirPath, name))}: a ${consequence}`);
|
|
52093
53208
|
return false;
|
|
52094
53209
|
});
|
|
52095
53210
|
}
|
|
@@ -57025,7 +58140,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57025
58140
|
supportsProject: true,
|
|
57026
58141
|
supportsSimulated: false,
|
|
57027
58142
|
supportsGlobal: true,
|
|
57028
|
-
filePattern:
|
|
58143
|
+
filePattern: "*/AGENTS.md"
|
|
57029
58144
|
}
|
|
57030
58145
|
}],
|
|
57031
58146
|
["devin", {
|
|
@@ -57034,7 +58149,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57034
58149
|
supportsProject: true,
|
|
57035
58150
|
supportsSimulated: false,
|
|
57036
58151
|
supportsGlobal: true,
|
|
57037
|
-
filePattern:
|
|
58152
|
+
filePattern: "*/AGENT.md"
|
|
57038
58153
|
}
|
|
57039
58154
|
}],
|
|
57040
58155
|
["factorydroid", {
|
|
@@ -57124,7 +58239,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57124
58239
|
supportsProject: true,
|
|
57125
58240
|
supportsSimulated: false,
|
|
57126
58241
|
supportsGlobal: true,
|
|
57127
|
-
filePattern:
|
|
58242
|
+
filePattern: "**/*.md"
|
|
57128
58243
|
}
|
|
57129
58244
|
}],
|
|
57130
58245
|
["opencode", {
|
|
@@ -57151,7 +58266,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
57151
58266
|
supportsProject: true,
|
|
57152
58267
|
supportsSimulated: false,
|
|
57153
58268
|
supportsGlobal: true,
|
|
57154
|
-
filePattern:
|
|
58269
|
+
filePattern: "*/SKILL.md"
|
|
57155
58270
|
}
|
|
57156
58271
|
}],
|
|
57157
58272
|
["roo", {
|
|
@@ -57388,7 +58503,10 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
57388
58503
|
rootPath: rootOutputRoot,
|
|
57389
58504
|
targetPath: baseDir
|
|
57390
58505
|
});
|
|
57391
|
-
const subagentFilePaths = await findFilesByGlobs(
|
|
58506
|
+
const subagentFilePaths = await findFilesByGlobs(factory.meta.filePattern, {
|
|
58507
|
+
cwd: baseDir,
|
|
58508
|
+
followSymbolicLinks: !forDeletion
|
|
58509
|
+
});
|
|
57392
58510
|
const toRelativeFilePath = (path) => relative(baseDir, path);
|
|
57393
58511
|
let ownedFilePaths = subagentFilePaths;
|
|
57394
58512
|
if (factory.class.isFileOwned) {
|
|
@@ -57611,15 +58729,14 @@ var ToolRule = class extends ToolFile {
|
|
|
57611
58729
|
* than files under a rulesync-owned directory, so enumerating them for
|
|
57612
58730
|
* `--delete` would sweep away work rulesync never wrote.
|
|
57613
58731
|
*/
|
|
57614
|
-
static buildNestedFilePatterns({
|
|
57615
|
-
const root = toPosixPath(outputRoot);
|
|
58732
|
+
static buildNestedFilePatterns({ fileName }) {
|
|
57616
58733
|
return {
|
|
57617
|
-
include: [
|
|
58734
|
+
include: [`**/${fileName}`],
|
|
57618
58735
|
ignore: [
|
|
57619
|
-
|
|
57620
|
-
|
|
57621
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
57622
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
58736
|
+
fileName,
|
|
58737
|
+
"**/.*/**",
|
|
58738
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
58739
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
57623
58740
|
]
|
|
57624
58741
|
};
|
|
57625
58742
|
}
|
|
@@ -57780,11 +58897,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
|
|
|
57780
58897
|
*
|
|
57781
58898
|
* @see https://agents.md/
|
|
57782
58899
|
*/
|
|
57783
|
-
static getNestedFilePatterns(
|
|
57784
|
-
return this.buildNestedFilePatterns({
|
|
57785
|
-
outputRoot,
|
|
57786
|
-
fileName: AGENTSMD_RULE_FILE_NAME
|
|
57787
|
-
});
|
|
58900
|
+
static getNestedFilePatterns() {
|
|
58901
|
+
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
57788
58902
|
}
|
|
57789
58903
|
/**
|
|
57790
58904
|
* The subproject directory this rule scopes, or `undefined` for the project
|
|
@@ -58733,6 +59847,78 @@ var AugmentcodeRule = class AugmentcodeRule extends ToolRule {
|
|
|
58733
59847
|
}
|
|
58734
59848
|
};
|
|
58735
59849
|
//#endregion
|
|
59850
|
+
//#region src/features/rules/claudecode-language-settings.ts
|
|
59851
|
+
/**
|
|
59852
|
+
* Claude Code's native home for the root `language` key of `rulesync.jsonc`.
|
|
59853
|
+
*
|
|
59854
|
+
* Claude Code reads a top-level `language` setting, so for this target the
|
|
59855
|
+
* rules feature writes the preference there instead of appending a prompt
|
|
59856
|
+
* block to CLAUDE.md. The key is patched into the existing settings file
|
|
59857
|
+
* through the shared config gateway (the `rules` feature owns `language`
|
|
59858
|
+
* there and nothing else), so hooks, permissions, and deny lists written by
|
|
59859
|
+
* sibling features — and everything the user authored — are left alone.
|
|
59860
|
+
*
|
|
59861
|
+
* Removing `language` from `rulesync.jsonc` later does not retract the key
|
|
59862
|
+
* already written: the settings file is shared with the user's own
|
|
59863
|
+
* configuration, so nothing is ever deleted from it, and the value stays
|
|
59864
|
+
* until the user removes it by hand.
|
|
59865
|
+
*/
|
|
59866
|
+
var ClaudecodeLanguageSettings = class ClaudecodeLanguageSettings extends ToolFile {
|
|
59867
|
+
/**
|
|
59868
|
+
* Project scope goes to `.claude/settings.local.json`: a response language
|
|
59869
|
+
* is a per-developer preference, and the local file is the one Claude Code
|
|
59870
|
+
* keeps out of version control. Global scope goes to `~/.claude/settings.json`
|
|
59871
|
+
* because Claude Code reads no `~/.claude/settings.local.json`.
|
|
59872
|
+
*/
|
|
59873
|
+
static getSettablePaths({ global = false } = {}) {
|
|
59874
|
+
return {
|
|
59875
|
+
relativeDirPath: CLAUDECODE_DIR,
|
|
59876
|
+
relativeFilePath: global ? CLAUDECODE_SETTINGS_FILE_NAME : CLAUDECODE_SETTINGS_LOCAL_FILE_NAME
|
|
59877
|
+
};
|
|
59878
|
+
}
|
|
59879
|
+
static async fromLanguage({ outputRoot = process.cwd(), language, global = false, validate = true }) {
|
|
59880
|
+
const paths = this.getSettablePaths({ global });
|
|
59881
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
59882
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
59883
|
+
return new ClaudecodeLanguageSettings({
|
|
59884
|
+
outputRoot,
|
|
59885
|
+
relativeDirPath: paths.relativeDirPath,
|
|
59886
|
+
relativeFilePath: paths.relativeFilePath,
|
|
59887
|
+
fileContent: applySharedConfigPatch({
|
|
59888
|
+
fileKey: sharedConfigFileKey(paths),
|
|
59889
|
+
feature: "rules",
|
|
59890
|
+
existingContent,
|
|
59891
|
+
patch: { language: getClaudecodeLanguageValue(language) },
|
|
59892
|
+
filePath
|
|
59893
|
+
}),
|
|
59894
|
+
validate,
|
|
59895
|
+
global
|
|
59896
|
+
});
|
|
59897
|
+
}
|
|
59898
|
+
/**
|
|
59899
|
+
* A settings file the user (and other features) share: never swept as an
|
|
59900
|
+
* orphan, even though the rules feature stops writing it when `language`
|
|
59901
|
+
* is unset again.
|
|
59902
|
+
*/
|
|
59903
|
+
isDeletable() {
|
|
59904
|
+
return false;
|
|
59905
|
+
}
|
|
59906
|
+
validate() {
|
|
59907
|
+
try {
|
|
59908
|
+
JSON.parse(this.fileContent);
|
|
59909
|
+
return {
|
|
59910
|
+
success: true,
|
|
59911
|
+
error: null
|
|
59912
|
+
};
|
|
59913
|
+
} catch (error) {
|
|
59914
|
+
return {
|
|
59915
|
+
success: false,
|
|
59916
|
+
error: new Error(`Invalid JSON in ${this.getRelativePathFromCwd()}: ${formatError(error)}`, { cause: error })
|
|
59917
|
+
};
|
|
59918
|
+
}
|
|
59919
|
+
}
|
|
59920
|
+
};
|
|
59921
|
+
//#endregion
|
|
58736
59922
|
//#region src/features/rules/claudecode-legacy-rule.ts
|
|
58737
59923
|
/**
|
|
58738
59924
|
* Legacy rule generator for Claude Code AI assistant
|
|
@@ -58868,6 +60054,15 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
|
|
|
58868
60054
|
nonRoot: { relativeDirPath: buildToolPath(CLAUDECODE_DIR, CLAUDECODE_RULES_DIR_NAME, excludeToolDir) }
|
|
58869
60055
|
};
|
|
58870
60056
|
}
|
|
60057
|
+
/**
|
|
60058
|
+
* The settings file the rules feature patches `language` into (see
|
|
60059
|
+
* {@link ClaudecodeLanguageSettings}): not a rule path, so `getSettablePaths`
|
|
60060
|
+
* does not report it, but the shared-config gateway must know the rules
|
|
60061
|
+
* feature writes there.
|
|
60062
|
+
*/
|
|
60063
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
60064
|
+
return [ClaudecodeLanguageSettings.getSettablePaths({ global })];
|
|
60065
|
+
}
|
|
58871
60066
|
constructor({ frontmatter, body, ...rest }) {
|
|
58872
60067
|
if (rest.validate) {
|
|
58873
60068
|
const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
|
|
@@ -59573,6 +60768,12 @@ const CursorRuleFrontmatterSchema = z.object({
|
|
|
59573
60768
|
globs: z.optional(z.string()),
|
|
59574
60769
|
alwaysApply: z.optional(z.boolean())
|
|
59575
60770
|
});
|
|
60771
|
+
/**
|
|
60772
|
+
* Globs that match the whole project, and so say nothing `alwaysApply: true`
|
|
60773
|
+
* does not already say. Same pair the Cline and Qwen Code adapters treat as
|
|
60774
|
+
* universal.
|
|
60775
|
+
*/
|
|
60776
|
+
const UNIVERSAL_GLOBS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
59576
60777
|
var CursorRule = class CursorRule extends ToolRule {
|
|
59577
60778
|
frontmatter;
|
|
59578
60779
|
body;
|
|
@@ -59601,7 +60802,10 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59601
60802
|
const rawDescription = frontmatter.description;
|
|
59602
60803
|
const description = typeof rawDescription === "string" ? rawDescription.replace(/\n+/g, " ").trim() : rawDescription;
|
|
59603
60804
|
if (description) lines.push(dump({ description }, { lineWidth: -1 }).trimEnd());
|
|
59604
|
-
if (frontmatter.globs !== void 0)
|
|
60805
|
+
if (frontmatter.globs !== void 0) {
|
|
60806
|
+
const globs = String(frontmatter.globs).replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim();
|
|
60807
|
+
lines.push(`globs: ${globs}`);
|
|
60808
|
+
}
|
|
59605
60809
|
lines.push("---");
|
|
59606
60810
|
lines.push("");
|
|
59607
60811
|
if (body) lines.push(body);
|
|
@@ -59619,11 +60823,12 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59619
60823
|
toRulesyncRule() {
|
|
59620
60824
|
const targets = ["*"];
|
|
59621
60825
|
const isAlways = this.frontmatter.alwaysApply === true;
|
|
59622
|
-
const
|
|
59623
|
-
|
|
59624
|
-
|
|
59625
|
-
|
|
59626
|
-
|
|
60826
|
+
const rawGlobs = this.frontmatter.globs;
|
|
60827
|
+
const sourceGlobs = rawGlobs && rawGlobs.trim() !== "" ? rawGlobs.split(",").map((g) => g.trim()).filter((g) => g.length > 0) : [];
|
|
60828
|
+
const globs = sourceGlobs.length === 0 && isAlways ? ["**/*"] : sourceGlobs;
|
|
60829
|
+
let cursorGlobs;
|
|
60830
|
+
if (sourceGlobs.length > 0) cursorGlobs = sourceGlobs;
|
|
60831
|
+
else if (isAlways) cursorGlobs = [];
|
|
59627
60832
|
return new RulesyncRule({
|
|
59628
60833
|
frontmatter: {
|
|
59629
60834
|
targets,
|
|
@@ -59633,7 +60838,7 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59633
60838
|
cursor: {
|
|
59634
60839
|
alwaysApply: this.frontmatter.alwaysApply,
|
|
59635
60840
|
description: this.frontmatter.description,
|
|
59636
|
-
globs:
|
|
60841
|
+
globs: cursorGlobs
|
|
59637
60842
|
}
|
|
59638
60843
|
},
|
|
59639
60844
|
body: this.body,
|
|
@@ -59643,21 +60848,44 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
59643
60848
|
});
|
|
59644
60849
|
}
|
|
59645
60850
|
/**
|
|
59646
|
-
* Resolve
|
|
59647
|
-
*
|
|
59648
|
-
*
|
|
59649
|
-
*
|
|
60851
|
+
* Resolve the Cursor `globs` string with priority: cursor-specific > parent.
|
|
60852
|
+
*
|
|
60853
|
+
* The two ways `cursorSpecificGlobs` can be absent are deliberately different
|
|
60854
|
+
* and must stay that way: `undefined` means "no Cursor-specific opinion", so
|
|
60855
|
+
* the canonical globs are used, while an explicit `[]` means "this rule has
|
|
60856
|
+
* no Cursor globs" and wins over them. Collapsing the two -- say, to
|
|
60857
|
+
* `cursorSpecificGlobs?.length ? … : parentGlobs` -- would put a universal
|
|
60858
|
+
* canonical glob back onto every Always Apply rule.
|
|
60859
|
+
*
|
|
60860
|
+
* A universal glob (see {@link UNIVERSAL_GLOBS}) is dropped outright when the
|
|
60861
|
+
* rule is Always Apply. `alwaysApply: true` already applies the rule
|
|
60862
|
+
* everywhere, Cursor's docs say globs are ignored once it is set, and
|
|
60863
|
+
* Cursor's own staff describe the two together as a semantic conflict that
|
|
60864
|
+
* some versions resolve by classifying the rule as a glob rule instead of an
|
|
60865
|
+
* Always one. Dropping it here is what heals, on the next generate, both a
|
|
60866
|
+
* `.rulesync` file an older version wrote with the invented universal glob in
|
|
60867
|
+
* `cursor.globs` and a rule hand-written with a universal canonical glob
|
|
60868
|
+
* beside `cursor.alwaysApply: true`. Specific globs are left alone: they say
|
|
60869
|
+
* something a flag cannot, so they are the author's to keep even alongside
|
|
60870
|
+
* it.
|
|
59650
60871
|
*/
|
|
59651
|
-
static resolveCursorGlobs(cursorSpecificGlobs, parentGlobs) {
|
|
60872
|
+
static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
|
|
59652
60873
|
const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
|
|
59653
|
-
|
|
60874
|
+
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
60875
|
+
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS.has(glob.trim()))) return;
|
|
60876
|
+
return targetGlobs.join(",");
|
|
59654
60877
|
}
|
|
59655
60878
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
59656
60879
|
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
60880
|
+
const alwaysApply = rulesyncFrontmatter.cursor?.alwaysApply;
|
|
59657
60881
|
const cursorFrontmatter = {
|
|
59658
60882
|
description: rulesyncFrontmatter.description,
|
|
59659
|
-
globs: this.resolveCursorGlobs(
|
|
59660
|
-
|
|
60883
|
+
globs: this.resolveCursorGlobs({
|
|
60884
|
+
cursorSpecificGlobs: rulesyncFrontmatter.cursor?.globs,
|
|
60885
|
+
parentGlobs: rulesyncFrontmatter.globs,
|
|
60886
|
+
alwaysApply: alwaysApply === true
|
|
60887
|
+
}),
|
|
60888
|
+
alwaysApply
|
|
59661
60889
|
};
|
|
59662
60890
|
const body = rulesyncRule.getBody();
|
|
59663
60891
|
const newFileName = `${rulesyncRule.getRelativeFilePath().replace(/\.md$/, "")}.mdc`;
|
|
@@ -60982,15 +62210,14 @@ var KiroRule = class KiroRule extends ToolRule {
|
|
|
60982
62210
|
* never wrote.
|
|
60983
62211
|
* @see https://kiro.dev/docs/steering/
|
|
60984
62212
|
*/
|
|
60985
|
-
static getNestedFilePatterns(
|
|
60986
|
-
const root = toPosixPath(outputRoot);
|
|
62213
|
+
static getNestedFilePatterns() {
|
|
60987
62214
|
return {
|
|
60988
|
-
include: [
|
|
62215
|
+
include: [`**/${KIRO_NESTED_STEERING_FILE_NAME}`],
|
|
60989
62216
|
ignore: [
|
|
60990
|
-
|
|
60991
|
-
|
|
60992
|
-
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) =>
|
|
60993
|
-
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${
|
|
62217
|
+
KIRO_NESTED_STEERING_FILE_NAME,
|
|
62218
|
+
"**/.*/**",
|
|
62219
|
+
...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
|
|
62220
|
+
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
|
|
60994
62221
|
]
|
|
60995
62222
|
};
|
|
60996
62223
|
}
|
|
@@ -61620,8 +62847,8 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
|
|
|
61620
62847
|
* The personal local context file lives under `.qwen/`, not at the project
|
|
61621
62848
|
* root where the settable root path points, so override the deletion glob.
|
|
61622
62849
|
*/
|
|
61623
|
-
static getLocalRootFileGlob({
|
|
61624
|
-
return join(
|
|
62850
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
62851
|
+
return posix.join(toPosixPath(QWENCODE_DIR), fileName);
|
|
61625
62852
|
}
|
|
61626
62853
|
};
|
|
61627
62854
|
//#endregion
|
|
@@ -61648,11 +62875,8 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
|
|
|
61648
62875
|
* (same exclusions, import-only, project scope).
|
|
61649
62876
|
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/v1.18.0/docs/SESSION_MEMORY_RETRIEVAL.md
|
|
61650
62877
|
*/
|
|
61651
|
-
static getNestedFilePatterns(
|
|
61652
|
-
return this.buildNestedFilePatterns({
|
|
61653
|
-
outputRoot,
|
|
61654
|
-
fileName: REASONIX_RULE_FILE_NAME
|
|
61655
|
-
});
|
|
62878
|
+
static getNestedFilePatterns() {
|
|
62879
|
+
return this.buildNestedFilePatterns({ fileName: REASONIX_RULE_FILE_NAME });
|
|
61656
62880
|
}
|
|
61657
62881
|
/**
|
|
61658
62882
|
* The subproject directory this rule scopes, or `undefined` for the root
|
|
@@ -61909,9 +63133,9 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
61909
63133
|
* generic directory is the only one the deletion sweep enumerates, because a
|
|
61910
63134
|
* `rules-*` glob would also match mode rules a user wrote by hand.
|
|
61911
63135
|
*/
|
|
61912
|
-
static getNestedFilePatterns(
|
|
63136
|
+
static getNestedFilePatterns() {
|
|
61913
63137
|
return {
|
|
61914
|
-
include: [`${toPosixPath(
|
|
63138
|
+
include: [`${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
|
|
61915
63139
|
ignore: []
|
|
61916
63140
|
};
|
|
61917
63141
|
}
|
|
@@ -61949,8 +63173,8 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
61949
63173
|
* Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
|
|
61950
63174
|
* at the project root, not under `.roo/` (mirrors rovodev).
|
|
61951
63175
|
*/
|
|
61952
|
-
static getLocalRootFileGlob({
|
|
61953
|
-
return
|
|
63176
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
63177
|
+
return fileName;
|
|
61954
63178
|
}
|
|
61955
63179
|
};
|
|
61956
63180
|
//#endregion
|
|
@@ -62130,15 +63354,15 @@ var RovodevRule = class RovodevRule extends ToolRule {
|
|
|
62130
63354
|
root: true
|
|
62131
63355
|
})];
|
|
62132
63356
|
},
|
|
62133
|
-
getMirrorDeletionGlobs: (
|
|
62134
|
-
primaryGlob: join(
|
|
62135
|
-
mirrorGlob:
|
|
63357
|
+
getMirrorDeletionGlobs: () => ({
|
|
63358
|
+
primaryGlob: posix.join(toPosixPath(ROVODEV_DIR), ROVODEV_RULE_FILE_NAME),
|
|
63359
|
+
mirrorGlob: ROVODEV_RULE_FILE_NAME
|
|
62136
63360
|
})
|
|
62137
63361
|
};
|
|
62138
63362
|
}
|
|
62139
63363
|
/** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
|
|
62140
|
-
static getLocalRootFileGlob({
|
|
62141
|
-
return
|
|
63364
|
+
static getLocalRootFileGlob({ fileName }) {
|
|
63365
|
+
return fileName;
|
|
62142
63366
|
}
|
|
62143
63367
|
};
|
|
62144
63368
|
//#endregion
|
|
@@ -62278,11 +63502,8 @@ var VibeRule = class VibeRule extends ToolRule {
|
|
|
62278
63502
|
* literally the same files.
|
|
62279
63503
|
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
62280
63504
|
*/
|
|
62281
|
-
static getNestedFilePatterns(
|
|
62282
|
-
return this.buildNestedFilePatterns({
|
|
62283
|
-
outputRoot,
|
|
62284
|
-
fileName: AGENTSMD_RULE_FILE_NAME
|
|
62285
|
-
});
|
|
63505
|
+
static getNestedFilePatterns() {
|
|
63506
|
+
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
62286
63507
|
}
|
|
62287
63508
|
/**
|
|
62288
63509
|
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
@@ -63033,21 +64254,34 @@ const MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS = 10;
|
|
|
63033
64254
|
* the tool will read" — can keep the two apart. A legacy root is a file
|
|
63034
64255
|
* Rulesync reads but never writes, and the difference matters to them.
|
|
63035
64256
|
*/
|
|
63036
|
-
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob) => {
|
|
64257
|
+
const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAltGlob, outputRoot) => {
|
|
63037
64258
|
if (primaryFilePaths.length > 0) return primaryFilePaths;
|
|
63038
|
-
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob));
|
|
64259
|
+
if (alternativeRoots) return await findFilesByGlobs(alternativeRoots.map(buildAltGlob), { cwd: outputRoot });
|
|
63039
64260
|
return [];
|
|
63040
64261
|
};
|
|
64262
|
+
/**
|
|
64263
|
+
* A project-root-relative glob for a file a tool keeps at a fixed path, joined
|
|
64264
|
+
* with `/` because a glob is always posix-separated.
|
|
64265
|
+
*
|
|
64266
|
+
* Relative because the root goes to `findFilesByGlobs` as `cwd` rather than into
|
|
64267
|
+
* the pattern: a project directory named `project(a)` or `project{a,b}` would
|
|
64268
|
+
* otherwise be read as a glob and match nothing at all — and on a `--delete`
|
|
64269
|
+
* sweep an empty result reads as "every source was removed", so rulesync would
|
|
64270
|
+
* delete generated files it can no longer regenerate and report success.
|
|
64271
|
+
*/
|
|
64272
|
+
const rootRelativeGlob = (...segments) => posix.join(...segments.filter((segment) => segment !== void 0).map(toPosixPath));
|
|
63041
64273
|
var RulesProcessor = class extends FeatureProcessor {
|
|
63042
64274
|
toolTarget;
|
|
63043
64275
|
simulateCommands;
|
|
63044
64276
|
simulateSubagents;
|
|
63045
64277
|
simulateSkills;
|
|
64278
|
+
language;
|
|
64279
|
+
deriveSubprojectPathFromGlobs;
|
|
63046
64280
|
global;
|
|
63047
64281
|
getFactory;
|
|
63048
64282
|
skills;
|
|
63049
64283
|
featureOptions;
|
|
63050
|
-
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
64284
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, language, deriveSubprojectPathFromGlobs = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, dryRun = false, logger }) {
|
|
63051
64285
|
super({
|
|
63052
64286
|
outputRoot,
|
|
63053
64287
|
inputRoots,
|
|
@@ -63061,6 +64295,8 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63061
64295
|
this.simulateCommands = simulateCommands;
|
|
63062
64296
|
this.simulateSubagents = simulateSubagents;
|
|
63063
64297
|
this.simulateSkills = simulateSkills;
|
|
64298
|
+
this.language = language;
|
|
64299
|
+
this.deriveSubprojectPathFromGlobs = deriveSubprojectPathFromGlobs;
|
|
63064
64300
|
this.getFactory = getFactory;
|
|
63065
64301
|
this.skills = skills;
|
|
63066
64302
|
this.featureOptions = featureOptions;
|
|
@@ -63104,8 +64340,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63104
64340
|
});
|
|
63105
64341
|
this.applyRootRuleSections({
|
|
63106
64342
|
toolRules,
|
|
63107
|
-
factory
|
|
64343
|
+
factory,
|
|
64344
|
+
convertedRules
|
|
63108
64345
|
});
|
|
64346
|
+
extraFiles.push(...await this.buildLanguageSettingsFiles());
|
|
63109
64347
|
const outputFiles = [...toolRules, ...extraFiles];
|
|
63110
64348
|
this.warnForOutputPathCollisions({
|
|
63111
64349
|
outputFiles,
|
|
@@ -63163,7 +64401,8 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63163
64401
|
outputRoot: this.outputRoot,
|
|
63164
64402
|
instructions: instructionPaths,
|
|
63165
64403
|
validate: true,
|
|
63166
|
-
global: this.global
|
|
64404
|
+
global: this.global,
|
|
64405
|
+
logger: this.logger
|
|
63167
64406
|
});
|
|
63168
64407
|
return registered ? [registered] : [];
|
|
63169
64408
|
}
|
|
@@ -63172,11 +64411,19 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63172
64411
|
* reference and conventions sections to the root rule content. Mutates the
|
|
63173
64412
|
* root rule in place.
|
|
63174
64413
|
*/
|
|
63175
|
-
applyRootRuleSections({ toolRules, factory }) {
|
|
64414
|
+
applyRootRuleSections({ toolRules, factory, convertedRules }) {
|
|
63176
64415
|
const { meta } = factory;
|
|
63177
64416
|
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
63178
|
-
if (!rootRule)
|
|
63179
|
-
|
|
64417
|
+
if (!rootRule) {
|
|
64418
|
+
this.appendLanguageBlockToRootSourceRules({ convertedRules });
|
|
64419
|
+
return;
|
|
64420
|
+
}
|
|
64421
|
+
const assembledContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
|
|
64422
|
+
const promptLanguage = this.getPromptBlockLanguage();
|
|
64423
|
+
const newContent = promptLanguage === void 0 ? assembledContent : appendLanguageBlock({
|
|
64424
|
+
content: assembledContent,
|
|
64425
|
+
language: promptLanguage
|
|
64426
|
+
});
|
|
63180
64427
|
rootRule.setFileContent(newContent);
|
|
63181
64428
|
const rootMirror = factory.class.getRootMirror?.();
|
|
63182
64429
|
if (rootMirror && !this.global) toolRules.push(...rootMirror.getMirrorFiles({
|
|
@@ -63185,6 +64432,53 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
63185
64432
|
content: newContent
|
|
63186
64433
|
}));
|
|
63187
64434
|
}
|
|
64435
|
+
/**
|
|
64436
|
+
* The language delivered as a prompt block, or `undefined` when none is:
|
|
64437
|
+
* `language` is unset, or the target is Claude Code, which has a native
|
|
64438
|
+
* `language` setting (see {@link ClaudecodeLanguageSettings}) and so gets
|
|
64439
|
+
* no block in its root file.
|
|
64440
|
+
*/
|
|
64441
|
+
getPromptBlockLanguage() {
|
|
64442
|
+
return this.isClaudecodeTarget() ? void 0 : this.language;
|
|
64443
|
+
}
|
|
64444
|
+
isClaudecodeTarget() {
|
|
64445
|
+
return this.toolTarget === "claudecode" || this.toolTarget === "claudecode-legacy";
|
|
64446
|
+
}
|
|
64447
|
+
/**
|
|
64448
|
+
* The language block for targets whose adapters never mark a ToolRule as
|
|
64449
|
+
* root: Cursor emits every rule as `.cursor/rules/*.mdc`, and the fixed-name
|
|
64450
|
+
* targets (Cline, Roo, Kiro, ...) file the `root: true` source beside the
|
|
64451
|
+
* others. The file produced from the `root: true` source is still the root
|
|
64452
|
+
* rule from the user's point of view, so it is the one that gets the block.
|
|
64453
|
+
* Nested rules never do. When several sources are marked `root: true`, only
|
|
64454
|
+
* the file built from the first one (in conversion order, which follows the
|
|
64455
|
+
* source order) carries the block: one instruction per target is the
|
|
64456
|
+
* contract, and a root-marking target gets exactly one as well.
|
|
64457
|
+
*/
|
|
64458
|
+
appendLanguageBlockToRootSourceRules({ convertedRules }) {
|
|
64459
|
+
const language = this.getPromptBlockLanguage();
|
|
64460
|
+
if (language === void 0) return;
|
|
64461
|
+
const firstRootSource = convertedRules.find(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
|
|
64462
|
+
if (firstRootSource === void 0) return;
|
|
64463
|
+
const { toolRule } = firstRootSource;
|
|
64464
|
+
toolRule.setFileContent(appendLanguageBlock({
|
|
64465
|
+
content: toolRule.getFileContent(),
|
|
64466
|
+
language
|
|
64467
|
+
}));
|
|
64468
|
+
}
|
|
64469
|
+
/**
|
|
64470
|
+
* Claude Code's native delivery of `language`: a patch to the settings file
|
|
64471
|
+
* instead of a prompt block. Empty for every other target and when
|
|
64472
|
+
* `language` is unset, so an unset key never touches the settings file.
|
|
64473
|
+
*/
|
|
64474
|
+
async buildLanguageSettingsFiles() {
|
|
64475
|
+
if (this.language === void 0 || !this.isClaudecodeTarget()) return [];
|
|
64476
|
+
return [await ClaudecodeLanguageSettings.fromLanguage({
|
|
64477
|
+
outputRoot: this.outputRoot,
|
|
64478
|
+
language: this.language,
|
|
64479
|
+
global: this.global
|
|
64480
|
+
})];
|
|
64481
|
+
}
|
|
63188
64482
|
buildSkillList(skillClass) {
|
|
63189
64483
|
if (!this.skills) return [];
|
|
63190
64484
|
const toolRelativeDirPath = skillClass.getSettablePaths({ global: this.global }).relativeDirPath;
|
|
@@ -63414,7 +64708,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63414
64708
|
const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
|
|
63415
64709
|
const rulesyncRules = toolRules.map((toolRule) => {
|
|
63416
64710
|
if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
|
|
63417
|
-
return
|
|
64711
|
+
return this.withoutLanguageBlock({
|
|
64712
|
+
toolRule,
|
|
64713
|
+
rulesyncRule: toolRule.toRulesyncRule()
|
|
64714
|
+
});
|
|
63418
64715
|
});
|
|
63419
64716
|
const claimedBy = /* @__PURE__ */ new Map();
|
|
63420
64717
|
for (const [index, rulesyncRule] of rulesyncRules.entries()) {
|
|
@@ -63430,6 +64727,33 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63430
64727
|
return rulesyncRules;
|
|
63431
64728
|
}
|
|
63432
64729
|
/**
|
|
64730
|
+
* Drop the language block a previous `generate` appended, so that importing
|
|
64731
|
+
* a generated root file and generating again yields one block, not two.
|
|
64732
|
+
* Every imported rule is checked, not just root ones: Cursor and the
|
|
64733
|
+
* fixed-name targets import their root file as a non-root rule. The
|
|
64734
|
+
* `language` key itself lives in `rulesync.jsonc`, so nothing about the
|
|
64735
|
+
* detected language is carried into the rulesync rule — which is why the
|
|
64736
|
+
* strip is reported: a user who imports a file carrying the block and has
|
|
64737
|
+
* not set `language` would otherwise lose the instruction without a trace.
|
|
64738
|
+
* Once per file per run, since an import over several targets reads the
|
|
64739
|
+
* same root file for each of them.
|
|
64740
|
+
*/
|
|
64741
|
+
withoutLanguageBlock({ toolRule, rulesyncRule }) {
|
|
64742
|
+
const body = rulesyncRule.getBody();
|
|
64743
|
+
const stripped = stripLanguageBlock(body);
|
|
64744
|
+
if (stripped === body) return rulesyncRule;
|
|
64745
|
+
const source = stripControlCharacters(join(toolRule.getRelativeDirPath(), toolRule.getRelativeFilePath()));
|
|
64746
|
+
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.`);
|
|
64747
|
+
return new RulesyncRule({
|
|
64748
|
+
outputRoot: rulesyncRule.getOutputRoot(),
|
|
64749
|
+
relativeDirPath: rulesyncRule.getRelativeDirPath(),
|
|
64750
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
64751
|
+
frontmatter: rulesyncRule.getFrontmatter(),
|
|
64752
|
+
body: stripped,
|
|
64753
|
+
validate: false
|
|
64754
|
+
});
|
|
64755
|
+
}
|
|
64756
|
+
/**
|
|
63433
64757
|
* Load rulesync rule files from a single source-tree's `rules/` (and
|
|
63434
64758
|
* `rules/.curated/`) subtree. `sourceTree` is the source tree itself
|
|
63435
64759
|
* (e.g. `/repo/.rulesync` or `/repo/.rulesync.local`), NOT its parent.
|
|
@@ -63446,7 +64770,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63446
64770
|
const rulesyncOutputRoot = join(sourceTree, RULES_FEATURE_SUBDIR);
|
|
63447
64771
|
const curatedOutputRoot = join(sourceTree, CURATED_RULES_FEATURE_SUBDIR);
|
|
63448
64772
|
const [rulesDirExists, curatedDirExists] = await Promise.all([directoryExistsStrict(rulesyncOutputRoot), directoryExistsStrict(curatedOutputRoot)]);
|
|
63449
|
-
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([rulesDirExists ? findFilesByGlobs(
|
|
64773
|
+
const [discoveredFiles, discoveredCuratedFiles] = await Promise.all([rulesDirExists ? findFilesByGlobs("**/*.md", { cwd: rulesyncOutputRoot }) : [], curatedDirExists ? findFilesByGlobs("**/*.md", { cwd: curatedOutputRoot }) : []]);
|
|
63450
64774
|
const files = [.../* @__PURE__ */ new Set([...discoveredFiles, ...discoveredCuratedFiles])];
|
|
63451
64775
|
const localFiles = files.filter((file) => !relative(rulesyncOutputRoot, file).startsWith(`.curated${sep}`));
|
|
63452
64776
|
const localRelativePathsByIdentity = groupSpellingsByCaseFoldedIdentity(localFiles.map((file) => relative(rulesyncOutputRoot, file)));
|
|
@@ -63483,7 +64807,8 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63483
64807
|
const rule = await RulesyncRule.fromFile({
|
|
63484
64808
|
outputRoot: treeParent,
|
|
63485
64809
|
relativeDirPath: treeRulesDirPath,
|
|
63486
|
-
relativeFilePath: sourceRelativeFilePath
|
|
64810
|
+
relativeFilePath: sourceRelativeFilePath,
|
|
64811
|
+
deriveSubprojectPathFromGlobs: this.deriveSubprojectPathFromGlobs
|
|
63487
64812
|
});
|
|
63488
64813
|
if (sourceRelativeFilePath === relativeFilePath) return rule;
|
|
63489
64814
|
return new RulesyncRule({
|
|
@@ -63632,10 +64957,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63632
64957
|
* Resolved once, up front, so the two blocks that need it do not depend
|
|
63633
64958
|
* on each other's evaluation order.
|
|
63634
64959
|
*/
|
|
63635
|
-
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs(
|
|
64960
|
+
const primaryRootFilePaths = settablePaths.root ? await findFilesByGlobs(rootRelativeGlob(settablePaths.root.relativeDirPath ?? ".", settablePaths.root.relativeFilePath), { cwd: this.outputRoot }) : [];
|
|
63636
64961
|
const rootToolRules = await (async () => {
|
|
63637
64962
|
if (!settablePaths.root) return [];
|
|
63638
|
-
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) =>
|
|
64963
|
+
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => rootRelativeGlob(alt.relativeDirPath, alt.relativeFilePath), this.outputRoot);
|
|
63639
64964
|
if (forDeletion) return buildDeletionRulesFromPaths(uniqueRootFilePaths);
|
|
63640
64965
|
return await buildImportRulesFromPaths(uniqueRootFilePaths);
|
|
63641
64966
|
})();
|
|
@@ -63644,12 +64969,9 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63644
64969
|
if (this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
|
|
63645
64970
|
const fileName = factory.meta.localRootFileName;
|
|
63646
64971
|
const filePaths = await (async () => {
|
|
63647
|
-
if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
|
|
63648
|
-
outputRoot: this.outputRoot,
|
|
63649
|
-
fileName
|
|
63650
|
-
}));
|
|
64972
|
+
if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({ fileName }), { cwd: this.outputRoot });
|
|
63651
64973
|
if (!settablePaths.root) return [];
|
|
63652
|
-
return await findFilesWithFallback(await findFilesByGlobs(
|
|
64974
|
+
return await findFilesWithFallback(await findFilesByGlobs(rootRelativeGlob(settablePaths.root.relativeDirPath ?? ".", fileName), { cwd: this.outputRoot }), settablePaths.alternativeRoots, (alt) => rootRelativeGlob(alt.relativeDirPath, fileName), this.outputRoot);
|
|
63653
64975
|
})();
|
|
63654
64976
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
63655
64977
|
return (await Promise.all(filePaths.map(async (filePath) => {
|
|
@@ -63672,15 +64994,15 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63672
64994
|
const rootMirrorDeletionRules = await (async () => {
|
|
63673
64995
|
const rootMirror = factory.class.getRootMirror?.();
|
|
63674
64996
|
if (!forDeletion || this.global || !rootMirror) return [];
|
|
63675
|
-
const { primaryGlob, mirrorGlob } = rootMirror.getMirrorDeletionGlobs(
|
|
63676
|
-
if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
|
|
63677
|
-
const mirrorPaths = await findFilesByGlobs(mirrorGlob);
|
|
64997
|
+
const { primaryGlob, mirrorGlob } = rootMirror.getMirrorDeletionGlobs();
|
|
64998
|
+
if ((await findFilesByGlobs(primaryGlob, { cwd: this.outputRoot })).length === 0) return [];
|
|
64999
|
+
const mirrorPaths = await findFilesByGlobs(mirrorGlob, { cwd: this.outputRoot });
|
|
63678
65000
|
return buildDeletionRulesFromPaths(mirrorPaths);
|
|
63679
65001
|
})();
|
|
63680
65002
|
const extraFixedToolRules = await (async () => {
|
|
63681
65003
|
const extraFiles = factory.class.getExtraFixedFiles?.({ global: this.global });
|
|
63682
65004
|
if (!extraFiles || extraFiles.length === 0) return [];
|
|
63683
|
-
const filePaths = await findFilesByGlobs(extraFiles.map((file) =>
|
|
65005
|
+
const filePaths = await findFilesByGlobs(extraFiles.map((file) => rootRelativeGlob(file.relativeDirPath, file.relativeFilePath)), { cwd: this.outputRoot });
|
|
63684
65006
|
if (filePaths.length === 0) return [];
|
|
63685
65007
|
if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
|
|
63686
65008
|
return await Promise.all(filePaths.map((filePath) => {
|
|
@@ -63699,9 +65021,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63699
65021
|
})();
|
|
63700
65022
|
this.logger.debug(`Found ${extraFixedToolRules.length} extra fixed tool rule files`);
|
|
63701
65023
|
const nestedToolRules = await (async () => {
|
|
63702
|
-
const patterns = this.global ? void 0 : factory.class.getNestedFilePatterns?.(
|
|
65024
|
+
const patterns = this.global ? void 0 : factory.class.getNestedFilePatterns?.();
|
|
63703
65025
|
if (forDeletion || !patterns || patterns.include.length === 0) return [];
|
|
63704
65026
|
const matchedPaths = await findFilesByGlobs(patterns.include, {
|
|
65027
|
+
cwd: this.outputRoot,
|
|
63705
65028
|
type: "file",
|
|
63706
65029
|
followSymbolicLinks: false,
|
|
63707
65030
|
ignore: patterns.ignore
|
|
@@ -63732,7 +65055,10 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63732
65055
|
const scannedPaths = [];
|
|
63733
65056
|
const skippedPaths = [];
|
|
63734
65057
|
for (const importOnlyRoot of importOnlyRoots) {
|
|
63735
|
-
const matchedPaths = await findFilesByGlobs(
|
|
65058
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
65059
|
+
cwd: this.outputRoot,
|
|
65060
|
+
type: "file"
|
|
65061
|
+
});
|
|
63736
65062
|
if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
|
|
63737
65063
|
skippedPaths.push(...matchedPaths);
|
|
63738
65064
|
continue;
|
|
@@ -63751,7 +65077,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
63751
65077
|
const nonRootToolRules = await (async () => {
|
|
63752
65078
|
if (!settablePaths.nonRoot) return [];
|
|
63753
65079
|
const nonRootOutputRoot = join(this.outputRoot, settablePaths.nonRoot.relativeDirPath);
|
|
63754
|
-
const nonRootFilePaths = await findFilesByGlobs(
|
|
65080
|
+
const nonRootFilePaths = await findFilesByGlobs(`**/*.${factory.meta.extension}`, { cwd: nonRootOutputRoot });
|
|
63755
65081
|
if (forDeletion) return buildDeletionRulesFromPaths(nonRootFilePaths, {
|
|
63756
65082
|
outputRootOverride: nonRootOutputRoot,
|
|
63757
65083
|
relativeDirPathOverride: settablePaths.nonRoot.relativeDirPath
|
|
@@ -63894,7 +65220,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
|
|
|
63894
65220
|
* `.rulesync/` files to disk. Rulesync file instances live in memory only.
|
|
63895
65221
|
*/
|
|
63896
65222
|
async function convertFromTool(params) {
|
|
63897
|
-
|
|
65223
|
+
resetRunWarningState();
|
|
63898
65224
|
const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
|
|
63899
65225
|
if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
|
|
63900
65226
|
const ctx = params;
|
|
@@ -63983,7 +65309,8 @@ function buildRulesStrategy(ctx) {
|
|
|
63983
65309
|
toolTarget,
|
|
63984
65310
|
global,
|
|
63985
65311
|
dryRun,
|
|
63986
|
-
logger
|
|
65312
|
+
logger,
|
|
65313
|
+
language: config.getLanguage()
|
|
63987
65314
|
}),
|
|
63988
65315
|
loadSource: (p) => p.loadToolFiles(),
|
|
63989
65316
|
toRulesync: (p, files) => p.convertToolFilesToRulesyncFiles(files),
|
|
@@ -64373,6 +65700,9 @@ function createOrphanSweepPlan() {
|
|
|
64373
65700
|
const resolved = resolve(path);
|
|
64374
65701
|
return generatedPaths.has(resolved) || isInsideGeneratedTree(resolved);
|
|
64375
65702
|
},
|
|
65703
|
+
isGeneratedExactly({ path }) {
|
|
65704
|
+
return generatedPaths.has(resolve(path));
|
|
65705
|
+
},
|
|
64376
65706
|
rejectClaimed({ items, getPath }) {
|
|
64377
65707
|
return items.filter((item) => !plan.isGenerated({ path: getPath(item) }));
|
|
64378
65708
|
},
|
|
@@ -64606,13 +65936,18 @@ async function processDirFeatureGeneration(params) {
|
|
|
64606
65936
|
getPath: (d) => d.getDirPath()
|
|
64607
65937
|
}), toolDirs);
|
|
64608
65938
|
const existingFlatFiles = await processor.loadToolFlatFilesToDelete();
|
|
64609
|
-
|
|
65939
|
+
const orphanFileCount = await processor.removeOrphanFlatFiles({
|
|
64610
65940
|
existingFlatFiles: sweepPlan.rejectClaimed({
|
|
64611
65941
|
items: existingFlatFiles,
|
|
64612
65942
|
getPath: (d) => d.getFlatFilePath() ?? d.getDirPath()
|
|
64613
65943
|
}),
|
|
64614
65944
|
generatedDirs: toolDirs
|
|
64615
|
-
})
|
|
65945
|
+
});
|
|
65946
|
+
const orphanInDirCount = await processor.removeOrphanFilesInAiDirs({
|
|
65947
|
+
generatedDirs: toolDirs,
|
|
65948
|
+
isClaimed: (path) => sweepPlan.isGeneratedExactly({ path })
|
|
65949
|
+
});
|
|
65950
|
+
return orphanDirCount + orphanFileCount + orphanInDirCount > 0;
|
|
64616
65951
|
} });
|
|
64617
65952
|
return {
|
|
64618
65953
|
count: totalCount,
|
|
@@ -64931,7 +66266,7 @@ async function collectHermesProjectPluginNames({ config, resultsById }) {
|
|
|
64931
66266
|
async function generate(params) {
|
|
64932
66267
|
const { config, logger } = params;
|
|
64933
66268
|
resetRootShadowingWarnings({ logger });
|
|
64934
|
-
|
|
66269
|
+
resetRunWarningState();
|
|
64935
66270
|
for (const toolTarget of config.getTargets()) for (const outputRoot of config.getOutputRoots(toolTarget)) await assertPluginRootSafe({
|
|
64936
66271
|
toolTarget,
|
|
64937
66272
|
outputRoot
|
|
@@ -65091,6 +66426,8 @@ async function generateRulesCore(params) {
|
|
|
65091
66426
|
simulateCommands: config.getSimulateCommands(),
|
|
65092
66427
|
simulateSubagents: config.getSimulateSubagents(),
|
|
65093
66428
|
simulateSkills: config.getSimulateSkills(),
|
|
66429
|
+
language: config.getLanguage(),
|
|
66430
|
+
deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
|
|
65094
66431
|
skills,
|
|
65095
66432
|
featureOptions: config.getFeatureOptions(toolTarget, "rules"),
|
|
65096
66433
|
dryRun: config.isPreviewMode(),
|
|
@@ -65541,7 +66878,7 @@ function getToolOutputRoot({ config, tool }) {
|
|
|
65541
66878
|
*/
|
|
65542
66879
|
async function importFromTool(params) {
|
|
65543
66880
|
const { config, tool, logger } = params;
|
|
65544
|
-
|
|
66881
|
+
resetRunWarningState();
|
|
65545
66882
|
await assertPluginRootSafe({
|
|
65546
66883
|
toolTarget: tool,
|
|
65547
66884
|
outputRoot: getToolOutputRoot({
|
|
@@ -65863,6 +67200,6 @@ async function importChecksCore(params) {
|
|
|
65863
67200
|
return writtenCount;
|
|
65864
67201
|
}
|
|
65865
67202
|
//#endregion
|
|
65866
|
-
export {
|
|
67203
|
+
export { loadYaml as $, ToolTargetSchema as $t, caseFoldIdentity as A, ALL_FEATURES as An, getHomeDirectory as At, RulesyncRuleFrontmatterSchema as B, removeDirectory as Bt, CLAUDECODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, assertWritablePathInsideRoot as Ct, CLAUDECODE_SKILLS_DIR_PATH as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, ensureDir as Dt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, directoryExists as Et, RulesyncSubagent as F, hasDeceptiveHiddenCharacters as Fn, listFilePathsRecursively as Ft, getRulesyncSourceCandidates as G, resolvePath as Gt, RulesyncMcp as H, removeFile as Ht, RulesyncSubagentFrontmatterSchema as I, quoteForLog as In, listSubdirectoryNames as It, RulesyncCommand as J, writeFileBuffer as Jt, resolveRulesyncSourceWritePath as K, runWithDirectoryRollback as Kt, RulesyncSkill as L, stripControlCharacters as Ln, pathEscapesRoot as Lt, AUGMENTCODE_DIR as M, DEPRECATED_FEATURE_REPLACEMENTS as Mn, isFileSystemError as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, formatError as Nn, isSymlink as Nt, FACTORYDROID_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, fileExists as Ot, getLocalSkillDirNames as P, truncateText as Pn, listDirectoryEntryNames as Pt, stringifyFrontmatter as Q, PACKAGING_TOOL_TARGETS as Qt, RulesyncSkillFrontmatterSchema as R, stripControlCharactersKeepingLineFeeds as Rn, readFileContent as Rt, CODEXCLI_DIR as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, assertTreeContainsNoSymlinks as St, CLAUDECODE_MEMORIES_DIR_NAME as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, createTempDirectory as Tt, RulesyncIgnore as U, removeFileStrict as Ut, RulesyncPermissions as V, removeDirectoryStrict as Vt, RulesyncHooks as W, removeTempDirectory as Wt, RulesyncCheck as X, ALL_TOOL_TARGETS as Xt, RulesyncCommandFrontmatterSchema as Y, writeFileContent as Yt, RulesyncCheckFrontmatterSchema as Z, ALL_TOOL_TARGETS_WITH_WILDCARD as Zt, CommandsProcessor as _, RULESYNC_MCP_RELATIVE_FILE_PATH as _n, withWarnOnceScope as _t, getProcessorRegistryEntry as a, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as an, CONFLICTING_TARGET_PAIRS as at, ChecksProcessor as b, RULESYNC_PERMISSIONS_FILE_NAME as bn, applyFileMode as bt, RulesProcessor as c, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as cn, SourceEntrySchema as ct, ELLIPSIS_WIDTH as d, RULESYNC_HOOKS_LEGACY_FILE_NAME as dn, JsonLogger as dt, CURATED_RULES_FEATURE_SUBDIR as en, SHARED_USER_MANAGED_CONFIG_PATHS as et, displayWidthOf as f, RULESYNC_HOOKS_RELATIVE_FILE_PATH as fn, WarningCollectingLogger as ft, HooksProcessor as g, RULESYNC_MCP_LEGACY_FILE_NAME as gn, resetRunWarningState as gt, IgnoreProcessor as h, RULESYNC_MCP_FILE_NAME as hn, withFallbackLoggerTarget as ht, inspectInputRoots as i, RULESYNC_CHECKS_RELATIVE_DIR_PATH as in, resolveEffectiveInputRoots as it, groupSpellingsByCaseFoldedIdentity as j, ALL_FEATURES_WITH_WILDCARD as jn, isFileNotFoundError as jt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as k, parseCommaSeparatedList as kn, getFileSize as kt, SubagentsProcessor as l, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ln, findControlCharacter as lt, McpProcessor as m, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as mn, warnOnConflictingFlags as mt, formatSourceLoadFailure as n, RULESYNC_AIIGNORE_FILE_NAME as nn, ConfigResolver as nt, convertFromTool as o, RULESYNC_CONFIG_RELATIVE_FILE_PATH as on, ConfigFileSchema as ot, shortenToWidth as p, RULESYNC_IGNORE_RELATIVE_FILE_PATH as pn, fallbackLogger as pt, parseJsonc as q, toPosixPath as qt, generate as r, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as rn, mergeInputRootConfigs as rt, isPackagingToolTarget as s, RULESYNC_CONFIG_SCHEMA_URL as sn, GITIGNORE_DESTINATION_KEY as st, importFromTool as t, MAX_FILE_SIZE as tn, SKILL_FILE_NAME as tt, SkillsProcessor as u, RULESYNC_HOOKS_FILE_NAME as un, ConsoleLogger as ut, QWENCODE_DIR as v, RULESYNC_MCP_SCHEMA_URL as vn, CLIError as vt, CLAUDECODE_LOCAL_RULE_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, checkPathTraversal as wt, CODEXCLI_BASH_RULES_FILE_NAME as x, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as xn, assertDirectoryIfExists as xt, QWENCODE_LOCAL_RULE_FILE_NAME as y, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as yn, ErrorCodes as yt, RulesyncRule as z, stripHiddenCharacters as zn, readFileContentOrNull as zt };
|
|
65867
67204
|
|
|
65868
|
-
//# sourceMappingURL=import-
|
|
67205
|
+
//# sourceMappingURL=import-5_n-Y8N6.js.map
|