rulesync 16.18.0 → 16.19.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/README.md +1 -1
- package/dist/cli/index.cjs +605 -211
- package/dist/cli/index.js +604 -210
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BKnAc4rT.js → import-DijDR24m.js} +1543 -525
- package/dist/import-DijDR24m.js.map +1 -0
- package/dist/{import-BQpUs1JO.cjs → import-q1SKbuFR.cjs} +1614 -548
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BKnAc4rT.js.map +0 -1
|
@@ -30,16 +30,222 @@ let node_os = require("node:os");
|
|
|
30
30
|
node_os = __toESM(node_os, 1);
|
|
31
31
|
let es_toolkit = require("es-toolkit");
|
|
32
32
|
let globby = require("globby");
|
|
33
|
+
let node_async_hooks = require("node:async_hooks");
|
|
34
|
+
let node_util = require("node:util");
|
|
33
35
|
let gray_matter = require("gray-matter");
|
|
34
36
|
gray_matter = __toESM(gray_matter, 1);
|
|
35
37
|
let js_yaml = require("js-yaml");
|
|
36
38
|
let es_toolkit_object = require("es-toolkit/object");
|
|
37
39
|
let node_fs = require("node:fs");
|
|
38
40
|
let node_crypto = require("node:crypto");
|
|
39
|
-
let node_util = require("node:util");
|
|
40
41
|
let smol_toml = require("smol-toml");
|
|
41
42
|
smol_toml = __toESM(smol_toml, 1);
|
|
42
43
|
let _toon_format_toon = require("@toon-format/toon");
|
|
44
|
+
//#region src/utils/control-characters.ts
|
|
45
|
+
/**
|
|
46
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
47
|
+
* introducer U+009B), the bidirectional overrides and isolates, the Unicode
|
|
48
|
+
* line and paragraph separators, and the plain LRM/RLM/ALM marks. A name or
|
|
49
|
+
* value copied out of an untrusted config file, a fetched repository, or a
|
|
50
|
+
* tool's own settings file must never reach the terminal with these intact:
|
|
51
|
+
* they let the text forge log lines, reorder what is printed around them, or
|
|
52
|
+
* inject escape sequences. LRM, RLM and the Arabic letter mark open no bidi
|
|
53
|
+
* scope of their own, but they still reorder the neutral characters beside
|
|
54
|
+
* them, so they go too — a diagnostic line is not the place to preserve the
|
|
55
|
+
* typography of a right-to-left name.
|
|
56
|
+
*/
|
|
57
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
58
|
+
/**
|
|
59
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
60
|
+
* log line or other terminal output.
|
|
61
|
+
*/
|
|
62
|
+
function stripControlCharacters(text) {
|
|
63
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Removes every control character from `text` except the line feed, so a
|
|
67
|
+
* message written to be read over several lines still is.
|
|
68
|
+
*
|
|
69
|
+
* `stripControlCharacters` takes newlines out because a diagnostic is one line
|
|
70
|
+
* and a name that carries one can forge a second. An error message is not: a
|
|
71
|
+
* lock file names the process holding it over several lines, and the MCP
|
|
72
|
+
* `generate` failure lists one unreadable source per line. The carriage return
|
|
73
|
+
* still goes, since on its own it paints over the line already written — which
|
|
74
|
+
* is why this splits on the line feed and strips each line rather than carrying
|
|
75
|
+
* a second character class that has to be kept in step with the first.
|
|
76
|
+
*/
|
|
77
|
+
function stripControlCharactersKeepingLineFeeds(text) {
|
|
78
|
+
return text.split("\n").map(stripControlCharacters).join("\n");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Matches the characters that take no width of their own.
|
|
82
|
+
*
|
|
83
|
+
* `Default_Ignorable_Code_Point` is the Unicode property for exactly this — the
|
|
84
|
+
* zero-width joiners, the soft hyphen, the variation selectors, the Hangul
|
|
85
|
+
* fillers, the tag characters — and the format category `Cf` covers the few
|
|
86
|
+
* that sit outside it, such as the interlinear annotation marks. Both are used
|
|
87
|
+
* rather than a list of ranges because a list has to be revisited every time
|
|
88
|
+
* Unicode adds one, and the one that is missed is the one an attacker reaches
|
|
89
|
+
* for: U+3164 HANGUL FILLER, the classic of the homograph domain names, is a
|
|
90
|
+
* letter of the Hangul script and would pass every check aimed at Latin.
|
|
91
|
+
*
|
|
92
|
+
* The braille blank is named on its own. It carries no dots, so it draws as
|
|
93
|
+
* nothing while belonging to neither set.
|
|
94
|
+
*
|
|
95
|
+
* None of these is a control character, so none is caught by
|
|
96
|
+
* `stripControlCharacters` — and none of them shows. A name that differs from
|
|
97
|
+
* another only by one of these is drawn exactly like it, which is why a name
|
|
98
|
+
* that carries one is not a name a user can be asked to judge.
|
|
99
|
+
*/
|
|
100
|
+
const INVISIBLE_CHARACTERS_PATTERN = /[\p{Default_Ignorable_Code_Point}\p{Cf}\u2800]/gu;
|
|
101
|
+
/**
|
|
102
|
+
* Removes every zero-width and otherwise invisible character from `text`.
|
|
103
|
+
*
|
|
104
|
+
* Kept apart from `stripControlCharacters` because the two answer different
|
|
105
|
+
* questions. That one asks what is safe to print; this one asks whether a name
|
|
106
|
+
* shows everything it contains, which is what a prompt needs before it offers
|
|
107
|
+
* the name as something to pick.
|
|
108
|
+
*/
|
|
109
|
+
function stripInvisibleCharacters(text) {
|
|
110
|
+
return text.replace(INVISIBLE_CHARACTERS_PATTERN, "");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Removes every character that does not show: the control characters and the
|
|
114
|
+
* invisible ones alike.
|
|
115
|
+
*
|
|
116
|
+
* This is the form a name has to survive unchanged before it can be offered as
|
|
117
|
+
* something to choose. Callers that need the whole answer should reach for this
|
|
118
|
+
* rather than composing the two strippers themselves, so that the order — and
|
|
119
|
+
* the definition of "hidden" — lives in one place.
|
|
120
|
+
*/
|
|
121
|
+
function stripHiddenCharacters(text) {
|
|
122
|
+
return stripInvisibleCharacters(stripControlCharacters(text));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The invisible characters that do a job in some scripts rather than only
|
|
126
|
+
* hiding: the two zero-width joiners and the variation selectors.
|
|
127
|
+
*
|
|
128
|
+
* A Persian or Indic name spells a word with ZWNJ (U+200C) in it, and an emoji
|
|
129
|
+
* name is a chain of ZWJ (U+200D) and variation selectors. Refusing those
|
|
130
|
+
* outright would refuse names that are written the only way their script writes
|
|
131
|
+
* them, so they are judged by where they sit rather than by what they are.
|
|
132
|
+
*/
|
|
133
|
+
const ZERO_WIDTH_JOINER_PATTERN = /\u200c|\u200d/u;
|
|
134
|
+
const VARIATION_SELECTOR_PATTERN = /[\u{fe00}-\u{fe0f}]|[\u{e0100}-\u{e01ef}]/u;
|
|
135
|
+
/** The twelve characters a keycap can be built on, per ED-14 of UTS #51. */
|
|
136
|
+
const KEYCAP_BASE_PATTERN = /[0-9#*]/u;
|
|
137
|
+
/** The selector that asks for the emoji form of the character before it. */
|
|
138
|
+
const EMOJI_PRESENTATION_SELECTOR$1 = "️";
|
|
139
|
+
/** U+20E3, which draws the box the base and the selector go inside. */
|
|
140
|
+
const COMBINING_ENCLOSING_KEYCAP = "⃣";
|
|
141
|
+
/**
|
|
142
|
+
* Whether the three characters are an emoji keycap, spelled as UTS #51 spells
|
|
143
|
+
* it: `Emoji_Keycap_Sequence := [0-9#*] FE0F 20E3`.
|
|
144
|
+
*
|
|
145
|
+
* The twelve bases are the only ASCII characters Unicode gives the `Emoji`
|
|
146
|
+
* property to, and not one of them is a pictograph — `1` is a digit and `#` is
|
|
147
|
+
* punctuation — so the joining list below cannot see the sequence, and a
|
|
148
|
+
* directory named `1\u{fe0f}\u{20e3}` would be turned away as a digit padded
|
|
149
|
+
* with a variation selector. It is not padding: the selector is what asks for
|
|
150
|
+
* the emoji form of the digit, and the enclosing keycap behind it is what draws
|
|
151
|
+
* the box around it. Both neighbors are required, which is what keeps the
|
|
152
|
+
* exception to the sequence rather than handing it to every digit in every
|
|
153
|
+
* name: `pdf1` with a variation selector and no keycap after it is padding
|
|
154
|
+
* still, and is refused still.
|
|
155
|
+
*
|
|
156
|
+
* @see https://www.unicode.org/reports/tr51/#def_emoji_keycap_sequence
|
|
157
|
+
*/
|
|
158
|
+
function isKeycapSequence(params) {
|
|
159
|
+
const { base, selector, following } = params;
|
|
160
|
+
return base !== void 0 && KEYCAP_BASE_PATTERN.test(base) && selector === EMOJI_PRESENTATION_SELECTOR$1 && following === COMBINING_ENCLOSING_KEYCAP;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The characters a joiner has work to do beside: the scripts whose words are
|
|
164
|
+
* written with one, and the pictographs an emoji sequence is built from.
|
|
165
|
+
*
|
|
166
|
+
* A list of what may join rather than of what may not, because the two are not
|
|
167
|
+
* the same size. `pdf` with a ZWNJ between the d and the f is `pdf` on screen
|
|
168
|
+
* and a different directory underneath, and the same is true of `設定` with a
|
|
169
|
+
* ZWJ after the first character: neither Latin nor Han joins anything that way,
|
|
170
|
+
* and nor does Cyrillic, Greek, Hangul or kana. Naming the scripts that do —
|
|
171
|
+
* the Arabic family, the Indic ones, Mongolian, and the pictographs — is what
|
|
172
|
+
* keeps the exception to the names that need it, instead of handing it to every
|
|
173
|
+
* writing system that is merely not Latin.
|
|
174
|
+
*/
|
|
175
|
+
const JOINING_CONTEXT_PATTERN = /[\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Mongolian}\p{Script=Devanagari}\p{Script=Bengali}\p{Script=Gurmukhi}\p{Script=Gujarati}\p{Script=Oriya}\p{Script=Tamil}\p{Script=Telugu}\p{Script=Kannada}\p{Script=Malayalam}\p{Script=Sinhala}\p{Script=Myanmar}\p{Script=Khmer}\p{Script=Tibetan}\p{Script=Adlam}\p{Extended_Pictographic}]/u;
|
|
176
|
+
/** Non-global copies, because `test` on a global regex carries state between calls. */
|
|
177
|
+
const CONTROL_CHARACTER_PATTERN = new RegExp(CONTROL_CHARACTERS_PATTERN.source, "u");
|
|
178
|
+
const INVISIBLE_CHARACTER_PATTERN = new RegExp(INVISIBLE_CHARACTERS_PATTERN.source, "u");
|
|
179
|
+
/**
|
|
180
|
+
* Whether `text` carries a hidden character that is there to hide something.
|
|
181
|
+
*
|
|
182
|
+
* This is the question a name has to answer before it can be offered as
|
|
183
|
+
* something to pick, and it is a narrower one than `stripHiddenCharacters`
|
|
184
|
+
* answers. Every control character counts, and so does every invisible
|
|
185
|
+
* character — except a joiner or variation selector standing where its own
|
|
186
|
+
* script would put one, which is to say beside a character from a script that
|
|
187
|
+
* is written with joiners, or beside a pictograph.
|
|
188
|
+
*
|
|
189
|
+
* A joiner is held to both of its neighbors, since it exists to bind two
|
|
190
|
+
* characters and a name that ends in one is binding nothing: `設定` with a ZWJ
|
|
191
|
+
* after it is `設定` on screen and a second directory underneath. A variation
|
|
192
|
+
* selector is held only to the character before it, which is the one it selects
|
|
193
|
+
* a form for, and which is why an emoji name may end in one.
|
|
194
|
+
*
|
|
195
|
+
* The keycap sequence is the one emoji the joining list cannot recognize on its
|
|
196
|
+
* own, since what it is built on is a digit or an ASCII sign rather than a
|
|
197
|
+
* pictograph, so it is matched whole instead.
|
|
198
|
+
*
|
|
199
|
+
* Han is not on the joining list, so an ideographic variation sequence — a Han
|
|
200
|
+
* character followed by one of U+E0100 onward — is refused along with the rest.
|
|
201
|
+
* That is the intended trade: no skill directory here is named with one, and
|
|
202
|
+
* the pair is drawn as the bare character on every terminal that has no font
|
|
203
|
+
* for the variant, which is the shape the check exists to refuse.
|
|
204
|
+
*
|
|
205
|
+
* The test is a heuristic in place of the CONTEXTJ joining rules of IDNA,
|
|
206
|
+
* which decide the same question by the joining type of the characters around
|
|
207
|
+
* the joiner. It errs toward accepting a name written in a script that needs
|
|
208
|
+
* these characters, and toward rejecting one that mixes them into Latin, where
|
|
209
|
+
* they can only be padding.
|
|
210
|
+
*/
|
|
211
|
+
function hasDeceptiveHiddenCharacters(text) {
|
|
212
|
+
const characters = [...text];
|
|
213
|
+
const joinsCharacter = (neighbor) => neighbor !== void 0 && JOINING_CONTEXT_PATTERN.test(neighbor);
|
|
214
|
+
return characters.some((character, index) => {
|
|
215
|
+
if (CONTROL_CHARACTER_PATTERN.test(character)) return true;
|
|
216
|
+
if (!INVISIBLE_CHARACTER_PATTERN.test(character)) return false;
|
|
217
|
+
if (VARIATION_SELECTOR_PATTERN.test(character)) {
|
|
218
|
+
if (isKeycapSequence({
|
|
219
|
+
base: characters[index - 1],
|
|
220
|
+
selector: character,
|
|
221
|
+
following: characters[index + 1]
|
|
222
|
+
})) return false;
|
|
223
|
+
return !joinsCharacter(characters[index - 1]);
|
|
224
|
+
}
|
|
225
|
+
if (!ZERO_WIDTH_JOINER_PATTERN.test(character)) return true;
|
|
226
|
+
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/utils/truncate.ts
|
|
231
|
+
/**
|
|
232
|
+
* Cut `text` to `maxLength` without splitting a character in half.
|
|
233
|
+
*
|
|
234
|
+
* `String.prototype.slice` counts UTF-16 units, so cutting inside a surrogate
|
|
235
|
+
* pair leaves a lone surrogate that the next encoder turns into a replacement
|
|
236
|
+
* character, and cutting inside a `\uXXXX` escape that `JSON.stringify` wrote
|
|
237
|
+
* leaves a dangling backslash. Diagnostics quote files rulesync did not write,
|
|
238
|
+
* so both are reachable from a repository's own content rather than only from
|
|
239
|
+
* a hand-crafted string.
|
|
240
|
+
*/
|
|
241
|
+
function truncateText({ text, maxLength, suffix }) {
|
|
242
|
+
if (text.length <= maxLength) return text;
|
|
243
|
+
const characters = Array.from(text.slice(0, maxLength * 2 + 2));
|
|
244
|
+
if (characters.length <= maxLength) return text;
|
|
245
|
+
const cut = characters.slice(0, maxLength).join("");
|
|
246
|
+
return `${(/\\*$/.exec(cut)?.[0].length ?? 0) % 2 === 0 ? cut : cut.slice(0, -1)}${suffix}`;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
43
249
|
//#region src/utils/error.ts
|
|
44
250
|
/**
|
|
45
251
|
* Convert various error types to a readable error message
|
|
@@ -64,10 +270,52 @@ let _toon_format_toon = require("@toon-format/toon");
|
|
|
64
270
|
function isZodErrorLike(error) {
|
|
65
271
|
return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every((issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string");
|
|
66
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* How much of a Zod error the formatted message spells out.
|
|
275
|
+
*
|
|
276
|
+
* One `safeParse` of a large invalid document produces an issue per offending
|
|
277
|
+
* node, each carrying the path and message, so the raw expansion is bounded by
|
|
278
|
+
* the size of the input rather than by anything rulesync decides — and the
|
|
279
|
+
* formatted message no longer stops at a terminal: it becomes the `message` of
|
|
280
|
+
* a `--json` failure document and of an MCP result. The first few issues are
|
|
281
|
+
* what tells the reader which file to open; the rest is the same information
|
|
282
|
+
* again, at whatever length the input chose.
|
|
283
|
+
*/
|
|
284
|
+
const MAX_ZOD_ISSUES_LENGTH = 2e3;
|
|
285
|
+
/**
|
|
286
|
+
* How much of any other error the formatted message spells out.
|
|
287
|
+
*
|
|
288
|
+
* Larger than the Zod bound because the text is the error's own sentence rather
|
|
289
|
+
* than a re-listing of one issue per offending node, and because the MCP
|
|
290
|
+
* `generate` failure hands this one a line per unreadable source. Bounded all
|
|
291
|
+
* the same: a parser quotes the offending line verbatim, and a minified file is
|
|
292
|
+
* one line the length of the file.
|
|
293
|
+
*/
|
|
294
|
+
const MAX_ERROR_MESSAGE_LENGTH = 8e3;
|
|
295
|
+
/**
|
|
296
|
+
* Strip and bound an error message that is about to be read by something other
|
|
297
|
+
* than a terminal — a `--json` failure document, an MCP result.
|
|
298
|
+
*
|
|
299
|
+
* The line feed stays: several messages are deliberately written over more than
|
|
300
|
+
* one line, and running them together would cost more than the newline can do
|
|
301
|
+
* here. Everything that reorders the text around it, or that an escape sequence
|
|
302
|
+
* is written with, goes.
|
|
303
|
+
*/
|
|
304
|
+
function boundErrorMessage(text) {
|
|
305
|
+
return truncateText({
|
|
306
|
+
text: stripControlCharactersKeepingLineFeeds(text),
|
|
307
|
+
maxLength: MAX_ERROR_MESSAGE_LENGTH,
|
|
308
|
+
suffix: "…(truncated)"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
67
311
|
function formatError(error) {
|
|
68
|
-
if (error instanceof zod.ZodError || isZodErrorLike(error)) return `Zod raw error: ${
|
|
69
|
-
|
|
70
|
-
|
|
312
|
+
if (error instanceof zod.ZodError || isZodErrorLike(error)) return `Zod raw error: ${truncateText({
|
|
313
|
+
text: stripControlCharacters(JSON.stringify(error.issues)),
|
|
314
|
+
maxLength: MAX_ZOD_ISSUES_LENGTH,
|
|
315
|
+
suffix: "…(truncated)"
|
|
316
|
+
})}`;
|
|
317
|
+
if (error instanceof Error) return boundErrorMessage(`${error.name}: ${error.message}`);
|
|
318
|
+
return boundErrorMessage(String(error));
|
|
71
319
|
}
|
|
72
320
|
//#endregion
|
|
73
321
|
//#region src/types/features.ts
|
|
@@ -147,34 +395,34 @@ const isFeatureValueEnabled = (value) => {
|
|
|
147
395
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
148
396
|
//#endregion
|
|
149
397
|
//#region src/constants/rulesync-paths.ts
|
|
150
|
-
const { join: join$
|
|
398
|
+
const { join: join$298 } = node_path.posix;
|
|
151
399
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
152
400
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
153
401
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
154
402
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
155
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
403
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$298(RULES_FEATURE_SUBDIR, ".curated");
|
|
156
404
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
157
405
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
158
406
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
159
407
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
160
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
161
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
162
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
163
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
164
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
165
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
166
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
167
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
168
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
169
|
-
join$
|
|
170
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
171
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
408
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$298(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
409
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
410
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
411
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
412
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
413
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
414
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
415
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
416
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
417
|
+
join$298(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
418
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
419
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
172
420
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
173
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
421
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
174
422
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
175
423
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
176
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
177
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
424
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
425
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$298(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
178
426
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
179
427
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
180
428
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -361,7 +609,8 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
361
609
|
"zoocode",
|
|
362
610
|
"rovodev",
|
|
363
611
|
"takt",
|
|
364
|
-
"vibe"
|
|
612
|
+
"vibe",
|
|
613
|
+
"zcode"
|
|
365
614
|
];
|
|
366
615
|
const skillsProcessorToolTargetTuple = [
|
|
367
616
|
"agentsmd",
|
|
@@ -541,138 +790,6 @@ async function mapWithConcurrency({ items, limit, mapper }) {
|
|
|
541
790
|
return results;
|
|
542
791
|
}
|
|
543
792
|
//#endregion
|
|
544
|
-
//#region src/utils/control-characters.ts
|
|
545
|
-
/**
|
|
546
|
-
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
547
|
-
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
548
|
-
* line and paragraph separators, and the plain LRM/RLM/ALM marks. A name or value
|
|
549
|
-
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
550
|
-
* settings file must never reach the terminal with these intact: they let the
|
|
551
|
-
* text forge log lines, reorder what is printed around them, or inject escape
|
|
552
|
-
* sequences. LRM, RLM and the Arabic letter mark open no bidi scope of their
|
|
553
|
-
* own, but they still reorder the neutral characters beside them, so they go too — a diagnostic line is not the
|
|
554
|
-
* place to preserve the typography of a right-to-left name.
|
|
555
|
-
*/
|
|
556
|
-
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
557
|
-
/**
|
|
558
|
-
* Removes every control character from `text` so it is safe to splice into a
|
|
559
|
-
* log line or other terminal output.
|
|
560
|
-
*/
|
|
561
|
-
function stripControlCharacters(text) {
|
|
562
|
-
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Matches the characters that take no width of their own.
|
|
566
|
-
*
|
|
567
|
-
* `Default_Ignorable_Code_Point` is the Unicode property for exactly this — the
|
|
568
|
-
* zero-width joiners, the soft hyphen, the variation selectors, the Hangul
|
|
569
|
-
* fillers, the tag characters — and the format category `Cf` covers the few
|
|
570
|
-
* that sit outside it, such as the interlinear annotation marks. Both are used
|
|
571
|
-
* rather than a list of ranges because a list has to be revisited every time
|
|
572
|
-
* Unicode adds one, and the one that is missed is the one an attacker reaches
|
|
573
|
-
* for: U+3164 HANGUL FILLER, the classic of the homograph domain names, is a
|
|
574
|
-
* letter of the Hangul script and would pass every check aimed at Latin.
|
|
575
|
-
*
|
|
576
|
-
* The braille blank is named on its own. It carries no dots, so it draws as
|
|
577
|
-
* nothing while belonging to neither set.
|
|
578
|
-
*
|
|
579
|
-
* None of these is a control character, so none is caught by
|
|
580
|
-
* `stripControlCharacters` — and none of them shows. A name that differs from
|
|
581
|
-
* another only by one of these is drawn exactly like it, which is why a name
|
|
582
|
-
* that carries one is not a name a user can be asked to judge.
|
|
583
|
-
*/
|
|
584
|
-
const INVISIBLE_CHARACTERS_PATTERN = /[\p{Default_Ignorable_Code_Point}\p{Cf}\u2800]/gu;
|
|
585
|
-
/**
|
|
586
|
-
* Removes every zero-width and otherwise invisible character from `text`.
|
|
587
|
-
*
|
|
588
|
-
* Kept apart from `stripControlCharacters` because the two answer different
|
|
589
|
-
* questions. That one asks what is safe to print; this one asks whether a name
|
|
590
|
-
* shows everything it contains, which is what a prompt needs before it offers
|
|
591
|
-
* the name as something to pick.
|
|
592
|
-
*/
|
|
593
|
-
function stripInvisibleCharacters(text) {
|
|
594
|
-
return text.replace(INVISIBLE_CHARACTERS_PATTERN, "");
|
|
595
|
-
}
|
|
596
|
-
/**
|
|
597
|
-
* Removes every character that does not show: the control characters and the
|
|
598
|
-
* invisible ones alike.
|
|
599
|
-
*
|
|
600
|
-
* This is the form a name has to survive unchanged before it can be offered as
|
|
601
|
-
* something to choose. Callers that need the whole answer should reach for this
|
|
602
|
-
* rather than composing the two strippers themselves, so that the order — and
|
|
603
|
-
* the definition of "hidden" — lives in one place.
|
|
604
|
-
*/
|
|
605
|
-
function stripHiddenCharacters(text) {
|
|
606
|
-
return stripInvisibleCharacters(stripControlCharacters(text));
|
|
607
|
-
}
|
|
608
|
-
/**
|
|
609
|
-
* The invisible characters that do a job in some scripts rather than only
|
|
610
|
-
* hiding: the two zero-width joiners and the variation selectors.
|
|
611
|
-
*
|
|
612
|
-
* A Persian or Indic name spells a word with ZWNJ (U+200C) in it, and an emoji
|
|
613
|
-
* name is a chain of ZWJ (U+200D) and variation selectors. Refusing those
|
|
614
|
-
* outright would refuse names that are written the only way their script writes
|
|
615
|
-
* them, so they are judged by where they sit rather than by what they are.
|
|
616
|
-
*/
|
|
617
|
-
const ZERO_WIDTH_JOINER_PATTERN = /\u200c|\u200d/u;
|
|
618
|
-
const VARIATION_SELECTOR_PATTERN = /[\u{fe00}-\u{fe0f}]|[\u{e0100}-\u{e01ef}]/u;
|
|
619
|
-
/**
|
|
620
|
-
* The characters a joiner has work to do beside: the scripts whose words are
|
|
621
|
-
* written with one, and the pictographs an emoji sequence is built from.
|
|
622
|
-
*
|
|
623
|
-
* A list of what may join rather than of what may not, because the two are not
|
|
624
|
-
* the same size. `pdf` with a ZWNJ between the d and the f is `pdf` on screen
|
|
625
|
-
* and a different directory underneath, and the same is true of `設定` with a
|
|
626
|
-
* ZWJ after the first character: neither Latin nor Han joins anything that way,
|
|
627
|
-
* and nor does Cyrillic, Greek, Hangul or kana. Naming the scripts that do —
|
|
628
|
-
* the Arabic family, the Indic ones, Mongolian, and the pictographs — is what
|
|
629
|
-
* keeps the exception to the names that need it, instead of handing it to every
|
|
630
|
-
* writing system that is merely not Latin.
|
|
631
|
-
*/
|
|
632
|
-
const JOINING_CONTEXT_PATTERN = /[\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Mongolian}\p{Script=Devanagari}\p{Script=Bengali}\p{Script=Gurmukhi}\p{Script=Gujarati}\p{Script=Oriya}\p{Script=Tamil}\p{Script=Telugu}\p{Script=Kannada}\p{Script=Malayalam}\p{Script=Sinhala}\p{Script=Myanmar}\p{Script=Khmer}\p{Script=Tibetan}\p{Script=Adlam}\p{Extended_Pictographic}]/u;
|
|
633
|
-
/** Non-global copies, because `test` on a global regex carries state between calls. */
|
|
634
|
-
const CONTROL_CHARACTER_PATTERN = new RegExp(CONTROL_CHARACTERS_PATTERN.source, "u");
|
|
635
|
-
const INVISIBLE_CHARACTER_PATTERN = new RegExp(INVISIBLE_CHARACTERS_PATTERN.source, "u");
|
|
636
|
-
/**
|
|
637
|
-
* Whether `text` carries a hidden character that is there to hide something.
|
|
638
|
-
*
|
|
639
|
-
* This is the question a name has to answer before it can be offered as
|
|
640
|
-
* something to pick, and it is a narrower one than `stripHiddenCharacters`
|
|
641
|
-
* answers. Every control character counts, and so does every invisible
|
|
642
|
-
* character — except a joiner or variation selector standing where its own
|
|
643
|
-
* script would put one, which is to say beside a character from a script that
|
|
644
|
-
* is written with joiners, or beside a pictograph.
|
|
645
|
-
*
|
|
646
|
-
* A joiner is held to both of its neighbors, since it exists to bind two
|
|
647
|
-
* characters and a name that ends in one is binding nothing: `設定` with a ZWJ
|
|
648
|
-
* after it is `設定` on screen and a second directory underneath. A variation
|
|
649
|
-
* selector is held only to the character before it, which is the one it selects
|
|
650
|
-
* a form for, and which is why an emoji name may end in one.
|
|
651
|
-
*
|
|
652
|
-
* Han is not on the joining list, so an ideographic variation sequence — a Han
|
|
653
|
-
* character followed by one of U+E0100 onward — is refused along with the rest.
|
|
654
|
-
* That is the intended trade: no skill directory here is named with one, and
|
|
655
|
-
* the pair is drawn as the bare character on every terminal that has no font
|
|
656
|
-
* for the variant, which is the shape the check exists to refuse.
|
|
657
|
-
*
|
|
658
|
-
* The test is a heuristic in place of the CONTEXTJ joining rules of IDNA,
|
|
659
|
-
* which decide the same question by the joining type of the characters around
|
|
660
|
-
* the joiner. It errs toward accepting a name written in a script that needs
|
|
661
|
-
* these characters, and toward rejecting one that mixes them into Latin, where
|
|
662
|
-
* they can only be padding.
|
|
663
|
-
*/
|
|
664
|
-
function hasDeceptiveHiddenCharacters(text) {
|
|
665
|
-
const characters = [...text];
|
|
666
|
-
const joinsCharacter = (neighbor) => neighbor !== void 0 && JOINING_CONTEXT_PATTERN.test(neighbor);
|
|
667
|
-
return characters.some((character, index) => {
|
|
668
|
-
if (CONTROL_CHARACTER_PATTERN.test(character)) return true;
|
|
669
|
-
if (!INVISIBLE_CHARACTER_PATTERN.test(character)) return false;
|
|
670
|
-
if (VARIATION_SELECTOR_PATTERN.test(character)) return !joinsCharacter(characters[index - 1]);
|
|
671
|
-
if (!ZERO_WIDTH_JOINER_PATTERN.test(character)) return true;
|
|
672
|
-
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
//#endregion
|
|
676
793
|
//#region src/utils/vitest.ts
|
|
677
794
|
function isEnvTest() {
|
|
678
795
|
return process.env.NODE_ENV === "test";
|
|
@@ -1029,7 +1146,16 @@ async function isSymlink(filepath) {
|
|
|
1029
1146
|
return false;
|
|
1030
1147
|
}
|
|
1031
1148
|
}
|
|
1032
|
-
|
|
1149
|
+
/**
|
|
1150
|
+
* Every entry name directly under `dir`, of whatever kind, in the order the
|
|
1151
|
+
* filesystem reports them.
|
|
1152
|
+
*
|
|
1153
|
+
* Named for what it returns, because {@link listFileNames} sits beside it and
|
|
1154
|
+
* answers a narrower question: this one reports directories and links as well
|
|
1155
|
+
* as files, and reads an unreadable directory as an empty one instead of
|
|
1156
|
+
* failing. Picking this where the other was meant brings both of those back.
|
|
1157
|
+
*/
|
|
1158
|
+
async function listDirectoryEntryNames(dir) {
|
|
1033
1159
|
try {
|
|
1034
1160
|
return await (0, node_fs_promises.readdir)(dir);
|
|
1035
1161
|
} catch {
|
|
@@ -1041,6 +1167,20 @@ function countHiddenSegments(filePath) {
|
|
|
1041
1167
|
return splitPathSegments(filePath).filter(isHiddenPathSegment).length;
|
|
1042
1168
|
}
|
|
1043
1169
|
/**
|
|
1170
|
+
* A path the running platform produced, written posix-separated.
|
|
1171
|
+
*
|
|
1172
|
+
* Not {@link toPosixPath}, which rewrites every backslash whatever it means. That
|
|
1173
|
+
* is right for a path a caller spelled, which may be spelled either way; it is
|
|
1174
|
+
* wrong for one the platform handed back. On a posix platform a backslash is an
|
|
1175
|
+
* ordinary character in a name, so rewriting it folds `a\b` onto `a/b` -- two
|
|
1176
|
+
* different directories, and wherever the result is used as a file's identity the
|
|
1177
|
+
* second one to be seen is taken for a repeat of the first and dropped. On
|
|
1178
|
+
* Windows no name can hold a backslash, so the rewrite there is lossless.
|
|
1179
|
+
*/
|
|
1180
|
+
function nativePathToPosix(filePath) {
|
|
1181
|
+
return node_path.sep === "\\" ? filePath.replaceAll("\\", "/") : filePath;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1044
1184
|
* The real file a path denotes, posix-separated so it compares against the globby results
|
|
1045
1185
|
* that produce it. Two paths share an identity when they resolve to the very same file --
|
|
1046
1186
|
* a link beside its target, a link into a shared tree, or a cycle that walks back into an
|
|
@@ -1048,19 +1188,61 @@ function countHiddenSegments(filePath) {
|
|
|
1048
1188
|
*/
|
|
1049
1189
|
async function realFileIdentity(filePath) {
|
|
1050
1190
|
try {
|
|
1051
|
-
return
|
|
1191
|
+
return nativePathToPosix(await (0, node_fs_promises.realpath)(filePath));
|
|
1052
1192
|
} catch {
|
|
1053
|
-
return
|
|
1193
|
+
return nativePathToPosix(filePath);
|
|
1054
1194
|
}
|
|
1055
1195
|
}
|
|
1056
1196
|
/**
|
|
1197
|
+
* Where the file `targetPath` really denotes sits relative to the one `rootPath`
|
|
1198
|
+
* does, with every link on both sides resolved. Posix-separated, because the
|
|
1199
|
+
* resolved paths it is built from are; a caller that splits it must split on `/`
|
|
1200
|
+
* alone, which is also the only separator a real name can never contain.
|
|
1201
|
+
*
|
|
1202
|
+
* Both sides fall back to their literal path when they cannot be resolved, so an
|
|
1203
|
+
* unresolvable path reads as an escape rather than as a contained one.
|
|
1204
|
+
*/
|
|
1205
|
+
async function resolvedRelativePath({ rootPath, targetPath }) {
|
|
1206
|
+
const [realRootPath, realTargetPath] = await Promise.all([realFileIdentity(rootPath), realFileIdentity(targetPath)]);
|
|
1207
|
+
return node_path.posix.relative(realRootPath, realTargetPath);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Whether a posix relative path -- one {@link resolvedRelativePath} returned, or any
|
|
1211
|
+
* built the same way -- leads out of the root it was taken against.
|
|
1212
|
+
*
|
|
1213
|
+
* Exported so a caller that already holds the relative path can ask this of the very
|
|
1214
|
+
* path it goes on to read, instead of resolving both sides a second time and judging
|
|
1215
|
+
* a result it then has to trust is the same one.
|
|
1216
|
+
*/
|
|
1217
|
+
function posixRelativePathEscapesRoot(relativePath) {
|
|
1218
|
+
return relativePath === ".." || relativePath.startsWith("../") || node_path.posix.isAbsolute(relativePath);
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* How many trailing segments `filePath` and `identity` have in common.
|
|
1222
|
+
*
|
|
1223
|
+
* A path that walked through no link at all shares all of its own segments with
|
|
1224
|
+
* the file's identity; one that walked through a link named differently from
|
|
1225
|
+
* its target parts from the identity at that segment and shares only what
|
|
1226
|
+
* follows it. Counting from the end rather than testing the two for equality is
|
|
1227
|
+
* what lets the comparison hold for a path that is not itself resolved: a glob
|
|
1228
|
+
* rooted at a directory that is a link of its own gives every candidate the
|
|
1229
|
+
* same unresolved prefix, and only the segments below it decide.
|
|
1230
|
+
*/
|
|
1231
|
+
function sharedTrailingSegments(filePath, identity) {
|
|
1232
|
+
const left = splitPathSegments(nativePathToPosix(filePath));
|
|
1233
|
+
const right = splitPathSegments(identity);
|
|
1234
|
+
let shared = 0;
|
|
1235
|
+
while (shared < left.length && shared < right.length && left[left.length - 1 - shared] === right[right.length - 1 - shared]) shared++;
|
|
1236
|
+
return shared;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1057
1239
|
* Pick the one path that represents a file among the paths that resolve to it.
|
|
1058
1240
|
*
|
|
1059
|
-
* The path that walked through no link at all wins outright: it
|
|
1060
|
-
*
|
|
1061
|
-
* callers see, rather than an alias that happens to sort first -- a
|
|
1062
|
-
* `aaa` pointing at `zzz` must not make `zzz/x.md` disappear, and a
|
|
1063
|
-
* `sub/note.md` with the same file reached back through the cycle.
|
|
1241
|
+
* The path that walked through no link at all wins outright: it shares every one of its
|
|
1242
|
+
* segments with the file's identity, which no alias does. That keeps the real location of
|
|
1243
|
+
* a file as the path callers see, rather than an alias that happens to sort first -- a
|
|
1244
|
+
* directory link named `aaa` pointing at `zzz` must not make `zzz/x.md` disappear, and a
|
|
1245
|
+
* cycle must not replace `sub/note.md` with the same file reached back through the cycle.
|
|
1064
1246
|
* Failing that, the fewest dot-prefixed segments wins: when only links are on offer, the
|
|
1065
1247
|
* named one represents the entry rather than a hidden alias that a hidden-entry rule may
|
|
1066
1248
|
* then drop, taking the named path's content with it. `candidates` arrives in sorted
|
|
@@ -1068,8 +1250,9 @@ async function realFileIdentity(filePath) {
|
|
|
1068
1250
|
*/
|
|
1069
1251
|
function chooseRepresentative(candidates, identity) {
|
|
1070
1252
|
return candidates.reduce((best, candidate) => {
|
|
1071
|
-
|
|
1072
|
-
|
|
1253
|
+
const bestShared = sharedTrailingSegments(best, identity);
|
|
1254
|
+
const candidateShared = sharedTrailingSegments(candidate, identity);
|
|
1255
|
+
if (candidateShared !== bestShared) return candidateShared > bestShared ? candidate : best;
|
|
1073
1256
|
return countHiddenSegments(candidate) < countHiddenSegments(best) ? candidate : best;
|
|
1074
1257
|
});
|
|
1075
1258
|
}
|
|
@@ -1167,7 +1350,7 @@ async function dedupeNamesByFileIdentity(params) {
|
|
|
1167
1350
|
});
|
|
1168
1351
|
const entriesByIdentity = /* @__PURE__ */ new Map();
|
|
1169
1352
|
for (const [index, entry] of entries.entries()) {
|
|
1170
|
-
const identity = identities[index] ??
|
|
1353
|
+
const identity = identities[index] ?? nativePathToPosix((0, node_path.join)(dirPath, entry.name));
|
|
1171
1354
|
const group = entriesByIdentity.get(identity);
|
|
1172
1355
|
if (group === void 0) entriesByIdentity.set(identity, [entry]);
|
|
1173
1356
|
else group.push(entry);
|
|
@@ -1219,6 +1402,94 @@ async function listFileNames(dirPath, options = {}) {
|
|
|
1219
1402
|
nameFilter: options.nameFilter
|
|
1220
1403
|
});
|
|
1221
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* The paths of every file below `dirPath`, relative to it, walked rather than
|
|
1407
|
+
* globbed.
|
|
1408
|
+
*
|
|
1409
|
+
* The recursive counterpart of {@link listFileNames}, and there for the same
|
|
1410
|
+
* reason: globby reads a backslash as a path separator and rewrites it in the
|
|
1411
|
+
* paths it returns, so a file named `back\\slash.md` comes back as
|
|
1412
|
+
* `back/slash.md` — a path that belongs to no file, under a name that belongs
|
|
1413
|
+
* to no file either.
|
|
1414
|
+
*
|
|
1415
|
+
* A root that is not there is an empty root. A root that is there but cannot be
|
|
1416
|
+
* read is reported, so it is never mistaken for an empty one — as is a
|
|
1417
|
+
* directory below it, since a subtree silently missing from the result is the
|
|
1418
|
+
* same mistake one level down.
|
|
1419
|
+
*
|
|
1420
|
+
* A directory link is followed like any other directory, but each real
|
|
1421
|
+
* directory is walked only once, so neither a cycle nor a mesh of links can
|
|
1422
|
+
* make the walk repeat itself -- taking every distinct route through a graph of
|
|
1423
|
+
* aliases would cost one traversal per route, which a handful of links is
|
|
1424
|
+
* enough to make hopeless. The name a twice-reachable directory is reported
|
|
1425
|
+
* under is therefore whichever the walk reaches first, and the walk goes level
|
|
1426
|
+
* by level with each level sorted, so that is the shortest path to it and the
|
|
1427
|
+
* first in sorted order among equals. Note the difference from
|
|
1428
|
+
* {@link findFilesByGlobs}, which resolves each of its results and keeps the
|
|
1429
|
+
* real one: here a link nearer the root than the directory it points at stands
|
|
1430
|
+
* in for it. `nameFilter` narrows the files, not the directories the walk
|
|
1431
|
+
* descends into.
|
|
1432
|
+
*
|
|
1433
|
+
* The cost is one round of `readdir` per directory, taken in sequence, and the
|
|
1434
|
+
* whole of a level is held at once, so residency follows the widest level
|
|
1435
|
+
* rather than the deepest path.
|
|
1436
|
+
*/
|
|
1437
|
+
async function listFilePathsRecursively(dirPath, options = {}) {
|
|
1438
|
+
const { followSymbolicLinks = true, includeHidden = false, nameFilter, deduplicateByFileIdentity = false } = options;
|
|
1439
|
+
if (!await directoryExists(dirPath)) return [];
|
|
1440
|
+
const filePaths = [];
|
|
1441
|
+
const walkedIdentities = /* @__PURE__ */ new Set();
|
|
1442
|
+
let level = [{
|
|
1443
|
+
currentPath: dirPath,
|
|
1444
|
+
prefix: ""
|
|
1445
|
+
}];
|
|
1446
|
+
while (level.length > 0) {
|
|
1447
|
+
const nextLevel = [];
|
|
1448
|
+
for (const { currentPath, prefix } of level.toSorted((a, b) => a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0)) {
|
|
1449
|
+
const identity = await realFileIdentity(currentPath);
|
|
1450
|
+
if (walkedIdentities.has(identity)) continue;
|
|
1451
|
+
walkedIdentities.add(identity);
|
|
1452
|
+
const [fileNames, dirNames] = await Promise.all([listFileNames(currentPath, {
|
|
1453
|
+
followSymbolicLinks,
|
|
1454
|
+
includeHidden,
|
|
1455
|
+
nameFilter
|
|
1456
|
+
}), listSubdirectoryNames(currentPath, {
|
|
1457
|
+
followSymbolicLinks,
|
|
1458
|
+
includeHidden
|
|
1459
|
+
})]);
|
|
1460
|
+
for (const fileName of fileNames) filePaths.push(prefix === "" ? fileName : (0, node_path.join)(prefix, fileName));
|
|
1461
|
+
for (const dirName of dirNames) nextLevel.push({
|
|
1462
|
+
currentPath: (0, node_path.join)(currentPath, dirName),
|
|
1463
|
+
prefix: prefix === "" ? dirName : (0, node_path.join)(prefix, dirName)
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
level = nextLevel;
|
|
1467
|
+
}
|
|
1468
|
+
return deduplicateByFileIdentity ? await deduplicateRelativePathsByFileIdentity({
|
|
1469
|
+
dirPath,
|
|
1470
|
+
relativePaths: filePaths
|
|
1471
|
+
}) : filePaths.toSorted();
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* One path per real file, chosen the way {@link findFilesByGlobs} chooses it.
|
|
1475
|
+
*
|
|
1476
|
+
* The walk de-duplicates the directories it descends into, so it cannot loop,
|
|
1477
|
+
* but it still reports every name it walks past: a file reached under two names
|
|
1478
|
+
* is listed twice. A caller reading the result as a set of files has to fold
|
|
1479
|
+
* those aliases together, and has to fold them the same way the glob does, or
|
|
1480
|
+
* the two disagree about the name a file has.
|
|
1481
|
+
*/
|
|
1482
|
+
async function deduplicateRelativePathsByFileIdentity({ dirPath, relativePaths }) {
|
|
1483
|
+
const candidatesByFile = /* @__PURE__ */ new Map();
|
|
1484
|
+
for (const relativePath of relativePaths.toSorted()) {
|
|
1485
|
+
const absolutePath = (0, node_path.join)(dirPath, relativePath);
|
|
1486
|
+
const identity = await realFileIdentity(absolutePath);
|
|
1487
|
+
const candidates = candidatesByFile.get(identity);
|
|
1488
|
+
if (candidates === void 0) candidatesByFile.set(identity, [absolutePath]);
|
|
1489
|
+
else candidates.push(absolutePath);
|
|
1490
|
+
}
|
|
1491
|
+
return [...candidatesByFile.entries()].map(([identity, candidates]) => (0, node_path.relative)(dirPath, chooseRepresentative(candidates, identity))).toSorted();
|
|
1492
|
+
}
|
|
1222
1493
|
async function removeDirectory(dirPath) {
|
|
1223
1494
|
if ([
|
|
1224
1495
|
".",
|
|
@@ -1380,24 +1651,111 @@ var CLIError = class extends Error {
|
|
|
1380
1651
|
//#region src/utils/warned-once.ts
|
|
1381
1652
|
/**
|
|
1382
1653
|
* The messages a once-per-run warning has already emitted in this process.
|
|
1383
|
-
* This lives in its own module,
|
|
1384
|
-
* clear it between tests without pulling `logger.js` into every
|
|
1385
|
-
* graph (which would defeat the module mocks some of those tests
|
|
1654
|
+
* This lives in its own module, importing nothing of rulesync's, so the vitest
|
|
1655
|
+
* setup file can clear it between tests without pulling `logger.js` into every
|
|
1656
|
+
* test's module graph (which would defeat the module mocks some of those tests
|
|
1657
|
+
* install).
|
|
1386
1658
|
*/
|
|
1387
|
-
const
|
|
1659
|
+
const processWideMessages = /* @__PURE__ */ new Set();
|
|
1660
|
+
/**
|
|
1661
|
+
* The set an operation that opened its own scope uses instead.
|
|
1662
|
+
*
|
|
1663
|
+
* The MCP server does not serialize requests, so two runs can be in flight at
|
|
1664
|
+
* once. Sharing one set between them would let the first run spend the token
|
|
1665
|
+
* for a message and leave the second one's result silent about a diagnostic
|
|
1666
|
+
* that applies to it just as much. A scope gives each run its own bookkeeping.
|
|
1667
|
+
*/
|
|
1668
|
+
const scopedMessages = new node_async_hooks.AsyncLocalStorage();
|
|
1669
|
+
function currentMessages() {
|
|
1670
|
+
return scopedMessages.getStore() ?? processWideMessages;
|
|
1671
|
+
}
|
|
1388
1672
|
/** Whether `message` has not been emitted yet; records it when it has not. */
|
|
1389
1673
|
function claimWarnOnce(message) {
|
|
1390
|
-
|
|
1391
|
-
|
|
1674
|
+
const messages = currentMessages();
|
|
1675
|
+
if (messages.has(message)) return false;
|
|
1676
|
+
messages.add(message);
|
|
1392
1677
|
return true;
|
|
1393
1678
|
}
|
|
1394
|
-
/** Forget which warnings were already emitted, so
|
|
1679
|
+
/** Forget which warnings were already emitted, so the next run starts silent. */
|
|
1395
1680
|
function resetWarnedOnceMessages() {
|
|
1396
|
-
|
|
1681
|
+
currentMessages().clear();
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Run `operation` with its own once-per-run bookkeeping, so a concurrent run
|
|
1685
|
+
* neither spends its tokens nor clears its record.
|
|
1686
|
+
*/
|
|
1687
|
+
async function withWarnOnceScope(operation) {
|
|
1688
|
+
return await scopedMessages.run(/* @__PURE__ */ new Set(), operation);
|
|
1397
1689
|
}
|
|
1398
1690
|
//#endregion
|
|
1399
1691
|
//#region src/utils/logger.ts
|
|
1400
1692
|
/**
|
|
1693
|
+
* Formats a log line the way `console.warn` would, so a warning that is handed
|
|
1694
|
+
* back to a caller reads the same as the one that reaches a terminal.
|
|
1695
|
+
*/
|
|
1696
|
+
function formatLogLine({ message, args }) {
|
|
1697
|
+
return args.length === 0 ? message : (0, node_util.format)(message, ...args);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* How much a collecting logger keeps: at most this many warnings, each at most
|
|
1701
|
+
* this long, and no more than this in total.
|
|
1702
|
+
*
|
|
1703
|
+
* Collected warnings travel to places a console line does not — a `--json`
|
|
1704
|
+
* document that another program parses, an MCP result that an agent reads as
|
|
1705
|
+
* context — and their text quotes files rulesync did not write. So the amount a
|
|
1706
|
+
* repository can push through has to be bounded twice over: a config with
|
|
1707
|
+
* thousands of odd keys is a plausible accident, and a report sized in hundreds
|
|
1708
|
+
* of kilobytes is a generous budget for text aimed at whoever reads it next.
|
|
1709
|
+
* The total is the binding limit; the per-line and per-count limits keep one
|
|
1710
|
+
* enormous warning, or one enormous number of them, from being the whole of it.
|
|
1711
|
+
*/
|
|
1712
|
+
const MAX_COLLECTED_WARNINGS = 100;
|
|
1713
|
+
const MAX_COLLECTED_WARNING_LENGTH = 1e3;
|
|
1714
|
+
const MAX_COLLECTED_TOTAL_LENGTH = 8e3;
|
|
1715
|
+
/**
|
|
1716
|
+
* How many distinct warnings the de-duplication remembers.
|
|
1717
|
+
*
|
|
1718
|
+
* The record has to outlive the reported lines — a line dropped for want of
|
|
1719
|
+
* budget must not be counted again the next time the same diagnostic repeats —
|
|
1720
|
+
* so it grows with the number of distinct warnings a run raises rather than
|
|
1721
|
+
* with the number reported. Bounded for the same reason everything else here
|
|
1722
|
+
* is: past this many, later repeats are counted rather than recognized, which
|
|
1723
|
+
* inflates the trailing count but cannot grow the record without end.
|
|
1724
|
+
*/
|
|
1725
|
+
const MAX_DEDUPLICATED_WARNINGS = 1e3;
|
|
1726
|
+
/**
|
|
1727
|
+
* A bounded list of warning lines.
|
|
1728
|
+
*/
|
|
1729
|
+
var WarningCollection = class {
|
|
1730
|
+
lines = [];
|
|
1731
|
+
seen = /* @__PURE__ */ new Set();
|
|
1732
|
+
totalLength = 0;
|
|
1733
|
+
omitted = 0;
|
|
1734
|
+
add({ message, args }) {
|
|
1735
|
+
const kept = truncateText({
|
|
1736
|
+
text: stripControlCharacters(formatLogLine({
|
|
1737
|
+
message,
|
|
1738
|
+
args
|
|
1739
|
+
})),
|
|
1740
|
+
maxLength: MAX_COLLECTED_WARNING_LENGTH,
|
|
1741
|
+
suffix: "…(truncated)"
|
|
1742
|
+
});
|
|
1743
|
+
if (this.seen.has(kept)) return;
|
|
1744
|
+
if (this.lines.length >= MAX_COLLECTED_WARNINGS || this.totalLength + kept.length > MAX_COLLECTED_TOTAL_LENGTH) {
|
|
1745
|
+
if (this.seen.size < MAX_DEDUPLICATED_WARNINGS) this.seen.add(kept);
|
|
1746
|
+
this.omitted++;
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
this.seen.add(kept);
|
|
1750
|
+
this.lines.push(kept);
|
|
1751
|
+
this.totalLength += kept.length;
|
|
1752
|
+
}
|
|
1753
|
+
toArray() {
|
|
1754
|
+
if (this.omitted === 0) return [...this.lines];
|
|
1755
|
+
return [...this.lines, `… and ${this.omitted} more warning(s) not reported`];
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
/**
|
|
1401
1759
|
* Base class for shared verbose/silent state and configuration logic
|
|
1402
1760
|
*/
|
|
1403
1761
|
var BaseLogger = class {
|
|
@@ -1413,6 +1771,9 @@ var BaseLogger = class {
|
|
|
1413
1771
|
get silent() {
|
|
1414
1772
|
return this._silent;
|
|
1415
1773
|
}
|
|
1774
|
+
get reportsWhileSilent() {
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1416
1777
|
configure({ verbose, silent }) {
|
|
1417
1778
|
this._silent = silent;
|
|
1418
1779
|
this._verbose = verbose && !silent;
|
|
@@ -1458,11 +1819,18 @@ var ConsoleLogger = class extends BaseLogger {
|
|
|
1458
1819
|
/**
|
|
1459
1820
|
* JsonLogger - structured JSON output to stdout/stderr
|
|
1460
1821
|
*
|
|
1461
|
-
*
|
|
1822
|
+
* The console output methods (info, success, debug) are no-ops. `warn` is not:
|
|
1823
|
+
* a diagnostic that only reached the console would be invisible to a `--json`
|
|
1824
|
+
* consumer, which reads the document and nothing else, so warnings are
|
|
1825
|
+
* collected and emitted as the document's top-level `warnings` array instead.
|
|
1826
|
+
* Top-level rather than inside `data` so it can never collide with a key a
|
|
1827
|
+
* command captured, and so it survives on the failure document too — the case
|
|
1828
|
+
* where a diagnostic about the input is most likely to explain the failure.
|
|
1462
1829
|
*/
|
|
1463
1830
|
var JsonLogger = class extends BaseLogger {
|
|
1464
1831
|
_jsonOutputDone = false;
|
|
1465
1832
|
_jsonData = {};
|
|
1833
|
+
_warnings = new WarningCollection();
|
|
1466
1834
|
_commandName;
|
|
1467
1835
|
_version;
|
|
1468
1836
|
constructor({ command, version, verbose = false, silent = false }) {
|
|
@@ -1491,6 +1859,8 @@ var JsonLogger = class extends BaseLogger {
|
|
|
1491
1859
|
command: this._commandName,
|
|
1492
1860
|
version: this._version
|
|
1493
1861
|
};
|
|
1862
|
+
const warnings = this._warnings.toArray();
|
|
1863
|
+
if (warnings.length > 0) output.warnings = warnings;
|
|
1494
1864
|
if (success) output.data = this._jsonData;
|
|
1495
1865
|
else if (error) {
|
|
1496
1866
|
output.error = {
|
|
@@ -1506,7 +1876,13 @@ var JsonLogger = class extends BaseLogger {
|
|
|
1506
1876
|
}
|
|
1507
1877
|
info(_message, ..._args) {}
|
|
1508
1878
|
success(_message, ..._args) {}
|
|
1509
|
-
warn(
|
|
1879
|
+
warn(message, ...args) {
|
|
1880
|
+
if (this._silent) return;
|
|
1881
|
+
this._warnings.add({
|
|
1882
|
+
message,
|
|
1883
|
+
args
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1510
1886
|
error(message, code, ..._args) {
|
|
1511
1887
|
if (isEnvTest()) return;
|
|
1512
1888
|
const errorMessage = message instanceof Error ? message.message : message;
|
|
@@ -1532,13 +1908,110 @@ function warnOnConflictingFlags({ verbose, silent, jsonMode }) {
|
|
|
1532
1908
|
console.warn("Both --verbose and --silent specified; --silent takes precedence");
|
|
1533
1909
|
}
|
|
1534
1910
|
/**
|
|
1911
|
+
* Where `fallbackLogger` sends what it is given, scoped to the operation that
|
|
1912
|
+
* adopted it.
|
|
1913
|
+
*
|
|
1914
|
+
* An `AsyncLocalStorage` rather than a plain variable because the target is
|
|
1915
|
+
* per-operation, not per-process: a long-lived MCP server can have two requests
|
|
1916
|
+
* in flight at once, and a save/restore pair would let the first one to finish
|
|
1917
|
+
* hand the still-running request's warnings back to a console nobody reads.
|
|
1918
|
+
* Each operation sees only its own store.
|
|
1919
|
+
*/
|
|
1920
|
+
const fallbackTargetStorage = new node_async_hooks.AsyncLocalStorage();
|
|
1921
|
+
/**
|
|
1922
|
+
* Where warnings go outside any adopted scope: a plain console logger, which is
|
|
1923
|
+
* what a code path with no logger threaded through would otherwise have used.
|
|
1924
|
+
*/
|
|
1925
|
+
const defaultFallbackTarget = new ConsoleLogger();
|
|
1926
|
+
function currentFallbackTarget() {
|
|
1927
|
+
return fallbackTargetStorage.getStore() ?? defaultFallbackTarget;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* True while the forwarder is inside a call it is forwarding.
|
|
1931
|
+
*
|
|
1932
|
+
* The adopted target is supposed to be something other than the forwarder, but
|
|
1933
|
+
* a wrapper *around* it — `hooks-processor.ts` returns one that prefixes the
|
|
1934
|
+
* tool target onto every warning — passes the identity check in
|
|
1935
|
+
* {@link withFallbackLoggerTarget} and would forward straight back here. A
|
|
1936
|
+
* plain module-level flag is enough because the forwarding is synchronous: the
|
|
1937
|
+
* call returns before anything else can run.
|
|
1938
|
+
*/
|
|
1939
|
+
let forwarding = false;
|
|
1940
|
+
/**
|
|
1941
|
+
* Reads from, or writes to, whichever logger the running operation adopted,
|
|
1942
|
+
* with the wrapper case above cut off at one hop.
|
|
1943
|
+
*
|
|
1944
|
+
* Every member of the forwarder goes through here, not just the two that write:
|
|
1945
|
+
* `warnOnceWithFallback` reads `silent` and `reportsWhileSilent` before it ever
|
|
1946
|
+
* calls `warn`, so a guard on the writing side alone would still be reached
|
|
1947
|
+
* through a getter that never returns.
|
|
1948
|
+
*/
|
|
1949
|
+
function throughFallbackTarget(use) {
|
|
1950
|
+
if (forwarding) return use(defaultFallbackTarget);
|
|
1951
|
+
forwarding = true;
|
|
1952
|
+
try {
|
|
1953
|
+
return use(currentFallbackTarget());
|
|
1954
|
+
} finally {
|
|
1955
|
+
forwarding = false;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1535
1959
|
* Shared fallback logger for code paths that have no command logger threaded
|
|
1536
1960
|
* through (module-level translators, `warnWithFallback(undefined, ...)`).
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1961
|
+
*
|
|
1962
|
+
* It is a thin forwarder rather than a logger of its own so that the operation
|
|
1963
|
+
* currently running can adopt it: `wrapCommand` points it at the command
|
|
1964
|
+
* logger, which is how a warning raised deep in a translator still reaches a
|
|
1965
|
+
* `--json` document or an MCP result instead of being written to a console that
|
|
1966
|
+
* nobody in those modes is reading. Modules that captured a reference to
|
|
1967
|
+
* `fallbackLogger` at import time follow the redirection too, which a
|
|
1968
|
+
* swapped-out binding would not give us.
|
|
1969
|
+
*/
|
|
1970
|
+
const fallbackLogger = {
|
|
1971
|
+
configure(options) {
|
|
1972
|
+
defaultFallbackTarget.configure(options);
|
|
1973
|
+
},
|
|
1974
|
+
get verbose() {
|
|
1975
|
+
return throughFallbackTarget((target) => target.verbose);
|
|
1976
|
+
},
|
|
1977
|
+
get silent() {
|
|
1978
|
+
return throughFallbackTarget((target) => target.silent);
|
|
1979
|
+
},
|
|
1980
|
+
get reportsWhileSilent() {
|
|
1981
|
+
return throughFallbackTarget((target) => target.reportsWhileSilent);
|
|
1982
|
+
},
|
|
1983
|
+
get jsonMode() {
|
|
1984
|
+
return throughFallbackTarget((target) => target.jsonMode);
|
|
1985
|
+
},
|
|
1986
|
+
captureData(_key, _value) {},
|
|
1987
|
+
getJsonData() {
|
|
1988
|
+
return {};
|
|
1989
|
+
},
|
|
1990
|
+
outputJson(_success, _error) {},
|
|
1991
|
+
info(_message, ..._args) {},
|
|
1992
|
+
success(_message, ..._args) {},
|
|
1993
|
+
warn(message, ...args) {
|
|
1994
|
+
throughFallbackTarget((target) => {
|
|
1995
|
+
target.warn(message, ...args);
|
|
1996
|
+
});
|
|
1997
|
+
},
|
|
1998
|
+
error(message, code, ...args) {
|
|
1999
|
+
defaultFallbackTarget.error(message, code, ...args);
|
|
2000
|
+
},
|
|
2001
|
+
debug(_message, ..._args) {}
|
|
2002
|
+
};
|
|
2003
|
+
/**
|
|
2004
|
+
* Run `operation` with `fallbackLogger` pointed at `logger`, so warnings raised
|
|
2005
|
+
* where no logger was threaded through end up in the same place as the rest of
|
|
2006
|
+
* that operation's diagnostics.
|
|
2007
|
+
*
|
|
2008
|
+
* The redirection lasts exactly as long as the operation and is invisible to
|
|
2009
|
+
* anything running beside it.
|
|
1540
2010
|
*/
|
|
1541
|
-
|
|
2011
|
+
async function withFallbackLoggerTarget({ logger, operation }) {
|
|
2012
|
+
if (logger === fallbackLogger) return await operation();
|
|
2013
|
+
return await fallbackTargetStorage.run(logger, () => withWarnOnceScope(operation));
|
|
2014
|
+
}
|
|
1542
2015
|
/**
|
|
1543
2016
|
* Emit a warning through `logger.warn` if a logger is supplied, otherwise
|
|
1544
2017
|
* fall through to the shared `fallbackLogger`. Centralizes the "logger may
|
|
@@ -1556,9 +2029,35 @@ function warnWithFallback(logger, message) {
|
|
|
1556
2029
|
* varies with what the user should do next does not.
|
|
1557
2030
|
*/
|
|
1558
2031
|
function warnOnceWithFallback(logger, message) {
|
|
2032
|
+
const destination = logger ?? fallbackLogger;
|
|
2033
|
+
if (destination.silent && !destination.reportsWhileSilent) return;
|
|
1559
2034
|
if (!claimWarnOnce(message)) return;
|
|
1560
|
-
|
|
2035
|
+
destination.warn(message);
|
|
1561
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* A `ConsoleLogger` that keeps the warnings it is given.
|
|
2039
|
+
*
|
|
2040
|
+
* A caller with no console to write to — an MCP tool answering over stdio, where
|
|
2041
|
+
* the server's stderr never reaches the agent — can hand this in and put what
|
|
2042
|
+
* was reported into its own result, so a diagnostic about the files it just read
|
|
2043
|
+
* is something the agent can act on rather than something it never hears.
|
|
2044
|
+
*/
|
|
2045
|
+
var WarningCollectingLogger = class extends ConsoleLogger {
|
|
2046
|
+
warnings = new WarningCollection();
|
|
2047
|
+
get reportsWhileSilent() {
|
|
2048
|
+
return true;
|
|
2049
|
+
}
|
|
2050
|
+
warn(message, ...args) {
|
|
2051
|
+
this.warnings.add({
|
|
2052
|
+
message,
|
|
2053
|
+
args
|
|
2054
|
+
});
|
|
2055
|
+
super.warn(message, ...args);
|
|
2056
|
+
}
|
|
2057
|
+
getWarnings() {
|
|
2058
|
+
return this.warnings.toArray();
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
1562
2061
|
//#endregion
|
|
1563
2062
|
//#region src/utils/validation.ts
|
|
1564
2063
|
/**
|
|
@@ -4565,6 +5064,17 @@ function parseStrict(content) {
|
|
|
4565
5064
|
return result;
|
|
4566
5065
|
}
|
|
4567
5066
|
/**
|
|
5067
|
+
* The error a source document should fail with when
|
|
5068
|
+
* {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
|
|
5069
|
+
*
|
|
5070
|
+
* Shared by every source that reports them, so the three files a user can
|
|
5071
|
+
* author explain the same removal the same way rather than each inventing its
|
|
5072
|
+
* own wording.
|
|
5073
|
+
*/
|
|
5074
|
+
function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
|
|
5075
|
+
return /* @__PURE__ */ new Error(`${JSON.stringify(sourcePath)} uses ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")} as ${droppedKeys.length === 1 ? "a key" : "keys"}. Rulesync removes __proto__, constructor and prototype from every source document it parses, because assigning them would reach the prototype chain instead of the object. They are therefore never written to any tool's config — rename them rather than leaving entries that silently do nothing.`);
|
|
5076
|
+
}
|
|
5077
|
+
/**
|
|
4568
5078
|
* The same parse as {@link parseJsonc}, additionally reporting which
|
|
4569
5079
|
* prototype-pollution keys were removed, as dotted paths
|
|
4570
5080
|
* (`permission.bash.__proto__`).
|
|
@@ -4644,9 +5154,17 @@ async function resolveRulesyncSourceWritePath({ outputRoot, paths }) {
|
|
|
4644
5154
|
//#region src/features/hooks/rulesync-hooks.ts
|
|
4645
5155
|
var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
4646
5156
|
json;
|
|
5157
|
+
/**
|
|
5158
|
+
* Prototype-pollution keys the parser removed. They are dropped before the
|
|
5159
|
+
* schema ever sees them, so without this record a hook keyed `constructor`
|
|
5160
|
+
* would produce neither an error nor an entry in any generated file.
|
|
5161
|
+
*/
|
|
5162
|
+
droppedKeys;
|
|
4647
5163
|
constructor(params) {
|
|
4648
5164
|
super({ ...params });
|
|
4649
|
-
|
|
5165
|
+
const { value, droppedKeys } = parseJsoncReportingDroppedKeys({ content: this.fileContent });
|
|
5166
|
+
this.json = value;
|
|
5167
|
+
this.droppedKeys = droppedKeys;
|
|
4650
5168
|
if (params.validate) {
|
|
4651
5169
|
const result = this.validate();
|
|
4652
5170
|
if (!result.success) throw result.error;
|
|
@@ -4665,6 +5183,13 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
4665
5183
|
};
|
|
4666
5184
|
}
|
|
4667
5185
|
validate() {
|
|
5186
|
+
if (this.droppedKeys.length > 0) return {
|
|
5187
|
+
success: false,
|
|
5188
|
+
error: droppedPollutionKeysError({
|
|
5189
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
5190
|
+
droppedKeys: this.droppedKeys
|
|
5191
|
+
})
|
|
5192
|
+
};
|
|
4668
5193
|
const result = HooksConfigSchema.safeParse(this.json);
|
|
4669
5194
|
if (!result.success) return {
|
|
4670
5195
|
success: false,
|
|
@@ -5011,9 +5536,17 @@ function mergeMcpJsonOverlays({ base, overlay }) {
|
|
|
5011
5536
|
}
|
|
5012
5537
|
var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
5013
5538
|
json;
|
|
5539
|
+
/**
|
|
5540
|
+
* Prototype-pollution keys the parser removed. They are dropped before the
|
|
5541
|
+
* schema ever sees them, so without this record a server named `__proto__`
|
|
5542
|
+
* would produce neither an error nor an entry in any generated file.
|
|
5543
|
+
*/
|
|
5544
|
+
droppedKeys;
|
|
5014
5545
|
constructor(params) {
|
|
5015
5546
|
super(params);
|
|
5016
|
-
|
|
5547
|
+
const { value, droppedKeys } = parseJsoncReportingDroppedKeys({ content: this.fileContent });
|
|
5548
|
+
this.json = value;
|
|
5549
|
+
this.droppedKeys = droppedKeys;
|
|
5017
5550
|
if (params.validate) {
|
|
5018
5551
|
const result = this.validate();
|
|
5019
5552
|
if (!result.success) throw result.error;
|
|
@@ -5035,6 +5568,13 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5035
5568
|
};
|
|
5036
5569
|
}
|
|
5037
5570
|
validate() {
|
|
5571
|
+
if (this.droppedKeys.length > 0) return {
|
|
5572
|
+
success: false,
|
|
5573
|
+
error: droppedPollutionKeysError({
|
|
5574
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
5575
|
+
droppedKeys: this.droppedKeys
|
|
5576
|
+
})
|
|
5577
|
+
};
|
|
5038
5578
|
const result = RulesyncMcpFileSchema.safeParse(this.json);
|
|
5039
5579
|
if (!result.success) return {
|
|
5040
5580
|
success: false,
|
|
@@ -5104,16 +5644,23 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5104
5644
|
}
|
|
5105
5645
|
const fileContent = await readFileContent(filePath);
|
|
5106
5646
|
let parsed;
|
|
5647
|
+
let droppedKeys;
|
|
5107
5648
|
try {
|
|
5108
|
-
|
|
5109
|
-
if (!isRecord$1(
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
if (!result.success) throw result.error;
|
|
5113
|
-
}
|
|
5649
|
+
const result = parseJsoncReportingDroppedKeys({ content: fileContent });
|
|
5650
|
+
if (!isRecord$1(result.value)) throw new Error("Expected a JSON object.");
|
|
5651
|
+
parsed = result.value;
|
|
5652
|
+
droppedKeys = result.droppedKeys;
|
|
5114
5653
|
} catch (error) {
|
|
5115
5654
|
throw new Error(`Invalid MCP source file '${filePath}': ${formatError(error)}`, { cause: error });
|
|
5116
5655
|
}
|
|
5656
|
+
if (validate) {
|
|
5657
|
+
if (droppedKeys.length > 0) throw droppedPollutionKeysError({
|
|
5658
|
+
sourcePath: toPosixPath((0, node_path.relative)(process.cwd(), filePath)),
|
|
5659
|
+
droppedKeys
|
|
5660
|
+
});
|
|
5661
|
+
const result = RulesyncMcpFileSchema.safeParse(parsed);
|
|
5662
|
+
if (!result.success) throw new Error(`Invalid MCP source file '${filePath}': ${formatError(result.error)}`, { cause: result.error });
|
|
5663
|
+
}
|
|
5117
5664
|
rootSources.push({
|
|
5118
5665
|
record: parsed,
|
|
5119
5666
|
outputRoot: parent,
|
|
@@ -5359,12 +5906,13 @@ const PermissionActionSchema = zod_mini.z.enum([
|
|
|
5359
5906
|
"deny"
|
|
5360
5907
|
]);
|
|
5361
5908
|
/**
|
|
5362
|
-
* Whether a
|
|
5363
|
-
*
|
|
5364
|
-
* filter removes can never
|
|
5909
|
+
* Whether a key in a permission block — a category name or a pattern — is
|
|
5910
|
+
* blank, that is empty or only whitespace. Shared with the import-side filter
|
|
5911
|
+
* so the key the schema rejects and the key that filter removes can never
|
|
5912
|
+
* drift apart.
|
|
5365
5913
|
*/
|
|
5366
|
-
function
|
|
5367
|
-
return
|
|
5914
|
+
function isBlankPermissionKey(key) {
|
|
5915
|
+
return key.trim().length === 0;
|
|
5368
5916
|
}
|
|
5369
5917
|
/**
|
|
5370
5918
|
* A single permission pattern key.
|
|
@@ -5376,7 +5924,18 @@ function isBlankPermissionPattern(pattern) {
|
|
|
5376
5924
|
* silently ignores it. Rather than let each target decide, reject it here so
|
|
5377
5925
|
* the mistake surfaces once, on the source file.
|
|
5378
5926
|
*/
|
|
5379
|
-
const PermissionPatternSchema = zod_mini.z.string().check(zod_mini.z.refine((pattern) => !
|
|
5927
|
+
const PermissionPatternSchema = zod_mini.z.string().check(zod_mini.z.refine((pattern) => !isBlankPermissionKey(pattern), { message: "Permission pattern must not be blank" }));
|
|
5928
|
+
/**
|
|
5929
|
+
* A permission category key: the name of the tool surface a rules map applies
|
|
5930
|
+
* to (`bash`, `edit`, `webfetch`, ...).
|
|
5931
|
+
*
|
|
5932
|
+
* Blank is rejected for the same reason a blank pattern is, one step up. Every
|
|
5933
|
+
* translator reads categories by name, so `{"": {"git *": "allow"}}` reaches no
|
|
5934
|
+
* tool at all and the rules under it are silently dead — the mistake is only
|
|
5935
|
+
* visible as an entry missing from a generated config. Rejecting it here
|
|
5936
|
+
* surfaces it on the source file instead.
|
|
5937
|
+
*/
|
|
5938
|
+
const PermissionCategorySchema = zod_mini.z.string().check(zod_mini.z.refine((category) => !isBlankPermissionKey(category), { message: "Permission category must not be blank" }));
|
|
5380
5939
|
/**
|
|
5381
5940
|
* Permission rules for a single tool category.
|
|
5382
5941
|
* Keys are glob patterns matching tool input (commands, file paths, etc.).
|
|
@@ -5397,7 +5956,7 @@ const PermissionRulesSchema = zod_mini.z.record(PermissionPatternSchema, Permiss
|
|
|
5397
5956
|
* @example
|
|
5398
5957
|
* { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
|
|
5399
5958
|
*/
|
|
5400
|
-
const ToolScopedPermissionSchema = zod_mini.z.record(
|
|
5959
|
+
const ToolScopedPermissionSchema = zod_mini.z.record(PermissionCategorySchema, PermissionRulesSchema);
|
|
5401
5960
|
/**
|
|
5402
5961
|
* Generic tool-scoped override block for tools that have no tool-specific
|
|
5403
5962
|
* override keys of their own; it carries only the canonical tool-scoped
|
|
@@ -5455,7 +6014,7 @@ const OpencodeOverridePermissionValueSchema = zod_mini.z.union([PermissionAction
|
|
|
5455
6014
|
* @example
|
|
5456
6015
|
* { "permission": { "external_directory": "deny", "webfetch": "allow" } }
|
|
5457
6016
|
*/
|
|
5458
|
-
const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(
|
|
6017
|
+
const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(PermissionCategorySchema, OpencodeOverridePermissionValueSchema)) });
|
|
5459
6018
|
/**
|
|
5460
6019
|
* Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
|
|
5461
6020
|
* into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
|
|
@@ -5515,7 +6074,7 @@ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
5515
6074
|
* @see https://kilo.ai/docs/getting-started/settings/sandboxing
|
|
5516
6075
|
*/
|
|
5517
6076
|
const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
5518
|
-
permission: zod_mini.z.optional(zod_mini.z.record(
|
|
6077
|
+
permission: zod_mini.z.optional(zod_mini.z.record(PermissionCategorySchema, OpencodeOverridePermissionValueSchema)),
|
|
5519
6078
|
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
5520
6079
|
});
|
|
5521
6080
|
/**
|
|
@@ -5586,7 +6145,7 @@ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
5586
6145
|
* { "enabled_tools": ["bash", "read_file", "grep"] }
|
|
5587
6146
|
*/
|
|
5588
6147
|
const VibePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
5589
|
-
permission: zod_mini.z.optional(zod_mini.z.record(
|
|
6148
|
+
permission: zod_mini.z.optional(zod_mini.z.record(PermissionCategorySchema, zod_mini.z.looseObject({ sensitive_patterns: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))),
|
|
5590
6149
|
enabled_tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
5591
6150
|
});
|
|
5592
6151
|
/**
|
|
@@ -6320,12 +6879,12 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6320
6879
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
6321
6880
|
* Values are pattern-to-action mappings for that tool category.
|
|
6322
6881
|
*
|
|
6323
|
-
* The optional
|
|
6324
|
-
* `
|
|
6325
|
-
* `
|
|
6326
|
-
*
|
|
6327
|
-
*
|
|
6328
|
-
*
|
|
6882
|
+
* The optional tool keys below are tool-scoped overrides consumed only by their
|
|
6883
|
+
* respective translator (see the matching `*PermissionsOverrideSchema`); every
|
|
6884
|
+
* other tool reads the shared `permission` block and ignores them. The set of
|
|
6885
|
+
* keys is exactly `permissionsProcessorToolTargetTuple` mapped through
|
|
6886
|
+
* `PERMISSION_OVERRIDE_KEY_ALIASES` — a test asserts that, so this comment does
|
|
6887
|
+
* not enumerate them and go stale.
|
|
6329
6888
|
*
|
|
6330
6889
|
* Additionally, every permissions-capable tool accepts a canonical tool-scoped
|
|
6331
6890
|
* `permission` block under its override key (`{toolname}.permission`, same
|
|
@@ -6343,7 +6902,7 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6343
6902
|
* }
|
|
6344
6903
|
*/
|
|
6345
6904
|
const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
6346
|
-
permission: zod_mini.z.record(
|
|
6905
|
+
permission: zod_mini.z.record(PermissionCategorySchema, PermissionRulesSchema),
|
|
6347
6906
|
opencode: zod_mini.z.optional(OpencodePermissionsOverrideSchema),
|
|
6348
6907
|
hermes: zod_mini.z.optional(HermesPermissionsOverrideSchema),
|
|
6349
6908
|
cline: zod_mini.z.optional(ClinePermissionsOverrideSchema),
|
|
@@ -6372,7 +6931,9 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
6372
6931
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
6373
6932
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
6374
6933
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
6375
|
-
|
|
6934
|
+
roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
6935
|
+
rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
6936
|
+
zoocode: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
|
|
6376
6937
|
});
|
|
6377
6938
|
/**
|
|
6378
6939
|
* Full permissions file schema including optional $schema field.
|
|
@@ -6414,10 +6975,40 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
6414
6975
|
}]
|
|
6415
6976
|
};
|
|
6416
6977
|
}
|
|
6978
|
+
/**
|
|
6979
|
+
* The canonical document an importer produces from a tool's own config.
|
|
6980
|
+
*
|
|
6981
|
+
* Every importer has to run the blank-key filter over what it is about to
|
|
6982
|
+
* write: the canonical schema rejects a blank pattern and a blank category
|
|
6983
|
+
* outright, so a source file carrying either would be refused by the very
|
|
6984
|
+
* next `generate` — and that refusal takes the whole file with it, so one
|
|
6985
|
+
* blank key imported from one tool stops every tool's permissions from being
|
|
6986
|
+
* generated. Building the imported document here rather than calling the
|
|
6987
|
+
* filter beside each `new RulesyncPermissions(...)` is what keeps the next
|
|
6988
|
+
* importer from forgetting it.
|
|
6989
|
+
*
|
|
6990
|
+
* `sourcePath` is the tool config being read, used only to name it if
|
|
6991
|
+
* something is dropped.
|
|
6992
|
+
*/
|
|
6993
|
+
static fromImportedFileContent({ outputRoot, fileContent, sourcePath, logger }) {
|
|
6994
|
+
return new RulesyncPermissions({
|
|
6995
|
+
outputRoot,
|
|
6996
|
+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
6997
|
+
relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
|
|
6998
|
+
fileContent: withoutBlankPermissionKeys({
|
|
6999
|
+
fileContent,
|
|
7000
|
+
sourcePath,
|
|
7001
|
+
logger
|
|
7002
|
+
})
|
|
7003
|
+
});
|
|
7004
|
+
}
|
|
6417
7005
|
validate() {
|
|
6418
7006
|
if (this.droppedKeys.length > 0) return {
|
|
6419
7007
|
success: false,
|
|
6420
|
-
error:
|
|
7008
|
+
error: droppedPollutionKeysError({
|
|
7009
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
7010
|
+
droppedKeys: this.droppedKeys
|
|
7011
|
+
})
|
|
6421
7012
|
};
|
|
6422
7013
|
const result = RulesyncPermissionsFileSchema.safeParse(this.json);
|
|
6423
7014
|
if (!result.success) return {
|
|
@@ -6489,33 +7080,62 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
6489
7080
|
}
|
|
6490
7081
|
};
|
|
6491
7082
|
/**
|
|
6492
|
-
*
|
|
6493
|
-
*
|
|
7083
|
+
* Tool-scoped override keys whose `permission` block maps a category to
|
|
7084
|
+
* something other than a pattern map. Vibe alone keeps
|
|
7085
|
+
* `{ sensitive_patterns: [...] }` objects there, so the keys one level down are
|
|
7086
|
+
* field names and the blank-pattern filter must not walk them. The category
|
|
7087
|
+
* names above them are still category names, and are filtered like any other.
|
|
7088
|
+
*
|
|
7089
|
+
* Typed as `ToolTarget` so a renamed target fails to compile here rather than
|
|
7090
|
+
* silently stopping to match, which would let the filter start deleting Vibe's
|
|
7091
|
+
* fields and reporting them as removed permission patterns. That only holds for
|
|
7092
|
+
* targets whose override key is the target name itself: a target that aliases
|
|
7093
|
+
* to another key (see `PERMISSION_OVERRIDE_KEY_ALIASES`) would have to be listed
|
|
7094
|
+
* under the alias, which this type would reject. None of them is non-pattern-map
|
|
7095
|
+
* today, so add that spelling only when one becomes so.
|
|
7096
|
+
*/
|
|
7097
|
+
const NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["vibe"]);
|
|
7098
|
+
/**
|
|
7099
|
+
* Strip every blank key — category or pattern — from an already-parsed
|
|
7100
|
+
* canonical document, reporting how many were dropped from each block.
|
|
6494
7101
|
*
|
|
6495
7102
|
* Both the shared `permission` block and every tool-scoped
|
|
6496
7103
|
* `{toolname}.permission` block are walked, because import produces both:
|
|
6497
7104
|
* OpenCode and Kilo route their tool-only categories into the tool-scoped block
|
|
6498
|
-
* verbatim, so a blank
|
|
6499
|
-
* whose value is not a rules map
|
|
6500
|
-
* the tool-native shapes intact — OpenCode's and Kilo's bare action
|
|
6501
|
-
* (`"external_directory": "deny"`) have no pattern key to inspect,
|
|
6502
|
-
*
|
|
6503
|
-
*
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
7105
|
+
* verbatim, so a blank key in the user's own config lands there. A category
|
|
7106
|
+
* whose value is not a rules map keeps its value exactly as it is, which is what
|
|
7107
|
+
* keeps the tool-native shapes intact — OpenCode's and Kilo's bare action
|
|
7108
|
+
* strings (`"external_directory": "deny"`) have no pattern key to inspect, and
|
|
7109
|
+
* Kilo's `sandbox` is not a `permission` block at all.
|
|
7110
|
+
*
|
|
7111
|
+
* Categories are filtered for the same reason patterns are, one level up: the
|
|
7112
|
+
* canonical schema rejects a blank category, so reproducing one would write a
|
|
7113
|
+
* source file the very next `generate` refuses — and it would refuse the whole
|
|
7114
|
+
* file, taking every tool's permissions generation down with it.
|
|
7115
|
+
*
|
|
7116
|
+
* The patterns inside {@link NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS} blocks
|
|
7117
|
+
* are left alone: they are field names rather than patterns there. Their
|
|
7118
|
+
* category names are still filtered.
|
|
7119
|
+
*/
|
|
7120
|
+
function stripBlankPermissionKeys(config) {
|
|
7121
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
7122
|
+
const categories = /* @__PURE__ */ new Map();
|
|
7123
|
+
const filterBlock = ({ block, blockPath, filterPatterns }) => {
|
|
6508
7124
|
const filtered = {};
|
|
6509
7125
|
for (const [category, rules] of Object.entries(block)) {
|
|
6510
|
-
if (
|
|
7126
|
+
if (isBlankPermissionKey(category)) {
|
|
7127
|
+
categories.set(blockPath, (categories.get(blockPath) ?? 0) + 1);
|
|
7128
|
+
continue;
|
|
7129
|
+
}
|
|
7130
|
+
if (!filterPatterns || !isRecord$1(rules)) {
|
|
6511
7131
|
filtered[category] = rules;
|
|
6512
7132
|
continue;
|
|
6513
7133
|
}
|
|
6514
7134
|
const kept = {};
|
|
6515
7135
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
6516
|
-
if (
|
|
7136
|
+
if (isBlankPermissionKey(pattern)) {
|
|
6517
7137
|
const path = `${blockPath}.${category}`;
|
|
6518
|
-
|
|
7138
|
+
patterns.set(path, (patterns.get(path) ?? 0) + 1);
|
|
6519
7139
|
continue;
|
|
6520
7140
|
}
|
|
6521
7141
|
kept[pattern] = action;
|
|
@@ -6528,25 +7148,41 @@ function stripBlankPermissionPatterns(config) {
|
|
|
6528
7148
|
const next = { ...config };
|
|
6529
7149
|
if (isRecord$1(config.permission)) next.permission = filterBlock({
|
|
6530
7150
|
block: config.permission,
|
|
6531
|
-
blockPath: "permission"
|
|
7151
|
+
blockPath: "permission",
|
|
7152
|
+
filterPatterns: true
|
|
6532
7153
|
});
|
|
6533
7154
|
for (const [key, value] of Object.entries(config)) {
|
|
6534
7155
|
if (key === "permission" || !isRecord$1(value) || !isRecord$1(value.permission)) continue;
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
7156
|
+
const permission = filterBlock({
|
|
7157
|
+
block: value.permission,
|
|
7158
|
+
blockPath: `${key}.permission`,
|
|
7159
|
+
filterPatterns: !NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS.has(key)
|
|
7160
|
+
});
|
|
7161
|
+
if (!(Object.keys(permission).length === 0 && Object.keys(value.permission).length > 0)) {
|
|
7162
|
+
next[key] = {
|
|
7163
|
+
...value,
|
|
7164
|
+
permission
|
|
7165
|
+
};
|
|
7166
|
+
continue;
|
|
7167
|
+
}
|
|
7168
|
+
const { permission: _emptied, ...rest } = value;
|
|
7169
|
+
if (Object.keys(rest).length === 0) {
|
|
7170
|
+
delete next[key];
|
|
7171
|
+
continue;
|
|
7172
|
+
}
|
|
7173
|
+
next[key] = rest;
|
|
6542
7174
|
}
|
|
6543
7175
|
return {
|
|
6544
7176
|
config: next,
|
|
6545
|
-
removed
|
|
7177
|
+
removed: {
|
|
7178
|
+
patterns,
|
|
7179
|
+
categories
|
|
7180
|
+
}
|
|
6546
7181
|
};
|
|
6547
7182
|
}
|
|
7183
|
+
const summarizeDroppedCounts = (counts) => [...counts.entries()].map(([path, count]) => `${count} in ${JSON.stringify(path)}`).join(", ");
|
|
6548
7184
|
/**
|
|
6549
|
-
* Report the dropped
|
|
7185
|
+
* Report the dropped keys.
|
|
6550
7186
|
*
|
|
6551
7187
|
* Dropping an entry silently is the failure mode a permissions source must not
|
|
6552
7188
|
* have. A blanket blank pattern can read as "deny everything by default";
|
|
@@ -6557,27 +7193,36 @@ function stripBlankPermissionPatterns(config) {
|
|
|
6557
7193
|
* `logger` is optional because the import direction (`toRulesyncPermissions`)
|
|
6558
7194
|
* takes no logger parameter; the shared `fallbackLogger` is configured from the
|
|
6559
7195
|
* CLI flags and the resolved config, so `silent` is still honored.
|
|
7196
|
+
*
|
|
7197
|
+
* `sourcePath` names the tool config the keys came out of. A single import run
|
|
7198
|
+
* reads many tools, and the block paths alone (`permission.bash`) are the same
|
|
7199
|
+
* for all of them, so without it the user is told something was dropped but not
|
|
7200
|
+
* from where.
|
|
6560
7201
|
*/
|
|
6561
|
-
function
|
|
6562
|
-
|
|
7202
|
+
function warnAboutDroppedKeys({ removed, sourcePath, logger }) {
|
|
7203
|
+
const source = sourcePath === void 0 ? "a tool's permission configuration" : JSON.stringify(sourcePath);
|
|
7204
|
+
if (removed.patterns.size > 0) warnWithFallback(logger, `Dropped blank permission patterns while reading ${source} (${summarizeDroppedCounts(removed.patterns)}). An empty or whitespace-only pattern matches everything, and tools disagree on what it means — some apply it to every command, others ignore it entirely — so it is not carried into the rulesync permissions config. If one of them was a blanket deny, the imported configuration now allows more than the file it came from; re-add it with a real pattern.`);
|
|
7205
|
+
if (removed.categories.size > 0) warnWithFallback(logger, `Dropped blank permission categories while reading ${source} (${summarizeDroppedCounts(removed.categories)}). A category name is how every tool finds the rules underneath it, so an empty or whitespace-only one reaches no tool at all and the rules below it were never going to be generated. Carrying one into the rulesync permissions config would make the next generate refuse the whole file, so it is removed here; re-add those rules under a real category name.`);
|
|
6563
7206
|
}
|
|
6564
7207
|
/**
|
|
6565
|
-
* Drop blank permission
|
|
7208
|
+
* Drop blank permission keys from a canonical document produced by import.
|
|
6566
7209
|
*
|
|
6567
|
-
* The canonical schema rejects a blank pattern
|
|
6568
|
-
* has
|
|
6569
|
-
* pattern (Roo Code, for instance, keeps only
|
|
6570
|
-
* `cmd.trim().length > 0`). Reproducing
|
|
6571
|
-
* would therefore write a source file that the
|
|
6572
|
-
*
|
|
7210
|
+
* The canonical schema rejects a blank pattern and a blank category outright,
|
|
7211
|
+
* and every tool that has a blank pattern in its own config already treats it as
|
|
7212
|
+
* something other than a real pattern (Roo Code, for instance, keeps only
|
|
7213
|
+
* entries passing `cmd.trim().length > 0`). Reproducing either in
|
|
7214
|
+
* `.rulesync/permissions.jsonc` would therefore write a source file that the
|
|
7215
|
+
* very next `generate` refuses — the whole file, not just that entry — so they
|
|
7216
|
+
* are removed here instead, and reported.
|
|
6573
7217
|
*/
|
|
6574
|
-
function
|
|
7218
|
+
function withoutBlankPermissionKeys({ fileContent, sourcePath, logger }) {
|
|
6575
7219
|
const parsed = parseJsonc$8(fileContent);
|
|
6576
7220
|
if (!isRecord$1(parsed)) return fileContent;
|
|
6577
|
-
const { config, removed } =
|
|
6578
|
-
if (removed.size === 0) return fileContent;
|
|
6579
|
-
|
|
7221
|
+
const { config, removed } = stripBlankPermissionKeys(parsed);
|
|
7222
|
+
if (removed.patterns.size === 0 && removed.categories.size === 0) return fileContent;
|
|
7223
|
+
warnAboutDroppedKeys({
|
|
6580
7224
|
removed,
|
|
7225
|
+
sourcePath,
|
|
6581
7226
|
logger
|
|
6582
7227
|
});
|
|
6583
7228
|
return JSON.stringify(config, null, 2);
|
|
@@ -6586,14 +7231,15 @@ function withoutBlankPermissionPatterns({ fileContent, logger }) {
|
|
|
6586
7231
|
* The same filter over an already-parsed document, for callers that validate a
|
|
6587
7232
|
* canonical block before it is ever serialized. Hermes Agent stores its
|
|
6588
7233
|
* rulesync provenance inside its own config and parses it back on import; left
|
|
6589
|
-
* unfiltered, one blank pattern would fail `safeParse` and discard
|
|
6590
|
-
* provenance block without a word.
|
|
7234
|
+
* unfiltered, one blank pattern or category would fail `safeParse` and discard
|
|
7235
|
+
* the entire provenance block without a word.
|
|
6591
7236
|
*/
|
|
6592
|
-
function
|
|
6593
|
-
const { config: filtered, removed } =
|
|
6594
|
-
if (removed.size === 0) return config;
|
|
6595
|
-
|
|
7237
|
+
function withoutBlankPermissionKeysIn({ config, sourcePath, logger }) {
|
|
7238
|
+
const { config: filtered, removed } = stripBlankPermissionKeys(config);
|
|
7239
|
+
if (removed.patterns.size === 0 && removed.categories.size === 0) return config;
|
|
7240
|
+
warnAboutDroppedKeys({
|
|
6596
7241
|
removed,
|
|
7242
|
+
sourcePath,
|
|
6597
7243
|
logger
|
|
6598
7244
|
});
|
|
6599
7245
|
return filtered;
|
|
@@ -6742,6 +7388,24 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
6742
7388
|
//#endregion
|
|
6743
7389
|
//#region src/types/ai-dir.ts
|
|
6744
7390
|
/**
|
|
7391
|
+
* Whether `name` is a path rather than a single name.
|
|
7392
|
+
*
|
|
7393
|
+
* Both separators are rejected on every platform, which is why neither
|
|
7394
|
+
* `path.sep` nor the platform is consulted: a backslash is a legal character in
|
|
7395
|
+
* a POSIX name, but a name carrying one is a path the moment it reaches
|
|
7396
|
+
* Windows, and `AiDir` names travel between the two — most tools take one
|
|
7397
|
+
* straight from a skill's frontmatter.
|
|
7398
|
+
*
|
|
7399
|
+
* Exported because the checks that run *before* a name reaches `AiDir` — so
|
|
7400
|
+
* that an unusable name is reported and skipped rather than thrown over — have
|
|
7401
|
+
* to reject exactly the set this guard does. Two spellings of one rule drift
|
|
7402
|
+
* apart the moment either is tightened, and the pre-filter drifting narrower
|
|
7403
|
+
* turns a reported name back into a failed run.
|
|
7404
|
+
*/
|
|
7405
|
+
function containsPathSeparator(name) {
|
|
7406
|
+
return name.includes("/") || name.includes("\\");
|
|
7407
|
+
}
|
|
7408
|
+
/**
|
|
6745
7409
|
* Directories that hold credentials. Excluding these protects something, so
|
|
6746
7410
|
* their exclusion is reported rather than silent.
|
|
6747
7411
|
*/
|
|
@@ -7201,7 +7865,7 @@ var AiDir = class AiDir {
|
|
|
7201
7865
|
*/
|
|
7202
7866
|
global;
|
|
7203
7867
|
constructor({ outputRoot = process.cwd(), relativeDirPath, dirName, mainFile, otherFiles = [], global = false }) {
|
|
7204
|
-
if (
|
|
7868
|
+
if (containsPathSeparator(dirName)) throw new Error(`Directory name cannot contain path separators: dirName="${dirName}"`);
|
|
7205
7869
|
if (dirName === "" || dirName === "." || dirName === "..") throw new Error(`Directory name cannot be empty, ".", or "..": dirName="${dirName}"`);
|
|
7206
7870
|
this.outputRoot = outputRoot;
|
|
7207
7871
|
this.relativeDirPath = relativeDirPath;
|
|
@@ -7262,7 +7926,7 @@ var AiDir = class AiDir {
|
|
|
7262
7926
|
const mainFile = this.getMainFile();
|
|
7263
7927
|
if (mainFile === void 0) return;
|
|
7264
7928
|
const name = mainFile.name;
|
|
7265
|
-
if (name === "" || name === "." || name === ".." ||
|
|
7929
|
+
if (name === "" || name === "." || name === ".." || containsPathSeparator(name)) return;
|
|
7266
7930
|
return node_path.default.join(this.getDirPath(), name);
|
|
7267
7931
|
}
|
|
7268
7932
|
getDirPath() {
|
|
@@ -7593,7 +8257,7 @@ var AiDir = class AiDir {
|
|
|
7593
8257
|
* differ from the path the run claims, which turns it into an orphan.
|
|
7594
8258
|
*/
|
|
7595
8259
|
function isUnsafeSkillDirName(name) {
|
|
7596
|
-
return name === "" || name === "." || name === ".." ||
|
|
8260
|
+
return name === "" || name === "." || name === ".." || containsPathSeparator(name) || name.endsWith(".") || name.endsWith(" ");
|
|
7597
8261
|
}
|
|
7598
8262
|
const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
7599
8263
|
name: zod_mini.z.string().check(zod_mini.z.refine((name) => !isUnsafeSkillDirName(name), { message: "name is used as a directory name: it may not be empty, \".\" or \"..\", contain a path separator, or end with a dot or a space" })),
|
|
@@ -7853,6 +8517,16 @@ const RulesyncSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
7853
8517
|
zoocode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7854
8518
|
/** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
|
|
7855
8519
|
allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
|
|
8520
|
+
zcode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
8521
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
8522
|
+
thoughtLevel: zod_mini.z.optional(zod_mini.z.string()),
|
|
8523
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
8524
|
+
tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8525
|
+
disallowedTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
8526
|
+
maxTurns: zod_mini.z.optional(zod_mini.z.number().check(zod_mini.z.int(), zod_mini.z.positive())),
|
|
8527
|
+
injectAgentsMd: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8528
|
+
mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
8529
|
+
})),
|
|
7856
8530
|
vibe: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7857
8531
|
agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
|
|
7858
8532
|
display_name: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -7940,10 +8614,12 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
7940
8614
|
* lets a directory be *created* with a backslash in its name, so such a
|
|
7941
8615
|
* directory can sit in a skills root that nothing here can name — it is
|
|
7942
8616
|
* reported rather than passed on, since the alternative is a candidate built
|
|
7943
|
-
* from a name that belongs to no directory at all.
|
|
8617
|
+
* from a name that belongs to no directory at all. The test is `AiDir`'s own,
|
|
8618
|
+
* so this pre-filter cannot come to accept a name the guard behind it throws
|
|
8619
|
+
* over.
|
|
7944
8620
|
*/
|
|
7945
8621
|
function isAddressableSkillName(name) {
|
|
7946
|
-
return !
|
|
8622
|
+
return !containsPathSeparator(name);
|
|
7947
8623
|
}
|
|
7948
8624
|
/**
|
|
7949
8625
|
* The names of the skill directories directly under `skillsRoot`.
|
|
@@ -9334,18 +10010,168 @@ function splitCheckFile({ fileContent, fallbackName }) {
|
|
|
9334
10010
|
});
|
|
9335
10011
|
}
|
|
9336
10012
|
//#endregion
|
|
10013
|
+
//#region src/features/checks/aggregated-tool-check.ts
|
|
10014
|
+
/**
|
|
10015
|
+
* Shared skeleton for the checks adapters whose output is a single aggregated
|
|
10016
|
+
* instruction file — one file whose sections are the marked blocks
|
|
10017
|
+
* `aggregated-check-file.ts` renders and splits, rather than a directory with a
|
|
10018
|
+
* file per check.
|
|
10019
|
+
*
|
|
10020
|
+
* That module already held the rendering and the splitting; what is here is the
|
|
10021
|
+
* adapter around it, which was near-verbatim in Cursor Bugbot, Rovo Dev and
|
|
10022
|
+
* Factory Droid. A subclass supplies {@link ToolCheck.getSettablePaths} and
|
|
10023
|
+
* {@link getAggregatedCheckConfig}, and inherits the rest — so a fix to how
|
|
10024
|
+
* these files are read or written lands once instead of three times.
|
|
10025
|
+
*
|
|
10026
|
+
* `fromRulesyncCheck` is refused here rather than implemented: sections share
|
|
10027
|
+
* one file, so an output cannot be produced from one check in isolation and the
|
|
10028
|
+
* processor calls {@link fromRulesyncChecks} instead.
|
|
10029
|
+
*
|
|
10030
|
+
* Not declared `abstract`, even though nothing instantiates it directly: the
|
|
10031
|
+
* statics below build the subclass with `new this(...)`, which an abstract
|
|
10032
|
+
* constructor type forbids. What a subclass owes is enforced the way the rest
|
|
10033
|
+
* of this codebase enforces it on a static — {@link getAggregatedCheckConfig}
|
|
10034
|
+
* throws until it is overridden.
|
|
10035
|
+
*/
|
|
10036
|
+
var AggregatedToolCheck = class extends ToolCheck {
|
|
10037
|
+
/**
|
|
10038
|
+
* The per-tool values the shared skeleton reads. Thrown rather than abstract
|
|
10039
|
+
* because TypeScript has no abstract statics; a subclass that forgets it
|
|
10040
|
+
* fails on its first use rather than silently taking a default.
|
|
10041
|
+
*/
|
|
10042
|
+
static getAggregatedCheckConfig() {
|
|
10043
|
+
throw new Error("Please implement this method in the subclass.");
|
|
10044
|
+
}
|
|
10045
|
+
/**
|
|
10046
|
+
* The settable paths with the file name required. An aggregated adapter names
|
|
10047
|
+
* the one file it writes — that is what keeps consumers which would otherwise
|
|
10048
|
+
* claim the whole tool directory, the gitignore derivation among them,
|
|
10049
|
+
* narrowed to it — so a missing name is a mistake in the subclass rather than
|
|
10050
|
+
* a case to fall back for.
|
|
10051
|
+
*/
|
|
10052
|
+
static getAggregatedPaths({ global = false } = {}) {
|
|
10053
|
+
const paths = this.getSettablePaths({ global });
|
|
10054
|
+
if (!paths.relativeFilePath) throw new Error(`${this.name} writes one aggregated file, so getSettablePaths must name it.`);
|
|
10055
|
+
return {
|
|
10056
|
+
relativeDirPath: paths.relativeDirPath,
|
|
10057
|
+
relativeFilePath: paths.relativeFilePath
|
|
10058
|
+
};
|
|
10059
|
+
}
|
|
10060
|
+
static getAggregatedFilePath({ outputRoot, global = false }) {
|
|
10061
|
+
const paths = this.getAggregatedPaths({ global });
|
|
10062
|
+
return {
|
|
10063
|
+
...paths,
|
|
10064
|
+
filePath: (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)
|
|
10065
|
+
};
|
|
10066
|
+
}
|
|
10067
|
+
static isTargetedByRulesyncCheck(rulesyncCheck) {
|
|
10068
|
+
return this.isTargetedByRulesyncCheckDefault({
|
|
10069
|
+
rulesyncCheck,
|
|
10070
|
+
toolTarget: this.getAggregatedCheckConfig().toolTarget
|
|
10071
|
+
});
|
|
10072
|
+
}
|
|
10073
|
+
/**
|
|
10074
|
+
* Ownership guard the processor consults before it deletes anything for this
|
|
10075
|
+
* tool. Every one of these paths is one a user may well have written by hand,
|
|
10076
|
+
* whether because the tool documents it as the place to write review
|
|
10077
|
+
* instructions or because it is shared with something else the tool loads, so
|
|
10078
|
+
* anything in it that rulesync did not write is not rulesync's to remove — dropping the last check targeting the tool must not
|
|
10079
|
+
* take somebody's review instructions with it. Deletion is therefore allowed
|
|
10080
|
+
* only for a file that is nothing but generated sections: one that carries no
|
|
10081
|
+
* marker at all, or that carries hand-written text ahead of the first marker,
|
|
10082
|
+
* stays.
|
|
10083
|
+
*/
|
|
10084
|
+
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
10085
|
+
const { filePath } = this.getAggregatedFilePath({ outputRoot });
|
|
10086
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
10087
|
+
if (fileContent === null) return true;
|
|
10088
|
+
return isOnlyGeneratedSections(fileContent);
|
|
10089
|
+
}
|
|
10090
|
+
static fromRulesyncCheck(_params) {
|
|
10091
|
+
const { displayName } = this.getAggregatedCheckConfig();
|
|
10092
|
+
throw new Error(`${displayName} checks are built from all checks at once; use fromRulesyncChecks.`);
|
|
10093
|
+
}
|
|
10094
|
+
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
10095
|
+
if (rulesyncChecks.length === 0) return [];
|
|
10096
|
+
const config = this.getAggregatedCheckConfig();
|
|
10097
|
+
const { relativeDirPath, relativeFilePath, filePath } = this.getAggregatedFilePath({
|
|
10098
|
+
outputRoot,
|
|
10099
|
+
global
|
|
10100
|
+
});
|
|
10101
|
+
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) {
|
|
10102
|
+
if (config.handWrittenPreamble === "skip") {
|
|
10103
|
+
logger?.warn(config.handWrittenWarning({
|
|
10104
|
+
filePath,
|
|
10105
|
+
displayName: config.displayName,
|
|
10106
|
+
toolTarget: config.toolTarget
|
|
10107
|
+
}));
|
|
10108
|
+
return [];
|
|
10109
|
+
}
|
|
10110
|
+
logger?.warn(`${config.displayName} checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets ${config.toolTarget} --features checks\` first to keep them.`);
|
|
10111
|
+
}
|
|
10112
|
+
return [new this({
|
|
10113
|
+
outputRoot,
|
|
10114
|
+
relativeDirPath,
|
|
10115
|
+
relativeFilePath,
|
|
10116
|
+
fileContent: renderCheckFile(rulesyncChecks),
|
|
10117
|
+
global
|
|
10118
|
+
})];
|
|
10119
|
+
}
|
|
10120
|
+
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
10121
|
+
const { relativeDirPath, relativeFilePath, filePath } = this.getAggregatedFilePath({
|
|
10122
|
+
outputRoot,
|
|
10123
|
+
global
|
|
10124
|
+
});
|
|
10125
|
+
return new this({
|
|
10126
|
+
outputRoot,
|
|
10127
|
+
relativeDirPath,
|
|
10128
|
+
relativeFilePath,
|
|
10129
|
+
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
10130
|
+
global
|
|
10131
|
+
});
|
|
10132
|
+
}
|
|
10133
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
10134
|
+
return new this({
|
|
10135
|
+
outputRoot,
|
|
10136
|
+
relativeDirPath,
|
|
10137
|
+
relativeFilePath,
|
|
10138
|
+
fileContent: "",
|
|
10139
|
+
validate: false,
|
|
10140
|
+
global
|
|
10141
|
+
});
|
|
10142
|
+
}
|
|
10143
|
+
validate() {
|
|
10144
|
+
return {
|
|
10145
|
+
success: true,
|
|
10146
|
+
error: null
|
|
10147
|
+
};
|
|
10148
|
+
}
|
|
10149
|
+
toRulesyncCheck() {
|
|
10150
|
+
const first = this.toRulesyncChecks()[0];
|
|
10151
|
+
if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
10152
|
+
return first;
|
|
10153
|
+
}
|
|
10154
|
+
toRulesyncChecks() {
|
|
10155
|
+
const config = this.constructor.getAggregatedCheckConfig();
|
|
10156
|
+
const fileContent = this.getFileContent();
|
|
10157
|
+
return splitCheckFile({
|
|
10158
|
+
fileContent: config.transformImportedContent?.(fileContent) ?? fileContent,
|
|
10159
|
+
fallbackName: config.fallbackCheckName
|
|
10160
|
+
});
|
|
10161
|
+
}
|
|
10162
|
+
};
|
|
10163
|
+
//#endregion
|
|
9337
10164
|
//#region src/features/checks/cursor-check.ts
|
|
9338
|
-
const FALLBACK_CHECK_NAME$2 = "bugbot";
|
|
9339
10165
|
/**
|
|
9340
10166
|
* Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
|
|
9341
10167
|
*
|
|
9342
10168
|
* Bugbot takes one aggregated instruction file per directory rather than a file
|
|
9343
10169
|
* per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
|
|
9344
|
-
* the repository-root `.cursor/BUGBOT.md` — hence
|
|
9345
|
-
*
|
|
9346
|
-
* an HTML-comment marker carrying the
|
|
9347
|
-
*
|
|
9348
|
-
* body is empty).
|
|
10170
|
+
* the repository-root `.cursor/BUGBOT.md` — hence `fromRulesyncChecks` rather
|
|
10171
|
+
* than the usual per-check conversion, which {@link AggregatedToolCheck}
|
|
10172
|
+
* provides. Each check becomes one section: an HTML-comment marker carrying the
|
|
10173
|
+
* check name, an `## <name>` heading, and the check body as the instruction
|
|
10174
|
+
* text (the `description` is used when the body is empty).
|
|
9349
10175
|
*
|
|
9350
10176
|
* Bugbot reads the file as free prose, so a check's `severity` and `tools` have
|
|
9351
10177
|
* no equivalent there: they are not written and do not come back on import. So
|
|
@@ -9361,97 +10187,26 @@ const FALLBACK_CHECK_NAME$2 = "bugbot";
|
|
|
9361
10187
|
* before the first marker — and a hand-written file with no markers at all —
|
|
9362
10188
|
* becomes a single `bugbot` check, so nothing in the file is dropped. A file
|
|
9363
10189
|
* holding anything rulesync did not write is never deleted either (see
|
|
9364
|
-
*
|
|
9365
|
-
* replace it — import first to keep what is there, which is warned about.
|
|
10190
|
+
* `canDeleteAuxiliaryFiles` on the base), though generating checks for Cursor
|
|
10191
|
+
* does replace it — import first to keep what is there, which is warned about.
|
|
9366
10192
|
*
|
|
9367
10193
|
* @see https://cursor.com/docs/bugbot
|
|
9368
10194
|
*/
|
|
9369
|
-
var CursorCheck = class
|
|
10195
|
+
var CursorCheck = class extends AggregatedToolCheck {
|
|
9370
10196
|
static getSettablePaths(_options = {}) {
|
|
9371
10197
|
return {
|
|
9372
10198
|
relativeDirPath: CURSOR_DIR,
|
|
9373
10199
|
relativeFilePath: CURSOR_BUGBOT_FILE_NAME
|
|
9374
10200
|
};
|
|
9375
10201
|
}
|
|
9376
|
-
static
|
|
9377
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9378
|
-
rulesyncCheck,
|
|
9379
|
-
toolTarget: "cursor"
|
|
9380
|
-
});
|
|
9381
|
-
}
|
|
9382
|
-
/**
|
|
9383
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9384
|
-
* tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
|
|
9385
|
-
* to hand-write, so anything in it that rulesync did not write is not
|
|
9386
|
-
* rulesync's to remove — dropping the last check targeting Cursor must not
|
|
9387
|
-
* take somebody's hand-written review instructions with it. Deletion is
|
|
9388
|
-
* therefore allowed only for a file that is nothing but generated sections:
|
|
9389
|
-
* one that carries no marker at all, or that carries hand-written text ahead
|
|
9390
|
-
* of the first marker, stays.
|
|
9391
|
-
*/
|
|
9392
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9393
|
-
const paths = CursorCheck.getSettablePaths();
|
|
9394
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
|
|
9395
|
-
if (fileContent === null) return true;
|
|
9396
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9397
|
-
}
|
|
9398
|
-
static fromRulesyncCheck(_params) {
|
|
9399
|
-
throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9400
|
-
}
|
|
9401
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9402
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9403
|
-
const paths = CursorCheck.getSettablePaths({ global });
|
|
9404
|
-
const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
|
|
9405
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9406
|
-
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
|
|
9407
|
-
const fileContent = renderCheckFile(rulesyncChecks);
|
|
9408
|
-
return [new CursorCheck({
|
|
9409
|
-
outputRoot,
|
|
9410
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9411
|
-
relativeFilePath,
|
|
9412
|
-
fileContent,
|
|
9413
|
-
global
|
|
9414
|
-
})];
|
|
9415
|
-
}
|
|
9416
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9417
|
-
const paths = CursorCheck.getSettablePaths({ global });
|
|
9418
|
-
const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
|
|
9419
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9420
|
-
return new CursorCheck({
|
|
9421
|
-
outputRoot,
|
|
9422
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9423
|
-
relativeFilePath,
|
|
9424
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9425
|
-
global
|
|
9426
|
-
});
|
|
9427
|
-
}
|
|
9428
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9429
|
-
return new CursorCheck({
|
|
9430
|
-
outputRoot,
|
|
9431
|
-
relativeDirPath,
|
|
9432
|
-
relativeFilePath,
|
|
9433
|
-
fileContent: "",
|
|
9434
|
-
validate: false,
|
|
9435
|
-
global
|
|
9436
|
-
});
|
|
9437
|
-
}
|
|
9438
|
-
validate() {
|
|
10202
|
+
static getAggregatedCheckConfig() {
|
|
9439
10203
|
return {
|
|
9440
|
-
|
|
9441
|
-
|
|
10204
|
+
displayName: "Cursor",
|
|
10205
|
+
toolTarget: "cursor",
|
|
10206
|
+
fallbackCheckName: "bugbot",
|
|
10207
|
+
handWrittenPreamble: "replace"
|
|
9442
10208
|
};
|
|
9443
10209
|
}
|
|
9444
|
-
toRulesyncCheck() {
|
|
9445
|
-
const first = this.toRulesyncChecks()[0];
|
|
9446
|
-
if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
9447
|
-
return first;
|
|
9448
|
-
}
|
|
9449
|
-
toRulesyncChecks() {
|
|
9450
|
-
return splitCheckFile({
|
|
9451
|
-
fileContent: this.getFileContent(),
|
|
9452
|
-
fallbackName: FALLBACK_CHECK_NAME$2
|
|
9453
|
-
});
|
|
9454
|
-
}
|
|
9455
10210
|
};
|
|
9456
10211
|
//#endregion
|
|
9457
10212
|
//#region src/constants/factorydroid-paths.ts
|
|
@@ -9488,7 +10243,6 @@ const FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME = "review-guidelines";
|
|
|
9488
10243
|
const FACTORYDROID_REVIEW_GUIDELINES_DIR_PATH = (0, node_path.join)(FACTORYDROID_SKILLS_DIR_PATH, FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME);
|
|
9489
10244
|
//#endregion
|
|
9490
10245
|
//#region src/features/checks/factorydroid-check.ts
|
|
9491
|
-
const FALLBACK_CHECK_NAME$1 = FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME;
|
|
9492
10246
|
/**
|
|
9493
10247
|
* Drop the YAML frontmatter of a hand-authored `review-guidelines` skill before
|
|
9494
10248
|
* the file is split into checks.
|
|
@@ -9523,6 +10277,9 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9523
10277
|
content = rest.slice(closing.index + closing[0].length).replace(LEADING_BLANK_LINE_PATTERN, "");
|
|
9524
10278
|
}
|
|
9525
10279
|
}
|
|
10280
|
+
function handWrittenWarning({ filePath, displayName, toolTarget }) {
|
|
10281
|
+
return `${displayName} checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for ${displayName}. Run \`rulesync import --targets ${toolTarget} --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there; delete it outright if you no longer want it, or rename the directory if it is an ordinary skill rather than review guidelines. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone. A rulesync skill named \`${FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME}\` is no longer generated here — Factory's reviewer reads this path, so the checks feature owns it — but a directory an older rulesync wrote can still be sitting there.`;
|
|
10282
|
+
}
|
|
9526
10283
|
/**
|
|
9527
10284
|
* Checks adapter for Factory Droid's code-review guidelines
|
|
9528
10285
|
* (`.factory/skills/review-guidelines/SKILL.md`).
|
|
@@ -9531,9 +10288,10 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9531
10288
|
* "repository-specific review guidelines" from a skill named
|
|
9532
10289
|
* `review-guidelines` and injects them into every review run. That makes the
|
|
9533
10290
|
* output a single aggregated file like Cursor Bugbot's and Rovo Dev's, so every
|
|
9534
|
-
* `.rulesync/checks/*.md` targeting Factory Droid collapses into it via
|
|
9535
|
-
* {@link
|
|
9536
|
-
* `aggregated-check-file.ts` for the marker convention the
|
|
10291
|
+
* `.rulesync/checks/*.md` targeting Factory Droid collapses into it via the
|
|
10292
|
+
* `fromRulesyncChecks` on {@link AggregatedToolCheck}, each check written as a
|
|
10293
|
+
* marked section (see `aggregated-check-file.ts` for the marker convention the
|
|
10294
|
+
* three aggregated adapters share).
|
|
9537
10295
|
*
|
|
9538
10296
|
* The file is plain Markdown with no frontmatter, matching Factory's documented
|
|
9539
10297
|
* example. Frontmatter would also be self-defeating here: `renderCheckFile`
|
|
@@ -9551,8 +10309,9 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9551
10309
|
* The output lives inside the same `.factory/skills/` tree the `skills` feature
|
|
9552
10310
|
* writes, so a user-authored `review-guidelines` skill collides with it. The
|
|
9553
10311
|
* path has one owner rather than a merge rule, and the owner is this feature:
|
|
9554
|
-
*
|
|
9555
|
-
*
|
|
10312
|
+
* generating leaves a file holding anything rulesync did not write untouched —
|
|
10313
|
+
* the `skip` policy below — and the base's `canDeleteAuxiliaryFiles` refuses to
|
|
10314
|
+
* remove one.
|
|
9556
10315
|
* Their content is somebody's own writing and rulesync cannot reconstruct it,
|
|
9557
10316
|
* so neither direction guesses. That is stricter than Cursor Bugbot's
|
|
9558
10317
|
* replace-and-warn, and deliberately: `.cursor/BUGBOT.md` is a path only the
|
|
@@ -9562,92 +10321,23 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9562
10321
|
*
|
|
9563
10322
|
* @see https://docs.factory.ai/software-factory/code-review-ci
|
|
9564
10323
|
*/
|
|
9565
|
-
var FactorydroidCheck = class
|
|
10324
|
+
var FactorydroidCheck = class extends AggregatedToolCheck {
|
|
9566
10325
|
static getSettablePaths(_options = {}) {
|
|
9567
10326
|
return {
|
|
9568
10327
|
relativeDirPath: FACTORYDROID_REVIEW_GUIDELINES_DIR_PATH,
|
|
9569
10328
|
relativeFilePath: SKILL_FILE_NAME
|
|
9570
10329
|
};
|
|
9571
10330
|
}
|
|
9572
|
-
static
|
|
9573
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9574
|
-
rulesyncCheck,
|
|
9575
|
-
toolTarget: "factorydroid"
|
|
9576
|
-
});
|
|
9577
|
-
}
|
|
9578
|
-
/**
|
|
9579
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9580
|
-
* tool. `review-guidelines` is an ordinary skill directory a user may have
|
|
9581
|
-
* authored by hand, so dropping the last check targeting Factory Droid must
|
|
9582
|
-
* not take their review instructions with it. Deletion is allowed only for a
|
|
9583
|
-
* file that is nothing but generated sections.
|
|
9584
|
-
*/
|
|
9585
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9586
|
-
const paths = FactorydroidCheck.getSettablePaths();
|
|
9587
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "SKILL.md"));
|
|
9588
|
-
if (fileContent === null) return true;
|
|
9589
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9590
|
-
}
|
|
9591
|
-
static fromRulesyncCheck(_params) {
|
|
9592
|
-
throw new Error("Factory Droid checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9593
|
-
}
|
|
9594
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9595
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9596
|
-
const paths = FactorydroidCheck.getSettablePaths({ global });
|
|
9597
|
-
const relativeFilePath = paths.relativeFilePath ?? "SKILL.md";
|
|
9598
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9599
|
-
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) {
|
|
9600
|
-
logger?.warn(`Factory Droid checks: ${filePath} holds instructions rulesync did not write, so it is left as it is and no checks were generated for Factory Droid. Run \`rulesync import --targets factorydroid --features checks\` to bring them into \`.rulesync/checks/\` and then delete the file, so the next generate writes it back from there; delete it outright if you no longer want it, or rename the directory if it is an ordinary skill rather than review guidelines. Importing alone leaves this file as it is, so it keeps blocking generation until it is gone. A rulesync skill named \`${FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME}\` is no longer generated here — Factory's reviewer reads this path, so the checks feature owns it — but a directory an older rulesync wrote can still be sitting there.`);
|
|
9601
|
-
return [];
|
|
9602
|
-
}
|
|
9603
|
-
const fileContent = renderCheckFile(rulesyncChecks);
|
|
9604
|
-
return [new FactorydroidCheck({
|
|
9605
|
-
outputRoot,
|
|
9606
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9607
|
-
relativeFilePath,
|
|
9608
|
-
fileContent,
|
|
9609
|
-
global
|
|
9610
|
-
})];
|
|
9611
|
-
}
|
|
9612
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9613
|
-
const paths = FactorydroidCheck.getSettablePaths({ global });
|
|
9614
|
-
const relativeFilePath = paths.relativeFilePath ?? "SKILL.md";
|
|
9615
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9616
|
-
return new FactorydroidCheck({
|
|
9617
|
-
outputRoot,
|
|
9618
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9619
|
-
relativeFilePath,
|
|
9620
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9621
|
-
global
|
|
9622
|
-
});
|
|
9623
|
-
}
|
|
9624
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9625
|
-
return new FactorydroidCheck({
|
|
9626
|
-
outputRoot,
|
|
9627
|
-
relativeDirPath,
|
|
9628
|
-
relativeFilePath,
|
|
9629
|
-
fileContent: "",
|
|
9630
|
-
validate: false,
|
|
9631
|
-
global
|
|
9632
|
-
});
|
|
9633
|
-
}
|
|
9634
|
-
validate() {
|
|
10331
|
+
static getAggregatedCheckConfig() {
|
|
9635
10332
|
return {
|
|
9636
|
-
|
|
9637
|
-
|
|
10333
|
+
displayName: "Factory Droid",
|
|
10334
|
+
toolTarget: "factorydroid",
|
|
10335
|
+
fallbackCheckName: FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME,
|
|
10336
|
+
handWrittenPreamble: "skip",
|
|
10337
|
+
handWrittenWarning,
|
|
10338
|
+
transformImportedContent: stripSkillFrontmatter
|
|
9638
10339
|
};
|
|
9639
10340
|
}
|
|
9640
|
-
toRulesyncCheck() {
|
|
9641
|
-
const first = this.toRulesyncChecks()[0];
|
|
9642
|
-
if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
9643
|
-
return first;
|
|
9644
|
-
}
|
|
9645
|
-
toRulesyncChecks() {
|
|
9646
|
-
return splitCheckFile({
|
|
9647
|
-
fileContent: stripSkillFrontmatter(this.getFileContent()),
|
|
9648
|
-
fallbackName: FALLBACK_CHECK_NAME$1
|
|
9649
|
-
});
|
|
9650
|
-
}
|
|
9651
10341
|
};
|
|
9652
10342
|
//#endregion
|
|
9653
10343
|
//#region src/constants/hermesagent-paths.ts
|
|
@@ -9906,7 +10596,6 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
9906
10596
|
};
|
|
9907
10597
|
//#endregion
|
|
9908
10598
|
//#region src/features/checks/rovodev-check.ts
|
|
9909
|
-
const FALLBACK_CHECK_NAME = "review-agent";
|
|
9910
10599
|
/**
|
|
9911
10600
|
* Checks adapter for Rovo Dev CLI's code-review custom instructions
|
|
9912
10601
|
* (`.rovodev/.review-agent.md`).
|
|
@@ -9915,9 +10604,9 @@ const FALLBACK_CHECK_NAME = "review-agent";
|
|
|
9915
10604
|
* `.rovodev/` folder — no frontmatter, and note the leading dot in the file
|
|
9916
10605
|
* name. Like Cursor Bugbot it is a single aggregated file rather than a file
|
|
9917
10606
|
* per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
|
|
9918
|
-
* it via {@link
|
|
9919
|
-
* section (see `aggregated-check-file.ts` for the
|
|
9920
|
-
* adapters share).
|
|
10607
|
+
* it via the `fromRulesyncChecks` on {@link AggregatedToolCheck}, with each
|
|
10608
|
+
* check written as a marked section (see `aggregated-check-file.ts` for the
|
|
10609
|
+
* marker convention the three aggregated adapters share).
|
|
9921
10610
|
*
|
|
9922
10611
|
* Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
|
|
9923
10612
|
* have no equivalent there: they are not written and do not come back on
|
|
@@ -9929,88 +10618,21 @@ const FALLBACK_CHECK_NAME = "review-agent";
|
|
|
9929
10618
|
*
|
|
9930
10619
|
* @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
|
|
9931
10620
|
*/
|
|
9932
|
-
var RovodevCheck = class
|
|
10621
|
+
var RovodevCheck = class extends AggregatedToolCheck {
|
|
9933
10622
|
static getSettablePaths(_options = {}) {
|
|
9934
10623
|
return {
|
|
9935
10624
|
relativeDirPath: ROVODEV_DIR,
|
|
9936
10625
|
relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
|
|
9937
10626
|
};
|
|
9938
10627
|
}
|
|
9939
|
-
static
|
|
9940
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9941
|
-
rulesyncCheck,
|
|
9942
|
-
toolTarget: "rovodev"
|
|
9943
|
-
});
|
|
9944
|
-
}
|
|
9945
|
-
/**
|
|
9946
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9947
|
-
* tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
|
|
9948
|
-
* to hand-write, so anything in it that rulesync did not write is not
|
|
9949
|
-
* rulesync's to remove — dropping the last check targeting Rovo Dev must not
|
|
9950
|
-
* take somebody's hand-written review instructions with it.
|
|
9951
|
-
*/
|
|
9952
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9953
|
-
const paths = RovodevCheck.getSettablePaths();
|
|
9954
|
-
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
|
|
9955
|
-
if (fileContent === null) return true;
|
|
9956
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9957
|
-
}
|
|
9958
|
-
static fromRulesyncCheck(_params) {
|
|
9959
|
-
throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9960
|
-
}
|
|
9961
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9962
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9963
|
-
const paths = RovodevCheck.getSettablePaths({ global });
|
|
9964
|
-
const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
|
|
9965
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9966
|
-
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Rovo Dev checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets rovodev --features checks\` first to keep them.`);
|
|
9967
|
-
return [new RovodevCheck({
|
|
9968
|
-
outputRoot,
|
|
9969
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9970
|
-
relativeFilePath,
|
|
9971
|
-
fileContent: renderCheckFile(rulesyncChecks),
|
|
9972
|
-
global
|
|
9973
|
-
})];
|
|
9974
|
-
}
|
|
9975
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9976
|
-
const paths = RovodevCheck.getSettablePaths({ global });
|
|
9977
|
-
const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
|
|
9978
|
-
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9979
|
-
return new RovodevCheck({
|
|
9980
|
-
outputRoot,
|
|
9981
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9982
|
-
relativeFilePath,
|
|
9983
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9984
|
-
global
|
|
9985
|
-
});
|
|
9986
|
-
}
|
|
9987
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9988
|
-
return new RovodevCheck({
|
|
9989
|
-
outputRoot,
|
|
9990
|
-
relativeDirPath,
|
|
9991
|
-
relativeFilePath,
|
|
9992
|
-
fileContent: "",
|
|
9993
|
-
validate: false,
|
|
9994
|
-
global
|
|
9995
|
-
});
|
|
9996
|
-
}
|
|
9997
|
-
validate() {
|
|
10628
|
+
static getAggregatedCheckConfig() {
|
|
9998
10629
|
return {
|
|
9999
|
-
|
|
10000
|
-
|
|
10630
|
+
displayName: "Rovo Dev",
|
|
10631
|
+
toolTarget: "rovodev",
|
|
10632
|
+
fallbackCheckName: "review-agent",
|
|
10633
|
+
handWrittenPreamble: "replace"
|
|
10001
10634
|
};
|
|
10002
10635
|
}
|
|
10003
|
-
toRulesyncCheck() {
|
|
10004
|
-
const first = this.toRulesyncChecks()[0];
|
|
10005
|
-
if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
10006
|
-
return first;
|
|
10007
|
-
}
|
|
10008
|
-
toRulesyncChecks() {
|
|
10009
|
-
return splitCheckFile({
|
|
10010
|
-
fileContent: this.getFileContent(),
|
|
10011
|
-
fallbackName: FALLBACK_CHECK_NAME
|
|
10012
|
-
});
|
|
10013
|
-
}
|
|
10014
10636
|
};
|
|
10015
10637
|
//#endregion
|
|
10016
10638
|
//#region src/constants/codexcli-paths.ts
|
|
@@ -11218,7 +11840,7 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
11218
11840
|
this.logger.debug(`Rulesync checks directory not found: ${checksDir}`);
|
|
11219
11841
|
return [];
|
|
11220
11842
|
}
|
|
11221
|
-
const mdFiles = (await
|
|
11843
|
+
const mdFiles = (await listDirectoryEntryNames(checksDir)).filter((file) => file.endsWith(".md"));
|
|
11222
11844
|
if (mdFiles.length === 0) {
|
|
11223
11845
|
this.logger.debug(`No markdown files found in rulesync checks directory: ${checksDir}`);
|
|
11224
11846
|
return [];
|
|
@@ -16040,6 +16662,7 @@ const ZCODE_SKILLS_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "skills");
|
|
|
16040
16662
|
const ZCODE_CONFIG_FILE_NAME = "config.json";
|
|
16041
16663
|
const ZCODE_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "cli");
|
|
16042
16664
|
const ZCODE_MCP_SERVERS_KEY = "servers";
|
|
16665
|
+
const ZCODE_AGENTS_DIR_PATH = (0, node_path.join)(ZCODE_DIR, "agents");
|
|
16043
16666
|
//#endregion
|
|
16044
16667
|
//#region src/features/commands/zcode-command.ts
|
|
16045
16668
|
/**
|
|
@@ -17002,6 +17625,52 @@ function compact(obj) {
|
|
|
17002
17625
|
return result;
|
|
17003
17626
|
}
|
|
17004
17627
|
//#endregion
|
|
17628
|
+
//#region src/utils/quote-value.ts
|
|
17629
|
+
/**
|
|
17630
|
+
* How much of a value read off disk a diagnostic quotes.
|
|
17631
|
+
*
|
|
17632
|
+
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
17633
|
+
* offending value so the reader can find it, but the values these warnings
|
|
17634
|
+
* quote come from files rulesync did not write — a tool's own settings, a
|
|
17635
|
+
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
17636
|
+
* no longer stop at a terminal: they travel into a `--json` document another
|
|
17637
|
+
* program parses and into an MCP result an agent reads as context. A command
|
|
17638
|
+
* line or a header is the shape most likely to carry a credential, and a long
|
|
17639
|
+
* value is the shape most likely to carry instructions aimed at the agent.
|
|
17640
|
+
*/
|
|
17641
|
+
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
17642
|
+
/**
|
|
17643
|
+
* A short, quotable rendering of a value for a diagnostic.
|
|
17644
|
+
*
|
|
17645
|
+
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
17646
|
+
* crafted one read as a second line; stripped of the control characters
|
|
17647
|
+
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
17648
|
+
* bidirectional overrides); and truncated.
|
|
17649
|
+
*/
|
|
17650
|
+
function quoteValueForWarning(value) {
|
|
17651
|
+
return truncateText({
|
|
17652
|
+
text: stripControlCharacters(serialize(value)),
|
|
17653
|
+
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
17654
|
+
suffix: "…(truncated)"
|
|
17655
|
+
});
|
|
17656
|
+
}
|
|
17657
|
+
function serialize(value) {
|
|
17658
|
+
try {
|
|
17659
|
+
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
17660
|
+
} catch {
|
|
17661
|
+
return `[unserializable ${typeof value}]`;
|
|
17662
|
+
}
|
|
17663
|
+
}
|
|
17664
|
+
/**
|
|
17665
|
+
* Strip the control characters out of every string before `JSON.stringify`
|
|
17666
|
+
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
17667
|
+
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
17668
|
+
* over the output can recognize as a control character again.
|
|
17669
|
+
*/
|
|
17670
|
+
function stripStrings(_key, value) {
|
|
17671
|
+
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
17672
|
+
}
|
|
17673
|
+
//#endregion
|
|
17005
17674
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
17006
17675
|
function isToolMatcherEntry(x) {
|
|
17007
17676
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -17103,7 +17772,7 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
|
|
|
17103
17772
|
if (!isValid({
|
|
17104
17773
|
value,
|
|
17105
17774
|
canonical
|
|
17106
|
-
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${
|
|
17775
|
+
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${quoteValueForWarning(value)} is not a value this tool can express as "${tool}".`);
|
|
17107
17776
|
}
|
|
17108
17777
|
return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
|
|
17109
17778
|
commandOnly,
|
|
@@ -17198,7 +17867,7 @@ function describeScalarConstraint({ canonical, value }) {
|
|
|
17198
17867
|
if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
|
|
17199
17868
|
return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
|
|
17200
17869
|
}
|
|
17201
|
-
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${
|
|
17870
|
+
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${quoteValueForWarning(value)}) while importing a hook: ${describeScalarConstraint({
|
|
17202
17871
|
canonical,
|
|
17203
17872
|
value
|
|
17204
17873
|
})} Importing it would fail validation on the next run.`;
|
|
@@ -17225,7 +17894,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
17225
17894
|
if (first === void 0) continue;
|
|
17226
17895
|
const firstStable = stableJson(first);
|
|
17227
17896
|
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
17228
|
-
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${
|
|
17897
|
+
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${quoteValueForWarning(first)} — including any that asked for something else, or for nothing.`);
|
|
17229
17898
|
emitted[tool] = first;
|
|
17230
17899
|
}
|
|
17231
17900
|
return emitted;
|
|
@@ -17712,7 +18381,7 @@ function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
|
17712
18381
|
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
17713
18382
|
const value = entry[tool];
|
|
17714
18383
|
if (subdividesGroup !== true || value === void 0) continue;
|
|
17715
|
-
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${
|
|
18384
|
+
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${quoteValueForWarning(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
|
|
17716
18385
|
}
|
|
17717
18386
|
}
|
|
17718
18387
|
/**
|
|
@@ -17729,7 +18398,7 @@ function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
|
17729
18398
|
value,
|
|
17730
18399
|
canonical: field
|
|
17731
18400
|
})) continue;
|
|
17732
|
-
return `Skipping a hook while importing: its "${field}" (${
|
|
18401
|
+
return `Skipping a hook while importing: its "${field}" (${quoteValueForWarning(value)}) is unusable — ${describeScalarConstraint({
|
|
17733
18402
|
canonical: field,
|
|
17734
18403
|
value
|
|
17735
18404
|
})} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
|
|
@@ -18051,7 +18720,22 @@ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseF
|
|
|
18051
18720
|
}
|
|
18052
18721
|
/** Quotes a name read off disk, the way every other such name is logged. */
|
|
18053
18722
|
function quoteKey(key) {
|
|
18054
|
-
return
|
|
18723
|
+
return quoteValueForWarning(key);
|
|
18724
|
+
}
|
|
18725
|
+
/**
|
|
18726
|
+
* How many keys the warning names before it stops counting.
|
|
18727
|
+
*
|
|
18728
|
+
* The keys come from a file rulesync did not write, and the warning now travels
|
|
18729
|
+
* into `--json` documents and MCP results as well as onto a console. A settings
|
|
18730
|
+
* file with hundreds of top-level keys is unusual but not impossible, and the
|
|
18731
|
+
* point of the sentence is to make the reader open the file — naming the first
|
|
18732
|
+
* few does that as well as naming all of them.
|
|
18733
|
+
*/
|
|
18734
|
+
const MAX_LISTED_KEYS = 20;
|
|
18735
|
+
function listKeys(keys) {
|
|
18736
|
+
const named = keys.slice(0, MAX_LISTED_KEYS).map(quoteKey).join(", ");
|
|
18737
|
+
const rest = keys.length - MAX_LISTED_KEYS;
|
|
18738
|
+
return rest > 0 ? `${named} and ${rest} more` : named;
|
|
18055
18739
|
}
|
|
18056
18740
|
/**
|
|
18057
18741
|
* Name the settings the machine-local file contributed, so nobody publishes one
|
|
@@ -18071,8 +18755,8 @@ function warnAboutLocalKeys({ localParsed, configPath, toolLabel, sensitiveKeys,
|
|
|
18071
18755
|
const keys = Object.keys(localParsed);
|
|
18072
18756
|
if (keys.length === 0) return;
|
|
18073
18757
|
const flagged = keys.filter((key) => sensitiveKeys.includes(key));
|
|
18074
|
-
const guardrailSentence = flagged.length === 0 ? "" : ` ${flagged
|
|
18075
|
-
warnOnceWithFallback(logger, `${toolLabel}: ${configPath} is a machine-local overrides file, and importing read ${keys
|
|
18758
|
+
const guardrailSentence = flagged.length === 0 ? "" : ` ${listKeys(flagged)} ${flagged.length === 1 ? "decides" : "decide"} what ${toolLabel} is allowed to do, so a value meant for one machine would become the team's guardrail.`;
|
|
18759
|
+
warnOnceWithFallback(logger, `${toolLabel}: ${configPath} is a machine-local overrides file, and importing read ${listKeys(keys)} from it. Whatever an import takes from there lands in files rulesync commits, so check the imported files and remove anything personal to this machine before sharing them.${guardrailSentence}`);
|
|
18076
18760
|
}
|
|
18077
18761
|
//#endregion
|
|
18078
18762
|
//#region src/utils/augmentcode-settings.ts
|
|
@@ -22767,6 +23451,9 @@ function withToolTargetPrefix({ logger, toolTarget }) {
|
|
|
22767
23451
|
get silent() {
|
|
22768
23452
|
return logger.silent;
|
|
22769
23453
|
},
|
|
23454
|
+
get reportsWhileSilent() {
|
|
23455
|
+
return logger.reportsWhileSilent;
|
|
23456
|
+
},
|
|
22770
23457
|
get jsonMode() {
|
|
22771
23458
|
return logger.jsonMode;
|
|
22772
23459
|
},
|
|
@@ -28881,7 +29568,7 @@ function convertFromMusecodeFormat(musecodeMcp) {
|
|
|
28881
29568
|
if (key === "mode") {
|
|
28882
29569
|
const mode = asMusecodeMode(value);
|
|
28883
29570
|
if (mode === void 0) {
|
|
28884
|
-
warnWithFallback(void 0, `Muse Code MCP: dropping mode ${
|
|
29571
|
+
warnWithFallback(void 0, `Muse Code MCP: dropping mode ${quoteValueForWarning(value)} on server ${quoteValueForWarning(name)} because it is neither "required" nor "optional", the only two modes Muse Code documents.`);
|
|
28885
29572
|
continue;
|
|
28886
29573
|
}
|
|
28887
29574
|
converted.musecodeMode = mode;
|
|
@@ -30085,7 +30772,7 @@ function pointerLabels(global) {
|
|
|
30085
30772
|
async function warnAtDocumentedDefault({ existing, outputRoot, logger }) {
|
|
30086
30773
|
const { pointer, configLabel, mcpLabel } = pointerLabels(true);
|
|
30087
30774
|
const displaced = await describeDisplacedGlobalServers({ outputRoot });
|
|
30088
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${
|
|
30775
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. That is the default Atlassian's settings reference documents, so it may be Rovo Dev's own value rather than one you chose — and while it stands, the generated ${mcpLabel} is never read. ` + (displaced === null ? `It defines no servers of its own, so setting mcp.mcpConfigPath to "${pointer}" costs nothing.` : `${displaced} — move the servers you want to keep into .rulesync/mcp.jsonc first, or none at all if it turns out to hold nothing you need, then change mcp.mcpConfigPath to "${pointer}".`));
|
|
30089
30776
|
}
|
|
30090
30777
|
/**
|
|
30091
30778
|
* Announce a pointer that was just written. Warned rather than noted in global
|
|
@@ -30142,10 +30829,10 @@ async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outp
|
|
|
30142
30829
|
return false;
|
|
30143
30830
|
}
|
|
30144
30831
|
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
30145
|
-
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${
|
|
30832
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${quoteValueForWarning(existing)}. That names ${mcpLabel} only if Rovo Dev expands environment variables in this setting, which Atlassian does not document — if it does not, the path resolves literally and Rovo Dev reads no MCP servers at all. Write "${pointer}" instead, the form its own documented default uses.`);
|
|
30146
30833
|
return false;
|
|
30147
30834
|
}
|
|
30148
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${
|
|
30835
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${quoteValueForWarning(existing)} in ${configLabel}. Rovo Dev reads MCP servers from that path, so the generated ${mcpLabel} is unused until it is set to "${pointer}".`);
|
|
30149
30836
|
return false;
|
|
30150
30837
|
}
|
|
30151
30838
|
/**
|
|
@@ -31765,11 +32452,10 @@ var ToolPermissions = class extends ToolFile {
|
|
|
31765
32452
|
throw new Error("Please implement this method in the subclass.");
|
|
31766
32453
|
}
|
|
31767
32454
|
toRulesyncPermissionsDefault({ fileContent }) {
|
|
31768
|
-
return
|
|
32455
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
31769
32456
|
outputRoot: this.outputRoot,
|
|
31770
|
-
|
|
31771
|
-
|
|
31772
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent })
|
|
32457
|
+
fileContent,
|
|
32458
|
+
sourcePath: this.getRelativePathFromCwd()
|
|
31773
32459
|
});
|
|
31774
32460
|
}
|
|
31775
32461
|
static async fromFile(_params) {
|
|
@@ -35527,7 +36213,7 @@ function asCursorPermissionEntryArray(value, logger, fieldLabel) {
|
|
|
35527
36213
|
if (!Array.isArray(value)) return [];
|
|
35528
36214
|
const result = [];
|
|
35529
36215
|
for (const item of value) if (typeof item === "string") result.push(item);
|
|
35530
|
-
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${
|
|
36216
|
+
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${quoteValueForWarning(item)}.`);
|
|
35531
36217
|
return result;
|
|
35532
36218
|
}
|
|
35533
36219
|
/**
|
|
@@ -37381,7 +38067,10 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
37381
38067
|
fileContent: this.getFileContent()
|
|
37382
38068
|
});
|
|
37383
38069
|
const rawProvenance = (isRecord$1(config.permissions) ? config.permissions : {}).rulesync;
|
|
37384
|
-
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(isRecord$1(rawProvenance) ?
|
|
38070
|
+
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(isRecord$1(rawProvenance) ? withoutBlankPermissionKeysIn({
|
|
38071
|
+
config: rawProvenance,
|
|
38072
|
+
sourcePath: this.getRelativePathFromCwd()
|
|
38073
|
+
}) : rawProvenance);
|
|
37385
38074
|
const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
|
|
37386
38075
|
const permission = clonePermissionBlock(provenance.permission);
|
|
37387
38076
|
reconcileCommandAllowlist({
|
|
@@ -37409,14 +38098,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
37409
38098
|
permission,
|
|
37410
38099
|
...Object.keys(hermes).length > 0 && { hermes }
|
|
37411
38100
|
};
|
|
37412
|
-
return
|
|
38101
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
37413
38102
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
37414
38103
|
nativeOutputRoot: this.outputRoot,
|
|
37415
38104
|
global: this.global
|
|
37416
38105
|
}),
|
|
37417
|
-
|
|
37418
|
-
|
|
37419
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent: JSON.stringify(imported, null, 2) })
|
|
38106
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
38107
|
+
fileContent: JSON.stringify(imported, null, 2)
|
|
37420
38108
|
});
|
|
37421
38109
|
}
|
|
37422
38110
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
@@ -38317,17 +39005,16 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
38317
39005
|
...nativeRules.length > 0 && { rules: nativeRules },
|
|
38318
39006
|
...tools && { tools }
|
|
38319
39007
|
};
|
|
38320
|
-
return
|
|
39008
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
38321
39009
|
outputRoot: getKimiCodeRulesyncOutputRoot({
|
|
38322
39010
|
nativeOutputRoot: this.outputRoot,
|
|
38323
39011
|
global: this.global
|
|
38324
39012
|
}),
|
|
38325
|
-
|
|
38326
|
-
|
|
38327
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent: JSON.stringify({
|
|
39013
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
39014
|
+
fileContent: JSON.stringify({
|
|
38328
39015
|
permission,
|
|
38329
39016
|
...Object.keys(toolOverride).length > 0 && { "kimi-code": toolOverride }
|
|
38330
|
-
}, null, 2)
|
|
39017
|
+
}, null, 2)
|
|
38331
39018
|
});
|
|
38332
39019
|
}
|
|
38333
39020
|
static forDeletion({ outputRoot = process.cwd() }) {
|
|
@@ -40901,6 +41588,28 @@ function pickSecurityPolicies(source, report) {
|
|
|
40901
41588
|
* Ambiguous-width characters are counted as one column, which is what a
|
|
40902
41589
|
* terminal running a Latin font does.
|
|
40903
41590
|
*
|
|
41591
|
+
* Two of the ranges are here for a narrower reason: a name the skill prompt can
|
|
41592
|
+
* offer may not be counted narrower here than the prompt's own renderer counts
|
|
41593
|
+
* it, or a label that fits the budget wraps anyway and paints the second row
|
|
41594
|
+
* the budget exists to prevent. `@inquirer/core` measures with
|
|
41595
|
+
* `fast-string-width`, which takes the whole of `Script=Hangul` as wide and
|
|
41596
|
+
* every `Emoji_Modifier_Base` as an emoji. So the Hangul jamo are taken to
|
|
41597
|
+
* U+11FF rather than stopping at the leading consonants, and the modifier bases
|
|
41598
|
+
* are named beside the emoji: U+261D, U+26F9 and the two hands of U+270C–U+270D
|
|
41599
|
+
* are `Emoji` without being `Emoji_Presentation`, and were the only characters
|
|
41600
|
+
* outside Hangul this counted at one column while the renderer counted two.
|
|
41601
|
+
*
|
|
41602
|
+
* The rule is over the names that can reach the prompt, which is a smaller set
|
|
41603
|
+
* than the characters that exist. The renderer counts a tab at eight columns and
|
|
41604
|
+
* the Hangul fillers at two, where this counts one and none: a name carrying
|
|
41605
|
+
* either is refused outright by `hasDeceptiveHiddenCharacters` — the tab as a
|
|
41606
|
+
* control character, the fillers as characters that draw as nothing — and never
|
|
41607
|
+
* becomes a row to be measured. The two joiners are the invisible characters
|
|
41608
|
+
* that check lets through, so they are counted below rather than left to the
|
|
41609
|
+
* zero-width rule. Where this is used to lay out text of the tool's own rather
|
|
41610
|
+
* than to bound an untrusted name, the difference is a column of alignment and
|
|
41611
|
+
* not a forged row.
|
|
41612
|
+
*
|
|
40904
41613
|
* The wide planes are taken whole rather than range by range — Tangut, Khitan
|
|
40905
41614
|
* and Nushu together are U+17000–U+18DFF, and the kana supplements are
|
|
40906
41615
|
* U+1AFF0–U+1B2FF — because a gap between two of them is exactly the character
|
|
@@ -40913,7 +41622,7 @@ function pickSecurityPolicies(source, report) {
|
|
|
40913
41622
|
* is canonically the ordinary ideograph U+8C48, and a range that starts there
|
|
40914
41623
|
* instead silently swallows thirty thousand code points that are not wide.
|
|
40915
41624
|
*/
|
|
40916
|
-
const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|[\u2329\u232a\u2630-\u2637\u268a-\u268f\u4dc0-\u4dff]|[\u1100-\
|
|
41625
|
+
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;
|
|
40917
41626
|
/**
|
|
40918
41627
|
* U+FE0F VARIATION SELECTOR-16, which takes no width of its own but asks the
|
|
40919
41628
|
* character before it to be drawn as an emoji — that is, in two columns rather
|
|
@@ -40927,6 +41636,27 @@ const COMBINING_MARK_PATTERN = /[\p{Mn}\p{Me}]/u;
|
|
|
40927
41636
|
/** The characters that take no width at all, marks aside. */
|
|
40928
41637
|
const ZERO_WIDTH_CHARACTERS_PATTERN = /[\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
|
|
40929
41638
|
/**
|
|
41639
|
+
* The zero-width joiner and its non-joining twin, which the renderer spends a
|
|
41640
|
+
* column on apiece.
|
|
41641
|
+
*
|
|
41642
|
+
* A terminal draws neither, and every other character that draws as nothing is
|
|
41643
|
+
* counted at nothing here. These two are the exception because they are the
|
|
41644
|
+
* only invisible characters `hasDeceptiveHiddenCharacters` lets through — a
|
|
41645
|
+
* Persian or Indic name spells a word with one, and an emoji is a chain of them
|
|
41646
|
+
* — so they are the only ones an attacker can put in a name that reaches the
|
|
41647
|
+
* prompt. `fast-string-width`, which is where the prompt's own wrapping is
|
|
41648
|
+
* decided, counts each of them as a column, and 40 of them in a name is 40
|
|
41649
|
+
* columns of budget this would otherwise hand over for free: enough for a name
|
|
41650
|
+
* measured at 39 columns to be drawn at 77 and wrap a forged row underneath
|
|
41651
|
+
* itself.
|
|
41652
|
+
*
|
|
41653
|
+
* The cost is that an emoji built from a chain is overstated by a column per
|
|
41654
|
+
* joiner, on top of the two columns per component it is already overstated by.
|
|
41655
|
+
* Overstating shortens a label that did not need it; understating lets one
|
|
41656
|
+
* wrap.
|
|
41657
|
+
*/
|
|
41658
|
+
const RENDERER_COUNTED_JOINERS = /\u200c|\u200d/u;
|
|
41659
|
+
/**
|
|
40930
41660
|
* How many marks a single character is allowed to carry for free.
|
|
40931
41661
|
*
|
|
40932
41662
|
* A written language stacks two or three at most — a Devanagari vowel sign and
|
|
@@ -40954,6 +41684,7 @@ function widthInContext(params) {
|
|
|
40954
41684
|
const { character, precedingMarks } = params;
|
|
40955
41685
|
if (character === EMOJI_PRESENTATION_SELECTOR) return 1;
|
|
40956
41686
|
if (isCombiningMark(character)) return precedingMarks < FREE_MARKS_PER_CHARACTER ? 0 : 1;
|
|
41687
|
+
if (RENDERER_COUNTED_JOINERS.test(character)) return 1;
|
|
40957
41688
|
if (ZERO_WIDTH_CHARACTERS_PATTERN.test(character)) return 0;
|
|
40958
41689
|
return WIDE_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
40959
41690
|
}
|
|
@@ -40976,6 +41707,8 @@ function displayWidthOf(text) {
|
|
|
40976
41707
|
}
|
|
40977
41708
|
return width;
|
|
40978
41709
|
}
|
|
41710
|
+
/** The mark a cut string ends in. */
|
|
41711
|
+
const SHORTENING_ELLIPSIS = "…";
|
|
40979
41712
|
/**
|
|
40980
41713
|
* Cut `text` down to at most `budget` columns, marking the cut with an ellipsis.
|
|
40981
41714
|
*
|
|
@@ -41001,7 +41734,7 @@ function shortenToWidth(params) {
|
|
|
41001
41734
|
width += characterWidth;
|
|
41002
41735
|
marks = isCombiningMark(character) ? marks + 1 : 0;
|
|
41003
41736
|
}
|
|
41004
|
-
return `${kept.join("")}
|
|
41737
|
+
return `${kept.join("")}${SHORTENING_ELLIPSIS}`;
|
|
41005
41738
|
}
|
|
41006
41739
|
//#endregion
|
|
41007
41740
|
//#region src/features/permissions/vibe-permissions.ts
|
|
@@ -44789,6 +45522,83 @@ const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
|
|
|
44789
45522
|
];
|
|
44790
45523
|
//#endregion
|
|
44791
45524
|
//#region src/features/skills/claudecode-skill.ts
|
|
45525
|
+
/**
|
|
45526
|
+
* The `.claude/skills` tail every scanned root is expected to end with, written
|
|
45527
|
+
* posix-separated so it can be compared against a resolved relative path, and
|
|
45528
|
+
* split into its segments so what sits above it can be taken apart.
|
|
45529
|
+
*
|
|
45530
|
+
* Split here rather than through the shared helper: this is evaluated as the
|
|
45531
|
+
* module loads, before a test that mocks the file utilities can supply one.
|
|
45532
|
+
*/
|
|
45533
|
+
const CLAUDECODE_SKILLS_DIR_SEGMENTS = CLAUDECODE_SKILLS_DIR_PATH.split(node_path.sep);
|
|
45534
|
+
const CLAUDECODE_SKILLS_DIR_POSIX_PATH = CLAUDECODE_SKILLS_DIR_SEGMENTS.join("/");
|
|
45535
|
+
/**
|
|
45536
|
+
* The segment that puts `relativeDirPath` inside a tree the nested scan
|
|
45537
|
+
* excludes, or `undefined` when none does. These are the same three rules the
|
|
45538
|
+
* scan states as glob `ignore` patterns below -- dependency trees at any depth,
|
|
45539
|
+
* build and vendoring directories at the project root, hidden directories other
|
|
45540
|
+
* than the `.claude` being matched -- and the two have to be kept in step.
|
|
45541
|
+
*
|
|
45542
|
+
* Saying them twice is what a second pass costs. globby matches its patterns
|
|
45543
|
+
* against the path it reports, before the `..` a rewritten directory name
|
|
45544
|
+
* carries is folded away and before a link in the path is resolved. A root
|
|
45545
|
+
* reported at `x/../node_modules/.claude/skills` matches none of the patterns
|
|
45546
|
+
* and then leads to the dependency tree they name, so the decision has to be
|
|
45547
|
+
* taken again on the path that is really read.
|
|
45548
|
+
*
|
|
45549
|
+
* The segments passed are the ones above the `.claude/skills` tail, taken from
|
|
45550
|
+
* the resolved path: a directory name may hold a backslash -- the whole reason a
|
|
45551
|
+
* path can arrive here misspelled -- so the split that produced them has to be on
|
|
45552
|
+
* `/`, the one separator no name can contain. The tail itself is the part the
|
|
45553
|
+
* glob matched and is not judged; `.claude` is hidden by definition and every
|
|
45554
|
+
* root the scan reports ends with it.
|
|
45555
|
+
*/
|
|
45556
|
+
function excludedNestedScanSegment(segments) {
|
|
45557
|
+
return segments.find((segment, index) => NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.includes(segment) || index === 0 && NESTED_SCAN_EXCLUDED_ROOT_DIRS.includes(segment) || isHiddenPathSegment(segment));
|
|
45558
|
+
}
|
|
45559
|
+
/**
|
|
45560
|
+
* Whether a nested skills directory the scan reported can be used as an import
|
|
45561
|
+
* root: either the reason it cannot, or the path it resolves to relative to the
|
|
45562
|
+
* project, which the caller uses to tell two spellings of one root apart.
|
|
45563
|
+
*
|
|
45564
|
+
* A recursive glob cannot be swapped for a walk the way a flat one can, so the
|
|
45565
|
+
* path it hands back is checked instead. globby reads a backslash as a path
|
|
45566
|
+
* separator and rewrites it, so a root below a directory really named
|
|
45567
|
+
* `back\\slash` is reported at `back/slash`. Where that leads decides what to
|
|
45568
|
+
* do with it, and the spelling alone does not say: `back/slash` usually answers
|
|
45569
|
+
* to nothing, but `x\\..\\..\\outside` is reported at `x/../../outside`, which
|
|
45570
|
+
* climbs out of the project through the real sibling `x/`, and `a\\b` at `a/b`,
|
|
45571
|
+
* which may be a symbolic link out of the project that the scan — it passes
|
|
45572
|
+
* `followSymbolicLinks: false` — never meant to reach. Both are refused by
|
|
45573
|
+
* resolving the path rather than reading it.
|
|
45574
|
+
*
|
|
45575
|
+
* What is deliberately not refused is a rewritten path that stays inside the
|
|
45576
|
+
* project, such as `x\\..\\y` reported at `x/../y`. It names a real directory
|
|
45577
|
+
* `y`, and the scan reports that directory under this spelling *instead of* its
|
|
45578
|
+
* own, so refusing it would lose `y`'s skills rather than protect anything. The
|
|
45579
|
+
* skills under the directory that was really named are unreachable either way:
|
|
45580
|
+
* no path the scan can report leads back to a name holding a backslash.
|
|
45581
|
+
*
|
|
45582
|
+
* That last shape is the one case the scan cannot warn about. `a\\b` reported
|
|
45583
|
+
* at `a/b`, where `a/b` is itself a real directory, is indistinguishable from
|
|
45584
|
+
* the ordinary root `a/b` -- both are spelled the same and both are there -- so
|
|
45585
|
+
* the skills under `a\\b` are dropped without a word. Nothing in the path says
|
|
45586
|
+
* a second directory was ever involved.
|
|
45587
|
+
*/
|
|
45588
|
+
async function checkNestedSkillsRoot({ outputRoot, dirPath }) {
|
|
45589
|
+
if (!await directoryExists(dirPath)) return { reason: "it could not be read under the path the scan reports, most often because a directory name above it contains a backslash." };
|
|
45590
|
+
const realRelativeDirPath = await resolvedRelativePath({
|
|
45591
|
+
rootPath: outputRoot,
|
|
45592
|
+
targetPath: dirPath
|
|
45593
|
+
});
|
|
45594
|
+
if (posixRelativePathEscapesRoot(realRelativeDirPath)) return { reason: "it resolves outside the project." };
|
|
45595
|
+
const segments = realRelativeDirPath.split("/");
|
|
45596
|
+
const aboveTailSegments = segments.slice(0, -CLAUDECODE_SKILLS_DIR_SEGMENTS.length);
|
|
45597
|
+
if (segments.slice(-CLAUDECODE_SKILLS_DIR_SEGMENTS.length).join("/") !== CLAUDECODE_SKILLS_DIR_POSIX_PATH) return { reason: `it resolves to ${realRelativeDirPath === "" ? "the project root" : JSON.stringify(stripControlCharacters(realRelativeDirPath))}, which is not a ${CLAUDECODE_SKILLS_DIR_POSIX_PATH} directory.` };
|
|
45598
|
+
const excludedSegment = excludedNestedScanSegment(aboveTailSegments);
|
|
45599
|
+
if (excludedSegment !== void 0) return { reason: `it resolves inside ${JSON.stringify(stripControlCharacters(excludedSegment))}, which the nested scan excludes.` };
|
|
45600
|
+
return { realRelativeDirPath };
|
|
45601
|
+
}
|
|
44792
45602
|
const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
44793
45603
|
name: zod_mini.z.string(),
|
|
44794
45604
|
description: zod_mini.z.string(),
|
|
@@ -45037,10 +45847,10 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
45037
45847
|
*
|
|
45038
45848
|
* @see https://code.claude.com/docs/en/skills
|
|
45039
45849
|
*/
|
|
45040
|
-
static async getConfiguredImportRoots({ outputRoot, global = false }) {
|
|
45850
|
+
static async getConfiguredImportRoots({ outputRoot, global = false, logger }) {
|
|
45041
45851
|
if (global) return [];
|
|
45042
45852
|
const root = toPosixPath(outputRoot);
|
|
45043
|
-
|
|
45853
|
+
const filteredDirPaths = filterOutPathsInGitIgnoredDirectories({
|
|
45044
45854
|
rootDir: outputRoot,
|
|
45045
45855
|
filePaths: await findFilesByGlobs([`${root}/*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`], {
|
|
45046
45856
|
type: "dir",
|
|
@@ -45051,10 +45861,27 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
45051
45861
|
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
|
|
45052
45862
|
]
|
|
45053
45863
|
})
|
|
45054
|
-
}).toSorted()
|
|
45055
|
-
|
|
45056
|
-
|
|
45057
|
-
|
|
45864
|
+
}).toSorted();
|
|
45865
|
+
const roots = [];
|
|
45866
|
+
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH]);
|
|
45867
|
+
for (const dirPath of filteredDirPaths) {
|
|
45868
|
+
const scannedDirPath = (0, node_path.resolve)(dirPath);
|
|
45869
|
+
const check = await checkNestedSkillsRoot({
|
|
45870
|
+
outputRoot,
|
|
45871
|
+
dirPath: scannedDirPath
|
|
45872
|
+
});
|
|
45873
|
+
if ("reason" in check) {
|
|
45874
|
+
logger?.warn(`Skipping the nested Claude Code skills directory ${JSON.stringify(stripControlCharacters(scannedDirPath))}: ${check.reason} Its skills are not imported.`);
|
|
45875
|
+
continue;
|
|
45876
|
+
}
|
|
45877
|
+
if (seenRealRelativeDirPaths.has(check.realRelativeDirPath)) continue;
|
|
45878
|
+
seenRealRelativeDirPaths.add(check.realRelativeDirPath);
|
|
45879
|
+
roots.push({
|
|
45880
|
+
outputRoot,
|
|
45881
|
+
relativeDirPath: (0, node_path.relative)(outputRoot, scannedDirPath)
|
|
45882
|
+
});
|
|
45883
|
+
}
|
|
45884
|
+
return roots;
|
|
45058
45885
|
}
|
|
45059
45886
|
getFrontmatter() {
|
|
45060
45887
|
return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
@@ -49941,7 +50768,8 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
49941
50768
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
49942
50769
|
const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
|
|
49943
50770
|
outputRoot: this.outputRoot,
|
|
49944
|
-
global: this.global
|
|
50771
|
+
global: this.global,
|
|
50772
|
+
logger: this.logger
|
|
49945
50773
|
}) : [];
|
|
49946
50774
|
const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
|
|
49947
50775
|
const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
|
|
@@ -54657,6 +55485,152 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
54657
55485
|
}
|
|
54658
55486
|
};
|
|
54659
55487
|
//#endregion
|
|
55488
|
+
//#region src/features/subagents/zcode-subagent.ts
|
|
55489
|
+
const ZcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
55490
|
+
name: zod_mini.z.string(),
|
|
55491
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
55492
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
55493
|
+
thoughtLevel: zod_mini.z.optional(zod_mini.z.string()),
|
|
55494
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
55495
|
+
tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
55496
|
+
disallowedTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
55497
|
+
maxTurns: zod_mini.z.optional(zod_mini.z.number().check(zod_mini.z.int(), zod_mini.z.positive())),
|
|
55498
|
+
injectAgentsMd: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
55499
|
+
mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
55500
|
+
});
|
|
55501
|
+
/**
|
|
55502
|
+
* ZCode subagents.
|
|
55503
|
+
*
|
|
55504
|
+
* Each subagent is one Markdown file with YAML frontmatter, named after the
|
|
55505
|
+
* agent, under `~/.zcode/agents/`.
|
|
55506
|
+
*
|
|
55507
|
+
* Global scope only. The current Beta "manages global / user-level subagents
|
|
55508
|
+
* stored under `~/.zcode/agents/`", and creating or editing workspace /
|
|
55509
|
+
* project-level subagents "is not available yet" — so this adapter is
|
|
55510
|
+
* registered with `supportsProject: false` and never writes into a project's
|
|
55511
|
+
* own `.zcode/`. The relative path is nonetheless spelled against
|
|
55512
|
+
* {@link ZCODE_AGENTS_DIR_PATH} so the workspace scope needs nothing more than
|
|
55513
|
+
* flipping that flag if ZCode ships it.
|
|
55514
|
+
*
|
|
55515
|
+
* @see https://zcode.z.ai/en/docs/subagents
|
|
55516
|
+
*/
|
|
55517
|
+
var ZcodeSubagent = class ZcodeSubagent extends ToolSubagent {
|
|
55518
|
+
frontmatter;
|
|
55519
|
+
body;
|
|
55520
|
+
constructor({ frontmatter, body, fileContent, ...rest }) {
|
|
55521
|
+
if (rest.validate !== false) {
|
|
55522
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
55523
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
55524
|
+
}
|
|
55525
|
+
super({
|
|
55526
|
+
...rest,
|
|
55527
|
+
fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
|
|
55528
|
+
});
|
|
55529
|
+
this.frontmatter = frontmatter;
|
|
55530
|
+
this.body = body;
|
|
55531
|
+
}
|
|
55532
|
+
static getSettablePaths(_options = {}) {
|
|
55533
|
+
return { relativeDirPath: ZCODE_AGENTS_DIR_PATH };
|
|
55534
|
+
}
|
|
55535
|
+
getFrontmatter() {
|
|
55536
|
+
return this.frontmatter;
|
|
55537
|
+
}
|
|
55538
|
+
getBody() {
|
|
55539
|
+
return this.body;
|
|
55540
|
+
}
|
|
55541
|
+
toRulesyncSubagent() {
|
|
55542
|
+
const { name, description, ...rest } = this.frontmatter;
|
|
55543
|
+
return new RulesyncSubagent({
|
|
55544
|
+
outputRoot: ".",
|
|
55545
|
+
frontmatter: {
|
|
55546
|
+
targets: ["*"],
|
|
55547
|
+
name,
|
|
55548
|
+
description,
|
|
55549
|
+
...Object.keys(rest).length > 0 && { zcode: rest }
|
|
55550
|
+
},
|
|
55551
|
+
body: this.body,
|
|
55552
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
55553
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
55554
|
+
validate: true
|
|
55555
|
+
});
|
|
55556
|
+
}
|
|
55557
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
55558
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
55559
|
+
const zcodeSection = rulesyncFrontmatter.zcode ?? {};
|
|
55560
|
+
const zcodeFrontmatter = {
|
|
55561
|
+
name: rulesyncFrontmatter.name,
|
|
55562
|
+
description: rulesyncFrontmatter.description,
|
|
55563
|
+
...zcodeSection
|
|
55564
|
+
};
|
|
55565
|
+
const body = rulesyncSubagent.getBody();
|
|
55566
|
+
const fileContent = stringifyFrontmatter(body, zcodeFrontmatter, { avoidBlockScalars: true });
|
|
55567
|
+
const paths = this.getSettablePaths({ global });
|
|
55568
|
+
return new ZcodeSubagent({
|
|
55569
|
+
outputRoot,
|
|
55570
|
+
frontmatter: zcodeFrontmatter,
|
|
55571
|
+
body,
|
|
55572
|
+
relativeDirPath: paths.relativeDirPath,
|
|
55573
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
55574
|
+
fileContent,
|
|
55575
|
+
validate,
|
|
55576
|
+
global
|
|
55577
|
+
});
|
|
55578
|
+
}
|
|
55579
|
+
validate() {
|
|
55580
|
+
if (!this.frontmatter) return {
|
|
55581
|
+
success: true,
|
|
55582
|
+
error: null
|
|
55583
|
+
};
|
|
55584
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
55585
|
+
if (result.success) return {
|
|
55586
|
+
success: true,
|
|
55587
|
+
error: null
|
|
55588
|
+
};
|
|
55589
|
+
else return {
|
|
55590
|
+
success: false,
|
|
55591
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
55592
|
+
};
|
|
55593
|
+
}
|
|
55594
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
55595
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
55596
|
+
rulesyncSubagent,
|
|
55597
|
+
toolTarget: "zcode"
|
|
55598
|
+
});
|
|
55599
|
+
}
|
|
55600
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
55601
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
|
|
55602
|
+
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
55603
|
+
const fileContent = await readFileContent(filePath);
|
|
55604
|
+
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
55605
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
55606
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
55607
|
+
return new ZcodeSubagent({
|
|
55608
|
+
outputRoot,
|
|
55609
|
+
relativeDirPath: dirPath,
|
|
55610
|
+
relativeFilePath,
|
|
55611
|
+
frontmatter: result.data,
|
|
55612
|
+
body: content.trim(),
|
|
55613
|
+
fileContent,
|
|
55614
|
+
validate,
|
|
55615
|
+
global
|
|
55616
|
+
});
|
|
55617
|
+
}
|
|
55618
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
55619
|
+
return new ZcodeSubagent({
|
|
55620
|
+
outputRoot,
|
|
55621
|
+
relativeDirPath,
|
|
55622
|
+
relativeFilePath,
|
|
55623
|
+
frontmatter: {
|
|
55624
|
+
name: "",
|
|
55625
|
+
description: ""
|
|
55626
|
+
},
|
|
55627
|
+
body: "",
|
|
55628
|
+
fileContent: "",
|
|
55629
|
+
validate: false
|
|
55630
|
+
});
|
|
55631
|
+
}
|
|
55632
|
+
};
|
|
55633
|
+
//#endregion
|
|
54660
55634
|
//#region src/features/subagents/zoocode-subagent.ts
|
|
54661
55635
|
/**
|
|
54662
55636
|
* Subagent (custom-mode) generator for **Zoo Code**, the community
|
|
@@ -54721,6 +55695,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54721
55695
|
["agentsmd", {
|
|
54722
55696
|
class: AgentsmdSubagent,
|
|
54723
55697
|
meta: {
|
|
55698
|
+
supportsProject: true,
|
|
54724
55699
|
supportsSimulated: true,
|
|
54725
55700
|
supportsGlobal: false,
|
|
54726
55701
|
filePattern: "*.md"
|
|
@@ -54729,6 +55704,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54729
55704
|
["antigravity-cli", {
|
|
54730
55705
|
class: AntigravityCliSubagent,
|
|
54731
55706
|
meta: {
|
|
55707
|
+
supportsProject: true,
|
|
54732
55708
|
supportsSimulated: false,
|
|
54733
55709
|
supportsGlobal: true,
|
|
54734
55710
|
filePattern: "*.md"
|
|
@@ -54737,6 +55713,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54737
55713
|
["antigravity-ide", {
|
|
54738
55714
|
class: AntigravityIdeSubagent,
|
|
54739
55715
|
meta: {
|
|
55716
|
+
supportsProject: true,
|
|
54740
55717
|
supportsSimulated: false,
|
|
54741
55718
|
supportsGlobal: true,
|
|
54742
55719
|
filePattern: "*.md"
|
|
@@ -54745,6 +55722,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54745
55722
|
["antigravity-plugin", {
|
|
54746
55723
|
class: AntigravityPluginSubagent,
|
|
54747
55724
|
meta: {
|
|
55725
|
+
supportsProject: true,
|
|
54748
55726
|
supportsSimulated: false,
|
|
54749
55727
|
supportsGlobal: false,
|
|
54750
55728
|
filePattern: "*.md"
|
|
@@ -54753,6 +55731,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54753
55731
|
["augmentcode", {
|
|
54754
55732
|
class: AugmentcodeSubagent,
|
|
54755
55733
|
meta: {
|
|
55734
|
+
supportsProject: true,
|
|
54756
55735
|
supportsSimulated: false,
|
|
54757
55736
|
supportsGlobal: true,
|
|
54758
55737
|
filePattern: "*.md"
|
|
@@ -54761,6 +55740,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54761
55740
|
["claudecode", {
|
|
54762
55741
|
class: ClaudecodeSubagent,
|
|
54763
55742
|
meta: {
|
|
55743
|
+
supportsProject: true,
|
|
54764
55744
|
supportsSimulated: false,
|
|
54765
55745
|
supportsGlobal: true,
|
|
54766
55746
|
filePattern: "*.md"
|
|
@@ -54769,6 +55749,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54769
55749
|
["claudecode-plugin", {
|
|
54770
55750
|
class: ClaudecodePluginSubagent,
|
|
54771
55751
|
meta: {
|
|
55752
|
+
supportsProject: true,
|
|
54772
55753
|
supportsSimulated: false,
|
|
54773
55754
|
supportsGlobal: false,
|
|
54774
55755
|
filePattern: "*.md"
|
|
@@ -54777,6 +55758,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54777
55758
|
["claudecode-legacy", {
|
|
54778
55759
|
class: ClaudecodeSubagent,
|
|
54779
55760
|
meta: {
|
|
55761
|
+
supportsProject: true,
|
|
54780
55762
|
supportsSimulated: false,
|
|
54781
55763
|
supportsGlobal: true,
|
|
54782
55764
|
filePattern: "*.md"
|
|
@@ -54785,6 +55767,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54785
55767
|
["cline", {
|
|
54786
55768
|
class: ClineSubagent,
|
|
54787
55769
|
meta: {
|
|
55770
|
+
supportsProject: true,
|
|
54788
55771
|
supportsSimulated: false,
|
|
54789
55772
|
supportsGlobal: true,
|
|
54790
55773
|
filePattern: "*.{yaml,yml}"
|
|
@@ -54793,6 +55776,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54793
55776
|
["codexcli", {
|
|
54794
55777
|
class: CodexCliSubagent,
|
|
54795
55778
|
meta: {
|
|
55779
|
+
supportsProject: true,
|
|
54796
55780
|
supportsSimulated: false,
|
|
54797
55781
|
supportsGlobal: true,
|
|
54798
55782
|
filePattern: "*.toml"
|
|
@@ -54801,6 +55785,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54801
55785
|
["copilot", {
|
|
54802
55786
|
class: CopilotSubagent,
|
|
54803
55787
|
meta: {
|
|
55788
|
+
supportsProject: true,
|
|
54804
55789
|
supportsSimulated: false,
|
|
54805
55790
|
supportsGlobal: true,
|
|
54806
55791
|
filePattern: "*.md"
|
|
@@ -54809,6 +55794,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54809
55794
|
["copilotcli", {
|
|
54810
55795
|
class: CopilotcliSubagent,
|
|
54811
55796
|
meta: {
|
|
55797
|
+
supportsProject: true,
|
|
54812
55798
|
supportsSimulated: false,
|
|
54813
55799
|
supportsGlobal: true,
|
|
54814
55800
|
filePattern: "*.agent.md"
|
|
@@ -54817,6 +55803,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54817
55803
|
["cursor", {
|
|
54818
55804
|
class: CursorSubagent,
|
|
54819
55805
|
meta: {
|
|
55806
|
+
supportsProject: true,
|
|
54820
55807
|
supportsSimulated: false,
|
|
54821
55808
|
supportsGlobal: true,
|
|
54822
55809
|
filePattern: "*.md"
|
|
@@ -54825,6 +55812,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54825
55812
|
["deepagents", {
|
|
54826
55813
|
class: DeepagentsSubagent,
|
|
54827
55814
|
meta: {
|
|
55815
|
+
supportsProject: true,
|
|
54828
55816
|
supportsSimulated: false,
|
|
54829
55817
|
supportsGlobal: true,
|
|
54830
55818
|
filePattern: (0, node_path.join)("*", "AGENTS.md")
|
|
@@ -54833,6 +55821,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54833
55821
|
["devin", {
|
|
54834
55822
|
class: DevinSubagent,
|
|
54835
55823
|
meta: {
|
|
55824
|
+
supportsProject: true,
|
|
54836
55825
|
supportsSimulated: false,
|
|
54837
55826
|
supportsGlobal: true,
|
|
54838
55827
|
filePattern: (0, node_path.join)("*", "AGENT.md")
|
|
@@ -54841,6 +55830,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54841
55830
|
["factorydroid", {
|
|
54842
55831
|
class: FactorydroidSubagent,
|
|
54843
55832
|
meta: {
|
|
55833
|
+
supportsProject: true,
|
|
54844
55834
|
supportsSimulated: false,
|
|
54845
55835
|
supportsGlobal: true,
|
|
54846
55836
|
filePattern: "*.md"
|
|
@@ -54849,6 +55839,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54849
55839
|
["goose", {
|
|
54850
55840
|
class: GooseSubagent,
|
|
54851
55841
|
meta: {
|
|
55842
|
+
supportsProject: true,
|
|
54852
55843
|
supportsSimulated: false,
|
|
54853
55844
|
supportsGlobal: true,
|
|
54854
55845
|
filePattern: "*.md"
|
|
@@ -54857,6 +55848,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54857
55848
|
["hermesagent", {
|
|
54858
55849
|
class: HermesagentSubagent,
|
|
54859
55850
|
meta: {
|
|
55851
|
+
supportsProject: true,
|
|
54860
55852
|
supportsGlobal: true,
|
|
54861
55853
|
supportsSimulated: false,
|
|
54862
55854
|
filePattern: "*.json"
|
|
@@ -54865,6 +55857,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54865
55857
|
["grokcli", {
|
|
54866
55858
|
class: GrokcliSubagent,
|
|
54867
55859
|
meta: {
|
|
55860
|
+
supportsProject: true,
|
|
54868
55861
|
supportsSimulated: false,
|
|
54869
55862
|
supportsGlobal: true,
|
|
54870
55863
|
filePattern: "*.md"
|
|
@@ -54873,6 +55866,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54873
55866
|
["junie", {
|
|
54874
55867
|
class: JunieSubagent,
|
|
54875
55868
|
meta: {
|
|
55869
|
+
supportsProject: true,
|
|
54876
55870
|
supportsSimulated: false,
|
|
54877
55871
|
supportsGlobal: true,
|
|
54878
55872
|
filePattern: "*.md"
|
|
@@ -54881,6 +55875,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54881
55875
|
["kiro", {
|
|
54882
55876
|
class: KiroSubagent,
|
|
54883
55877
|
meta: {
|
|
55878
|
+
supportsProject: true,
|
|
54884
55879
|
supportsSimulated: false,
|
|
54885
55880
|
supportsGlobal: false,
|
|
54886
55881
|
filePattern: "*.json"
|
|
@@ -54889,6 +55884,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54889
55884
|
["kiro-cli", {
|
|
54890
55885
|
class: KiroCliSubagent,
|
|
54891
55886
|
meta: {
|
|
55887
|
+
supportsProject: true,
|
|
54892
55888
|
supportsSimulated: false,
|
|
54893
55889
|
supportsGlobal: true,
|
|
54894
55890
|
filePattern: "*.json"
|
|
@@ -54897,6 +55893,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54897
55893
|
["kiro-ide", {
|
|
54898
55894
|
class: KiroIdeSubagent,
|
|
54899
55895
|
meta: {
|
|
55896
|
+
supportsProject: true,
|
|
54900
55897
|
supportsSimulated: false,
|
|
54901
55898
|
supportsGlobal: true,
|
|
54902
55899
|
filePattern: "*.md"
|
|
@@ -54905,6 +55902,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54905
55902
|
["kilo", {
|
|
54906
55903
|
class: KiloSubagent,
|
|
54907
55904
|
meta: {
|
|
55905
|
+
supportsProject: true,
|
|
54908
55906
|
supportsSimulated: false,
|
|
54909
55907
|
supportsGlobal: true,
|
|
54910
55908
|
filePattern: "*.md"
|
|
@@ -54913,6 +55911,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54913
55911
|
["kimi-code", {
|
|
54914
55912
|
class: KimiCodeSubagent,
|
|
54915
55913
|
meta: {
|
|
55914
|
+
supportsProject: true,
|
|
54916
55915
|
supportsSimulated: false,
|
|
54917
55916
|
supportsGlobal: true,
|
|
54918
55917
|
filePattern: (0, node_path.join)("**", "*.md")
|
|
@@ -54921,6 +55920,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54921
55920
|
["opencode", {
|
|
54922
55921
|
class: OpenCodeSubagent,
|
|
54923
55922
|
meta: {
|
|
55923
|
+
supportsProject: true,
|
|
54924
55924
|
supportsSimulated: false,
|
|
54925
55925
|
supportsGlobal: true,
|
|
54926
55926
|
filePattern: "*.md"
|
|
@@ -54929,6 +55929,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54929
55929
|
["qwencode", {
|
|
54930
55930
|
class: QwencodeSubagent,
|
|
54931
55931
|
meta: {
|
|
55932
|
+
supportsProject: true,
|
|
54932
55933
|
supportsSimulated: false,
|
|
54933
55934
|
supportsGlobal: true,
|
|
54934
55935
|
filePattern: "*.md"
|
|
@@ -54937,6 +55938,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54937
55938
|
["reasonix", {
|
|
54938
55939
|
class: ReasonixSubagent,
|
|
54939
55940
|
meta: {
|
|
55941
|
+
supportsProject: true,
|
|
54940
55942
|
supportsSimulated: false,
|
|
54941
55943
|
supportsGlobal: true,
|
|
54942
55944
|
filePattern: (0, node_path.join)("*", "SKILL.md")
|
|
@@ -54945,6 +55947,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54945
55947
|
["roo", {
|
|
54946
55948
|
class: RooSubagent,
|
|
54947
55949
|
meta: {
|
|
55950
|
+
supportsProject: true,
|
|
54948
55951
|
supportsSimulated: false,
|
|
54949
55952
|
supportsGlobal: false,
|
|
54950
55953
|
filePattern: ".roomodes"
|
|
@@ -54953,6 +55956,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54953
55956
|
["zoocode", {
|
|
54954
55957
|
class: ZoocodeSubagent,
|
|
54955
55958
|
meta: {
|
|
55959
|
+
supportsProject: true,
|
|
54956
55960
|
supportsSimulated: false,
|
|
54957
55961
|
supportsGlobal: false,
|
|
54958
55962
|
filePattern: ".roomodes"
|
|
@@ -54961,6 +55965,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54961
55965
|
["rovodev", {
|
|
54962
55966
|
class: RovodevSubagent,
|
|
54963
55967
|
meta: {
|
|
55968
|
+
supportsProject: true,
|
|
54964
55969
|
supportsSimulated: false,
|
|
54965
55970
|
supportsGlobal: true,
|
|
54966
55971
|
filePattern: "*.md"
|
|
@@ -54969,6 +55974,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54969
55974
|
["takt", {
|
|
54970
55975
|
class: TaktSubagent,
|
|
54971
55976
|
meta: {
|
|
55977
|
+
supportsProject: true,
|
|
54972
55978
|
supportsSimulated: false,
|
|
54973
55979
|
supportsGlobal: true,
|
|
54974
55980
|
filePattern: "*.md"
|
|
@@ -54977,10 +55983,20 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54977
55983
|
["vibe", {
|
|
54978
55984
|
class: VibeSubagent,
|
|
54979
55985
|
meta: {
|
|
55986
|
+
supportsProject: true,
|
|
54980
55987
|
supportsSimulated: false,
|
|
54981
55988
|
supportsGlobal: true,
|
|
54982
55989
|
filePattern: "*.toml"
|
|
54983
55990
|
}
|
|
55991
|
+
}],
|
|
55992
|
+
["zcode", {
|
|
55993
|
+
class: ZcodeSubagent,
|
|
55994
|
+
meta: {
|
|
55995
|
+
supportsProject: false,
|
|
55996
|
+
supportsSimulated: false,
|
|
55997
|
+
supportsGlobal: true,
|
|
55998
|
+
filePattern: "*.md"
|
|
55999
|
+
}
|
|
54984
56000
|
}]
|
|
54985
56001
|
]);
|
|
54986
56002
|
const defaultGetFactory$1 = (target) => {
|
|
@@ -54989,7 +56005,9 @@ const defaultGetFactory$1 = (target) => {
|
|
|
54989
56005
|
return factory;
|
|
54990
56006
|
};
|
|
54991
56007
|
const allToolTargetKeys$1 = [...toolSubagentFactories.keys()];
|
|
54992
|
-
const subagentsProcessorToolTargets = allToolTargetKeys$1
|
|
56008
|
+
const subagentsProcessorToolTargets = allToolTargetKeys$1.filter((target) => {
|
|
56009
|
+
return toolSubagentFactories.get(target)?.meta.supportsProject ?? false;
|
|
56010
|
+
});
|
|
54993
56011
|
const subagentsProcessorToolTargetsSimulated = allToolTargetKeys$1.filter((target) => {
|
|
54994
56012
|
return toolSubagentFactories.get(target)?.meta.supportsSimulated ?? false;
|
|
54995
56013
|
});
|
|
@@ -55089,7 +56107,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
55089
56107
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
55090
56108
|
return [];
|
|
55091
56109
|
}
|
|
55092
|
-
const mdFiles = (await
|
|
56110
|
+
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
55093
56111
|
if (mdFiles.length === 0) {
|
|
55094
56112
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
55095
56113
|
return [];
|
|
@@ -63725,6 +64743,12 @@ Object.defineProperty(exports, "CONFLICTING_TARGET_PAIRS", {
|
|
|
63725
64743
|
return CONFLICTING_TARGET_PAIRS;
|
|
63726
64744
|
}
|
|
63727
64745
|
});
|
|
64746
|
+
Object.defineProperty(exports, "CURATED_RULES_FEATURE_SUBDIR", {
|
|
64747
|
+
enumerable: true,
|
|
64748
|
+
get: function() {
|
|
64749
|
+
return CURATED_RULES_FEATURE_SUBDIR;
|
|
64750
|
+
}
|
|
64751
|
+
});
|
|
63728
64752
|
Object.defineProperty(exports, "ChecksProcessor", {
|
|
63729
64753
|
enumerable: true,
|
|
63730
64754
|
get: function() {
|
|
@@ -64121,6 +65145,12 @@ Object.defineProperty(exports, "ToolTargetSchema", {
|
|
|
64121
65145
|
return ToolTargetSchema;
|
|
64122
65146
|
}
|
|
64123
65147
|
});
|
|
65148
|
+
Object.defineProperty(exports, "WarningCollectingLogger", {
|
|
65149
|
+
enumerable: true,
|
|
65150
|
+
get: function() {
|
|
65151
|
+
return WarningCollectingLogger;
|
|
65152
|
+
}
|
|
65153
|
+
});
|
|
64124
65154
|
Object.defineProperty(exports, "__toESM", {
|
|
64125
65155
|
enumerable: true,
|
|
64126
65156
|
get: function() {
|
|
@@ -64205,12 +65235,6 @@ Object.defineProperty(exports, "findControlCharacter", {
|
|
|
64205
65235
|
return findControlCharacter;
|
|
64206
65236
|
}
|
|
64207
65237
|
});
|
|
64208
|
-
Object.defineProperty(exports, "findFilesByGlobs", {
|
|
64209
|
-
enumerable: true,
|
|
64210
|
-
get: function() {
|
|
64211
|
-
return findFilesByGlobs;
|
|
64212
|
-
}
|
|
64213
|
-
});
|
|
64214
65238
|
Object.defineProperty(exports, "formatError", {
|
|
64215
65239
|
enumerable: true,
|
|
64216
65240
|
get: function() {
|
|
@@ -64259,6 +65283,12 @@ Object.defineProperty(exports, "getRulesyncSourceCandidates", {
|
|
|
64259
65283
|
return getRulesyncSourceCandidates;
|
|
64260
65284
|
}
|
|
64261
65285
|
});
|
|
65286
|
+
Object.defineProperty(exports, "groupSpellingsByCaseFoldedIdentity", {
|
|
65287
|
+
enumerable: true,
|
|
65288
|
+
get: function() {
|
|
65289
|
+
return groupSpellingsByCaseFoldedIdentity;
|
|
65290
|
+
}
|
|
65291
|
+
});
|
|
64262
65292
|
Object.defineProperty(exports, "hasDeceptiveHiddenCharacters", {
|
|
64263
65293
|
enumerable: true,
|
|
64264
65294
|
get: function() {
|
|
@@ -64283,6 +65313,12 @@ Object.defineProperty(exports, "isFileNotFoundError", {
|
|
|
64283
65313
|
return isFileNotFoundError;
|
|
64284
65314
|
}
|
|
64285
65315
|
});
|
|
65316
|
+
Object.defineProperty(exports, "isFileSystemError", {
|
|
65317
|
+
enumerable: true,
|
|
65318
|
+
get: function() {
|
|
65319
|
+
return isFileSystemError;
|
|
65320
|
+
}
|
|
65321
|
+
});
|
|
64286
65322
|
Object.defineProperty(exports, "isPackagingToolTarget", {
|
|
64287
65323
|
enumerable: true,
|
|
64288
65324
|
get: function() {
|
|
@@ -64295,10 +65331,16 @@ Object.defineProperty(exports, "isSymlink", {
|
|
|
64295
65331
|
return isSymlink;
|
|
64296
65332
|
}
|
|
64297
65333
|
});
|
|
64298
|
-
Object.defineProperty(exports, "
|
|
65334
|
+
Object.defineProperty(exports, "listDirectoryEntryNames", {
|
|
64299
65335
|
enumerable: true,
|
|
64300
65336
|
get: function() {
|
|
64301
|
-
return
|
|
65337
|
+
return listDirectoryEntryNames;
|
|
65338
|
+
}
|
|
65339
|
+
});
|
|
65340
|
+
Object.defineProperty(exports, "listFilePathsRecursively", {
|
|
65341
|
+
enumerable: true,
|
|
65342
|
+
get: function() {
|
|
65343
|
+
return listFilePathsRecursively;
|
|
64302
65344
|
}
|
|
64303
65345
|
});
|
|
64304
65346
|
Object.defineProperty(exports, "listSubdirectoryNames", {
|
|
@@ -64427,6 +65469,12 @@ Object.defineProperty(exports, "stripControlCharacters", {
|
|
|
64427
65469
|
return stripControlCharacters;
|
|
64428
65470
|
}
|
|
64429
65471
|
});
|
|
65472
|
+
Object.defineProperty(exports, "stripControlCharactersKeepingLineFeeds", {
|
|
65473
|
+
enumerable: true,
|
|
65474
|
+
get: function() {
|
|
65475
|
+
return stripControlCharactersKeepingLineFeeds;
|
|
65476
|
+
}
|
|
65477
|
+
});
|
|
64430
65478
|
Object.defineProperty(exports, "stripHiddenCharacters", {
|
|
64431
65479
|
enumerable: true,
|
|
64432
65480
|
get: function() {
|
|
@@ -64439,12 +65487,30 @@ Object.defineProperty(exports, "toPosixPath", {
|
|
|
64439
65487
|
return toPosixPath;
|
|
64440
65488
|
}
|
|
64441
65489
|
});
|
|
65490
|
+
Object.defineProperty(exports, "truncateText", {
|
|
65491
|
+
enumerable: true,
|
|
65492
|
+
get: function() {
|
|
65493
|
+
return truncateText;
|
|
65494
|
+
}
|
|
65495
|
+
});
|
|
64442
65496
|
Object.defineProperty(exports, "warnOnConflictingFlags", {
|
|
64443
65497
|
enumerable: true,
|
|
64444
65498
|
get: function() {
|
|
64445
65499
|
return warnOnConflictingFlags;
|
|
64446
65500
|
}
|
|
64447
65501
|
});
|
|
65502
|
+
Object.defineProperty(exports, "withFallbackLoggerTarget", {
|
|
65503
|
+
enumerable: true,
|
|
65504
|
+
get: function() {
|
|
65505
|
+
return withFallbackLoggerTarget;
|
|
65506
|
+
}
|
|
65507
|
+
});
|
|
65508
|
+
Object.defineProperty(exports, "withWarnOnceScope", {
|
|
65509
|
+
enumerable: true,
|
|
65510
|
+
get: function() {
|
|
65511
|
+
return withWarnOnceScope;
|
|
65512
|
+
}
|
|
65513
|
+
});
|
|
64448
65514
|
Object.defineProperty(exports, "writeFileBuffer", {
|
|
64449
65515
|
enumerable: true,
|
|
64450
65516
|
get: function() {
|