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
|
@@ -6,15 +6,221 @@ import { parse, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { intersection, kebabCase, uniq } from "es-toolkit";
|
|
8
8
|
import { globbySync, isGitIgnoredSync } from "globby";
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
import { format, isDeepStrictEqual } from "node:util";
|
|
9
11
|
import matter from "gray-matter";
|
|
10
12
|
import { YAMLException, dump, load } from "js-yaml";
|
|
11
13
|
import { omit } from "es-toolkit/object";
|
|
12
14
|
import { constants } from "node:fs";
|
|
13
15
|
import { createHash } from "node:crypto";
|
|
14
|
-
import { isDeepStrictEqual } from "node:util";
|
|
15
16
|
import * as smolToml from "smol-toml";
|
|
16
17
|
import { parse as parse$1, stringify } from "smol-toml";
|
|
17
18
|
import { encode } from "@toon-format/toon";
|
|
19
|
+
//#region src/utils/control-characters.ts
|
|
20
|
+
/**
|
|
21
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
22
|
+
* introducer U+009B), the bidirectional overrides and isolates, the Unicode
|
|
23
|
+
* line and paragraph separators, and the plain LRM/RLM/ALM marks. A name or
|
|
24
|
+
* value copied out of an untrusted config file, a fetched repository, or a
|
|
25
|
+
* tool's own settings file must never reach the terminal with these intact:
|
|
26
|
+
* they let the text forge log lines, reorder what is printed around them, or
|
|
27
|
+
* inject escape sequences. LRM, RLM and the Arabic letter mark open no bidi
|
|
28
|
+
* scope of their own, but they still reorder the neutral characters beside
|
|
29
|
+
* them, so they go too — a diagnostic line is not the place to preserve the
|
|
30
|
+
* typography of a right-to-left name.
|
|
31
|
+
*/
|
|
32
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
33
|
+
/**
|
|
34
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
35
|
+
* log line or other terminal output.
|
|
36
|
+
*/
|
|
37
|
+
function stripControlCharacters(text) {
|
|
38
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Removes every control character from `text` except the line feed, so a
|
|
42
|
+
* message written to be read over several lines still is.
|
|
43
|
+
*
|
|
44
|
+
* `stripControlCharacters` takes newlines out because a diagnostic is one line
|
|
45
|
+
* and a name that carries one can forge a second. An error message is not: a
|
|
46
|
+
* lock file names the process holding it over several lines, and the MCP
|
|
47
|
+
* `generate` failure lists one unreadable source per line. The carriage return
|
|
48
|
+
* still goes, since on its own it paints over the line already written — which
|
|
49
|
+
* is why this splits on the line feed and strips each line rather than carrying
|
|
50
|
+
* a second character class that has to be kept in step with the first.
|
|
51
|
+
*/
|
|
52
|
+
function stripControlCharactersKeepingLineFeeds(text) {
|
|
53
|
+
return text.split("\n").map(stripControlCharacters).join("\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Matches the characters that take no width of their own.
|
|
57
|
+
*
|
|
58
|
+
* `Default_Ignorable_Code_Point` is the Unicode property for exactly this — the
|
|
59
|
+
* zero-width joiners, the soft hyphen, the variation selectors, the Hangul
|
|
60
|
+
* fillers, the tag characters — and the format category `Cf` covers the few
|
|
61
|
+
* that sit outside it, such as the interlinear annotation marks. Both are used
|
|
62
|
+
* rather than a list of ranges because a list has to be revisited every time
|
|
63
|
+
* Unicode adds one, and the one that is missed is the one an attacker reaches
|
|
64
|
+
* for: U+3164 HANGUL FILLER, the classic of the homograph domain names, is a
|
|
65
|
+
* letter of the Hangul script and would pass every check aimed at Latin.
|
|
66
|
+
*
|
|
67
|
+
* The braille blank is named on its own. It carries no dots, so it draws as
|
|
68
|
+
* nothing while belonging to neither set.
|
|
69
|
+
*
|
|
70
|
+
* None of these is a control character, so none is caught by
|
|
71
|
+
* `stripControlCharacters` — and none of them shows. A name that differs from
|
|
72
|
+
* another only by one of these is drawn exactly like it, which is why a name
|
|
73
|
+
* that carries one is not a name a user can be asked to judge.
|
|
74
|
+
*/
|
|
75
|
+
const INVISIBLE_CHARACTERS_PATTERN = /[\p{Default_Ignorable_Code_Point}\p{Cf}\u2800]/gu;
|
|
76
|
+
/**
|
|
77
|
+
* Removes every zero-width and otherwise invisible character from `text`.
|
|
78
|
+
*
|
|
79
|
+
* Kept apart from `stripControlCharacters` because the two answer different
|
|
80
|
+
* questions. That one asks what is safe to print; this one asks whether a name
|
|
81
|
+
* shows everything it contains, which is what a prompt needs before it offers
|
|
82
|
+
* the name as something to pick.
|
|
83
|
+
*/
|
|
84
|
+
function stripInvisibleCharacters(text) {
|
|
85
|
+
return text.replace(INVISIBLE_CHARACTERS_PATTERN, "");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Removes every character that does not show: the control characters and the
|
|
89
|
+
* invisible ones alike.
|
|
90
|
+
*
|
|
91
|
+
* This is the form a name has to survive unchanged before it can be offered as
|
|
92
|
+
* something to choose. Callers that need the whole answer should reach for this
|
|
93
|
+
* rather than composing the two strippers themselves, so that the order — and
|
|
94
|
+
* the definition of "hidden" — lives in one place.
|
|
95
|
+
*/
|
|
96
|
+
function stripHiddenCharacters(text) {
|
|
97
|
+
return stripInvisibleCharacters(stripControlCharacters(text));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The invisible characters that do a job in some scripts rather than only
|
|
101
|
+
* hiding: the two zero-width joiners and the variation selectors.
|
|
102
|
+
*
|
|
103
|
+
* A Persian or Indic name spells a word with ZWNJ (U+200C) in it, and an emoji
|
|
104
|
+
* name is a chain of ZWJ (U+200D) and variation selectors. Refusing those
|
|
105
|
+
* outright would refuse names that are written the only way their script writes
|
|
106
|
+
* them, so they are judged by where they sit rather than by what they are.
|
|
107
|
+
*/
|
|
108
|
+
const ZERO_WIDTH_JOINER_PATTERN = /\u200c|\u200d/u;
|
|
109
|
+
const VARIATION_SELECTOR_PATTERN = /[\u{fe00}-\u{fe0f}]|[\u{e0100}-\u{e01ef}]/u;
|
|
110
|
+
/** The twelve characters a keycap can be built on, per ED-14 of UTS #51. */
|
|
111
|
+
const KEYCAP_BASE_PATTERN = /[0-9#*]/u;
|
|
112
|
+
/** The selector that asks for the emoji form of the character before it. */
|
|
113
|
+
const EMOJI_PRESENTATION_SELECTOR$1 = "️";
|
|
114
|
+
/** U+20E3, which draws the box the base and the selector go inside. */
|
|
115
|
+
const COMBINING_ENCLOSING_KEYCAP = "⃣";
|
|
116
|
+
/**
|
|
117
|
+
* Whether the three characters are an emoji keycap, spelled as UTS #51 spells
|
|
118
|
+
* it: `Emoji_Keycap_Sequence := [0-9#*] FE0F 20E3`.
|
|
119
|
+
*
|
|
120
|
+
* The twelve bases are the only ASCII characters Unicode gives the `Emoji`
|
|
121
|
+
* property to, and not one of them is a pictograph — `1` is a digit and `#` is
|
|
122
|
+
* punctuation — so the joining list below cannot see the sequence, and a
|
|
123
|
+
* directory named `1\u{fe0f}\u{20e3}` would be turned away as a digit padded
|
|
124
|
+
* with a variation selector. It is not padding: the selector is what asks for
|
|
125
|
+
* the emoji form of the digit, and the enclosing keycap behind it is what draws
|
|
126
|
+
* the box around it. Both neighbors are required, which is what keeps the
|
|
127
|
+
* exception to the sequence rather than handing it to every digit in every
|
|
128
|
+
* name: `pdf1` with a variation selector and no keycap after it is padding
|
|
129
|
+
* still, and is refused still.
|
|
130
|
+
*
|
|
131
|
+
* @see https://www.unicode.org/reports/tr51/#def_emoji_keycap_sequence
|
|
132
|
+
*/
|
|
133
|
+
function isKeycapSequence(params) {
|
|
134
|
+
const { base, selector, following } = params;
|
|
135
|
+
return base !== void 0 && KEYCAP_BASE_PATTERN.test(base) && selector === EMOJI_PRESENTATION_SELECTOR$1 && following === COMBINING_ENCLOSING_KEYCAP;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The characters a joiner has work to do beside: the scripts whose words are
|
|
139
|
+
* written with one, and the pictographs an emoji sequence is built from.
|
|
140
|
+
*
|
|
141
|
+
* A list of what may join rather than of what may not, because the two are not
|
|
142
|
+
* the same size. `pdf` with a ZWNJ between the d and the f is `pdf` on screen
|
|
143
|
+
* and a different directory underneath, and the same is true of `設定` with a
|
|
144
|
+
* ZWJ after the first character: neither Latin nor Han joins anything that way,
|
|
145
|
+
* and nor does Cyrillic, Greek, Hangul or kana. Naming the scripts that do —
|
|
146
|
+
* the Arabic family, the Indic ones, Mongolian, and the pictographs — is what
|
|
147
|
+
* keeps the exception to the names that need it, instead of handing it to every
|
|
148
|
+
* writing system that is merely not Latin.
|
|
149
|
+
*/
|
|
150
|
+
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;
|
|
151
|
+
/** Non-global copies, because `test` on a global regex carries state between calls. */
|
|
152
|
+
const CONTROL_CHARACTER_PATTERN = new RegExp(CONTROL_CHARACTERS_PATTERN.source, "u");
|
|
153
|
+
const INVISIBLE_CHARACTER_PATTERN = new RegExp(INVISIBLE_CHARACTERS_PATTERN.source, "u");
|
|
154
|
+
/**
|
|
155
|
+
* Whether `text` carries a hidden character that is there to hide something.
|
|
156
|
+
*
|
|
157
|
+
* This is the question a name has to answer before it can be offered as
|
|
158
|
+
* something to pick, and it is a narrower one than `stripHiddenCharacters`
|
|
159
|
+
* answers. Every control character counts, and so does every invisible
|
|
160
|
+
* character — except a joiner or variation selector standing where its own
|
|
161
|
+
* script would put one, which is to say beside a character from a script that
|
|
162
|
+
* is written with joiners, or beside a pictograph.
|
|
163
|
+
*
|
|
164
|
+
* A joiner is held to both of its neighbors, since it exists to bind two
|
|
165
|
+
* characters and a name that ends in one is binding nothing: `設定` with a ZWJ
|
|
166
|
+
* after it is `設定` on screen and a second directory underneath. A variation
|
|
167
|
+
* selector is held only to the character before it, which is the one it selects
|
|
168
|
+
* a form for, and which is why an emoji name may end in one.
|
|
169
|
+
*
|
|
170
|
+
* The keycap sequence is the one emoji the joining list cannot recognize on its
|
|
171
|
+
* own, since what it is built on is a digit or an ASCII sign rather than a
|
|
172
|
+
* pictograph, so it is matched whole instead.
|
|
173
|
+
*
|
|
174
|
+
* Han is not on the joining list, so an ideographic variation sequence — a Han
|
|
175
|
+
* character followed by one of U+E0100 onward — is refused along with the rest.
|
|
176
|
+
* That is the intended trade: no skill directory here is named with one, and
|
|
177
|
+
* the pair is drawn as the bare character on every terminal that has no font
|
|
178
|
+
* for the variant, which is the shape the check exists to refuse.
|
|
179
|
+
*
|
|
180
|
+
* The test is a heuristic in place of the CONTEXTJ joining rules of IDNA,
|
|
181
|
+
* which decide the same question by the joining type of the characters around
|
|
182
|
+
* the joiner. It errs toward accepting a name written in a script that needs
|
|
183
|
+
* these characters, and toward rejecting one that mixes them into Latin, where
|
|
184
|
+
* they can only be padding.
|
|
185
|
+
*/
|
|
186
|
+
function hasDeceptiveHiddenCharacters(text) {
|
|
187
|
+
const characters = [...text];
|
|
188
|
+
const joinsCharacter = (neighbor) => neighbor !== void 0 && JOINING_CONTEXT_PATTERN.test(neighbor);
|
|
189
|
+
return characters.some((character, index) => {
|
|
190
|
+
if (CONTROL_CHARACTER_PATTERN.test(character)) return true;
|
|
191
|
+
if (!INVISIBLE_CHARACTER_PATTERN.test(character)) return false;
|
|
192
|
+
if (VARIATION_SELECTOR_PATTERN.test(character)) {
|
|
193
|
+
if (isKeycapSequence({
|
|
194
|
+
base: characters[index - 1],
|
|
195
|
+
selector: character,
|
|
196
|
+
following: characters[index + 1]
|
|
197
|
+
})) return false;
|
|
198
|
+
return !joinsCharacter(characters[index - 1]);
|
|
199
|
+
}
|
|
200
|
+
if (!ZERO_WIDTH_JOINER_PATTERN.test(character)) return true;
|
|
201
|
+
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/utils/truncate.ts
|
|
206
|
+
/**
|
|
207
|
+
* Cut `text` to `maxLength` without splitting a character in half.
|
|
208
|
+
*
|
|
209
|
+
* `String.prototype.slice` counts UTF-16 units, so cutting inside a surrogate
|
|
210
|
+
* pair leaves a lone surrogate that the next encoder turns into a replacement
|
|
211
|
+
* character, and cutting inside a `\uXXXX` escape that `JSON.stringify` wrote
|
|
212
|
+
* leaves a dangling backslash. Diagnostics quote files rulesync did not write,
|
|
213
|
+
* so both are reachable from a repository's own content rather than only from
|
|
214
|
+
* a hand-crafted string.
|
|
215
|
+
*/
|
|
216
|
+
function truncateText({ text, maxLength, suffix }) {
|
|
217
|
+
if (text.length <= maxLength) return text;
|
|
218
|
+
const characters = Array.from(text.slice(0, maxLength * 2 + 2));
|
|
219
|
+
if (characters.length <= maxLength) return text;
|
|
220
|
+
const cut = characters.slice(0, maxLength).join("");
|
|
221
|
+
return `${(/\\*$/.exec(cut)?.[0].length ?? 0) % 2 === 0 ? cut : cut.slice(0, -1)}${suffix}`;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
18
224
|
//#region src/utils/error.ts
|
|
19
225
|
/**
|
|
20
226
|
* Convert various error types to a readable error message
|
|
@@ -39,10 +245,52 @@ import { encode } from "@toon-format/toon";
|
|
|
39
245
|
function isZodErrorLike(error) {
|
|
40
246
|
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");
|
|
41
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* How much of a Zod error the formatted message spells out.
|
|
250
|
+
*
|
|
251
|
+
* One `safeParse` of a large invalid document produces an issue per offending
|
|
252
|
+
* node, each carrying the path and message, so the raw expansion is bounded by
|
|
253
|
+
* the size of the input rather than by anything rulesync decides — and the
|
|
254
|
+
* formatted message no longer stops at a terminal: it becomes the `message` of
|
|
255
|
+
* a `--json` failure document and of an MCP result. The first few issues are
|
|
256
|
+
* what tells the reader which file to open; the rest is the same information
|
|
257
|
+
* again, at whatever length the input chose.
|
|
258
|
+
*/
|
|
259
|
+
const MAX_ZOD_ISSUES_LENGTH = 2e3;
|
|
260
|
+
/**
|
|
261
|
+
* How much of any other error the formatted message spells out.
|
|
262
|
+
*
|
|
263
|
+
* Larger than the Zod bound because the text is the error's own sentence rather
|
|
264
|
+
* than a re-listing of one issue per offending node, and because the MCP
|
|
265
|
+
* `generate` failure hands this one a line per unreadable source. Bounded all
|
|
266
|
+
* the same: a parser quotes the offending line verbatim, and a minified file is
|
|
267
|
+
* one line the length of the file.
|
|
268
|
+
*/
|
|
269
|
+
const MAX_ERROR_MESSAGE_LENGTH = 8e3;
|
|
270
|
+
/**
|
|
271
|
+
* Strip and bound an error message that is about to be read by something other
|
|
272
|
+
* than a terminal — a `--json` failure document, an MCP result.
|
|
273
|
+
*
|
|
274
|
+
* The line feed stays: several messages are deliberately written over more than
|
|
275
|
+
* one line, and running them together would cost more than the newline can do
|
|
276
|
+
* here. Everything that reorders the text around it, or that an escape sequence
|
|
277
|
+
* is written with, goes.
|
|
278
|
+
*/
|
|
279
|
+
function boundErrorMessage(text) {
|
|
280
|
+
return truncateText({
|
|
281
|
+
text: stripControlCharactersKeepingLineFeeds(text),
|
|
282
|
+
maxLength: MAX_ERROR_MESSAGE_LENGTH,
|
|
283
|
+
suffix: "…(truncated)"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
42
286
|
function formatError(error) {
|
|
43
|
-
if (error instanceof ZodError || isZodErrorLike(error)) return `Zod raw error: ${
|
|
44
|
-
|
|
45
|
-
|
|
287
|
+
if (error instanceof ZodError || isZodErrorLike(error)) return `Zod raw error: ${truncateText({
|
|
288
|
+
text: stripControlCharacters(JSON.stringify(error.issues)),
|
|
289
|
+
maxLength: MAX_ZOD_ISSUES_LENGTH,
|
|
290
|
+
suffix: "…(truncated)"
|
|
291
|
+
})}`;
|
|
292
|
+
if (error instanceof Error) return boundErrorMessage(`${error.name}: ${error.message}`);
|
|
293
|
+
return boundErrorMessage(String(error));
|
|
46
294
|
}
|
|
47
295
|
//#endregion
|
|
48
296
|
//#region src/types/features.ts
|
|
@@ -336,7 +584,8 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
336
584
|
"zoocode",
|
|
337
585
|
"rovodev",
|
|
338
586
|
"takt",
|
|
339
|
-
"vibe"
|
|
587
|
+
"vibe",
|
|
588
|
+
"zcode"
|
|
340
589
|
];
|
|
341
590
|
const skillsProcessorToolTargetTuple = [
|
|
342
591
|
"agentsmd",
|
|
@@ -516,138 +765,6 @@ async function mapWithConcurrency({ items, limit, mapper }) {
|
|
|
516
765
|
return results;
|
|
517
766
|
}
|
|
518
767
|
//#endregion
|
|
519
|
-
//#region src/utils/control-characters.ts
|
|
520
|
-
/**
|
|
521
|
-
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
522
|
-
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
523
|
-
* line and paragraph separators, and the plain LRM/RLM/ALM marks. A name or value
|
|
524
|
-
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
525
|
-
* settings file must never reach the terminal with these intact: they let the
|
|
526
|
-
* text forge log lines, reorder what is printed around them, or inject escape
|
|
527
|
-
* sequences. LRM, RLM and the Arabic letter mark open no bidi scope of their
|
|
528
|
-
* own, but they still reorder the neutral characters beside them, so they go too — a diagnostic line is not the
|
|
529
|
-
* place to preserve the typography of a right-to-left name.
|
|
530
|
-
*/
|
|
531
|
-
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
532
|
-
/**
|
|
533
|
-
* Removes every control character from `text` so it is safe to splice into a
|
|
534
|
-
* log line or other terminal output.
|
|
535
|
-
*/
|
|
536
|
-
function stripControlCharacters(text) {
|
|
537
|
-
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Matches the characters that take no width of their own.
|
|
541
|
-
*
|
|
542
|
-
* `Default_Ignorable_Code_Point` is the Unicode property for exactly this — the
|
|
543
|
-
* zero-width joiners, the soft hyphen, the variation selectors, the Hangul
|
|
544
|
-
* fillers, the tag characters — and the format category `Cf` covers the few
|
|
545
|
-
* that sit outside it, such as the interlinear annotation marks. Both are used
|
|
546
|
-
* rather than a list of ranges because a list has to be revisited every time
|
|
547
|
-
* Unicode adds one, and the one that is missed is the one an attacker reaches
|
|
548
|
-
* for: U+3164 HANGUL FILLER, the classic of the homograph domain names, is a
|
|
549
|
-
* letter of the Hangul script and would pass every check aimed at Latin.
|
|
550
|
-
*
|
|
551
|
-
* The braille blank is named on its own. It carries no dots, so it draws as
|
|
552
|
-
* nothing while belonging to neither set.
|
|
553
|
-
*
|
|
554
|
-
* None of these is a control character, so none is caught by
|
|
555
|
-
* `stripControlCharacters` — and none of them shows. A name that differs from
|
|
556
|
-
* another only by one of these is drawn exactly like it, which is why a name
|
|
557
|
-
* that carries one is not a name a user can be asked to judge.
|
|
558
|
-
*/
|
|
559
|
-
const INVISIBLE_CHARACTERS_PATTERN = /[\p{Default_Ignorable_Code_Point}\p{Cf}\u2800]/gu;
|
|
560
|
-
/**
|
|
561
|
-
* Removes every zero-width and otherwise invisible character from `text`.
|
|
562
|
-
*
|
|
563
|
-
* Kept apart from `stripControlCharacters` because the two answer different
|
|
564
|
-
* questions. That one asks what is safe to print; this one asks whether a name
|
|
565
|
-
* shows everything it contains, which is what a prompt needs before it offers
|
|
566
|
-
* the name as something to pick.
|
|
567
|
-
*/
|
|
568
|
-
function stripInvisibleCharacters(text) {
|
|
569
|
-
return text.replace(INVISIBLE_CHARACTERS_PATTERN, "");
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Removes every character that does not show: the control characters and the
|
|
573
|
-
* invisible ones alike.
|
|
574
|
-
*
|
|
575
|
-
* This is the form a name has to survive unchanged before it can be offered as
|
|
576
|
-
* something to choose. Callers that need the whole answer should reach for this
|
|
577
|
-
* rather than composing the two strippers themselves, so that the order — and
|
|
578
|
-
* the definition of "hidden" — lives in one place.
|
|
579
|
-
*/
|
|
580
|
-
function stripHiddenCharacters(text) {
|
|
581
|
-
return stripInvisibleCharacters(stripControlCharacters(text));
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* The invisible characters that do a job in some scripts rather than only
|
|
585
|
-
* hiding: the two zero-width joiners and the variation selectors.
|
|
586
|
-
*
|
|
587
|
-
* A Persian or Indic name spells a word with ZWNJ (U+200C) in it, and an emoji
|
|
588
|
-
* name is a chain of ZWJ (U+200D) and variation selectors. Refusing those
|
|
589
|
-
* outright would refuse names that are written the only way their script writes
|
|
590
|
-
* them, so they are judged by where they sit rather than by what they are.
|
|
591
|
-
*/
|
|
592
|
-
const ZERO_WIDTH_JOINER_PATTERN = /\u200c|\u200d/u;
|
|
593
|
-
const VARIATION_SELECTOR_PATTERN = /[\u{fe00}-\u{fe0f}]|[\u{e0100}-\u{e01ef}]/u;
|
|
594
|
-
/**
|
|
595
|
-
* The characters a joiner has work to do beside: the scripts whose words are
|
|
596
|
-
* written with one, and the pictographs an emoji sequence is built from.
|
|
597
|
-
*
|
|
598
|
-
* A list of what may join rather than of what may not, because the two are not
|
|
599
|
-
* the same size. `pdf` with a ZWNJ between the d and the f is `pdf` on screen
|
|
600
|
-
* and a different directory underneath, and the same is true of `設定` with a
|
|
601
|
-
* ZWJ after the first character: neither Latin nor Han joins anything that way,
|
|
602
|
-
* and nor does Cyrillic, Greek, Hangul or kana. Naming the scripts that do —
|
|
603
|
-
* the Arabic family, the Indic ones, Mongolian, and the pictographs — is what
|
|
604
|
-
* keeps the exception to the names that need it, instead of handing it to every
|
|
605
|
-
* writing system that is merely not Latin.
|
|
606
|
-
*/
|
|
607
|
-
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;
|
|
608
|
-
/** Non-global copies, because `test` on a global regex carries state between calls. */
|
|
609
|
-
const CONTROL_CHARACTER_PATTERN = new RegExp(CONTROL_CHARACTERS_PATTERN.source, "u");
|
|
610
|
-
const INVISIBLE_CHARACTER_PATTERN = new RegExp(INVISIBLE_CHARACTERS_PATTERN.source, "u");
|
|
611
|
-
/**
|
|
612
|
-
* Whether `text` carries a hidden character that is there to hide something.
|
|
613
|
-
*
|
|
614
|
-
* This is the question a name has to answer before it can be offered as
|
|
615
|
-
* something to pick, and it is a narrower one than `stripHiddenCharacters`
|
|
616
|
-
* answers. Every control character counts, and so does every invisible
|
|
617
|
-
* character — except a joiner or variation selector standing where its own
|
|
618
|
-
* script would put one, which is to say beside a character from a script that
|
|
619
|
-
* is written with joiners, or beside a pictograph.
|
|
620
|
-
*
|
|
621
|
-
* A joiner is held to both of its neighbors, since it exists to bind two
|
|
622
|
-
* characters and a name that ends in one is binding nothing: `設定` with a ZWJ
|
|
623
|
-
* after it is `設定` on screen and a second directory underneath. A variation
|
|
624
|
-
* selector is held only to the character before it, which is the one it selects
|
|
625
|
-
* a form for, and which is why an emoji name may end in one.
|
|
626
|
-
*
|
|
627
|
-
* Han is not on the joining list, so an ideographic variation sequence — a Han
|
|
628
|
-
* character followed by one of U+E0100 onward — is refused along with the rest.
|
|
629
|
-
* That is the intended trade: no skill directory here is named with one, and
|
|
630
|
-
* the pair is drawn as the bare character on every terminal that has no font
|
|
631
|
-
* for the variant, which is the shape the check exists to refuse.
|
|
632
|
-
*
|
|
633
|
-
* The test is a heuristic in place of the CONTEXTJ joining rules of IDNA,
|
|
634
|
-
* which decide the same question by the joining type of the characters around
|
|
635
|
-
* the joiner. It errs toward accepting a name written in a script that needs
|
|
636
|
-
* these characters, and toward rejecting one that mixes them into Latin, where
|
|
637
|
-
* they can only be padding.
|
|
638
|
-
*/
|
|
639
|
-
function hasDeceptiveHiddenCharacters(text) {
|
|
640
|
-
const characters = [...text];
|
|
641
|
-
const joinsCharacter = (neighbor) => neighbor !== void 0 && JOINING_CONTEXT_PATTERN.test(neighbor);
|
|
642
|
-
return characters.some((character, index) => {
|
|
643
|
-
if (CONTROL_CHARACTER_PATTERN.test(character)) return true;
|
|
644
|
-
if (!INVISIBLE_CHARACTER_PATTERN.test(character)) return false;
|
|
645
|
-
if (VARIATION_SELECTOR_PATTERN.test(character)) return !joinsCharacter(characters[index - 1]);
|
|
646
|
-
if (!ZERO_WIDTH_JOINER_PATTERN.test(character)) return true;
|
|
647
|
-
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
//#endregion
|
|
651
768
|
//#region src/utils/vitest.ts
|
|
652
769
|
function isEnvTest() {
|
|
653
770
|
return process.env.NODE_ENV === "test";
|
|
@@ -1004,7 +1121,16 @@ async function isSymlink(filepath) {
|
|
|
1004
1121
|
return false;
|
|
1005
1122
|
}
|
|
1006
1123
|
}
|
|
1007
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* Every entry name directly under `dir`, of whatever kind, in the order the
|
|
1126
|
+
* filesystem reports them.
|
|
1127
|
+
*
|
|
1128
|
+
* Named for what it returns, because {@link listFileNames} sits beside it and
|
|
1129
|
+
* answers a narrower question: this one reports directories and links as well
|
|
1130
|
+
* as files, and reads an unreadable directory as an empty one instead of
|
|
1131
|
+
* failing. Picking this where the other was meant brings both of those back.
|
|
1132
|
+
*/
|
|
1133
|
+
async function listDirectoryEntryNames(dir) {
|
|
1008
1134
|
try {
|
|
1009
1135
|
return await readdir(dir);
|
|
1010
1136
|
} catch {
|
|
@@ -1016,6 +1142,20 @@ function countHiddenSegments(filePath) {
|
|
|
1016
1142
|
return splitPathSegments(filePath).filter(isHiddenPathSegment).length;
|
|
1017
1143
|
}
|
|
1018
1144
|
/**
|
|
1145
|
+
* A path the running platform produced, written posix-separated.
|
|
1146
|
+
*
|
|
1147
|
+
* Not {@link toPosixPath}, which rewrites every backslash whatever it means. That
|
|
1148
|
+
* is right for a path a caller spelled, which may be spelled either way; it is
|
|
1149
|
+
* wrong for one the platform handed back. On a posix platform a backslash is an
|
|
1150
|
+
* ordinary character in a name, so rewriting it folds `a\b` onto `a/b` -- two
|
|
1151
|
+
* different directories, and wherever the result is used as a file's identity the
|
|
1152
|
+
* second one to be seen is taken for a repeat of the first and dropped. On
|
|
1153
|
+
* Windows no name can hold a backslash, so the rewrite there is lossless.
|
|
1154
|
+
*/
|
|
1155
|
+
function nativePathToPosix(filePath) {
|
|
1156
|
+
return sep === "\\" ? filePath.replaceAll("\\", "/") : filePath;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1019
1159
|
* The real file a path denotes, posix-separated so it compares against the globby results
|
|
1020
1160
|
* that produce it. Two paths share an identity when they resolve to the very same file --
|
|
1021
1161
|
* a link beside its target, a link into a shared tree, or a cycle that walks back into an
|
|
@@ -1023,19 +1163,61 @@ function countHiddenSegments(filePath) {
|
|
|
1023
1163
|
*/
|
|
1024
1164
|
async function realFileIdentity(filePath) {
|
|
1025
1165
|
try {
|
|
1026
|
-
return
|
|
1166
|
+
return nativePathToPosix(await realpath(filePath));
|
|
1027
1167
|
} catch {
|
|
1028
|
-
return
|
|
1168
|
+
return nativePathToPosix(filePath);
|
|
1029
1169
|
}
|
|
1030
1170
|
}
|
|
1031
1171
|
/**
|
|
1172
|
+
* Where the file `targetPath` really denotes sits relative to the one `rootPath`
|
|
1173
|
+
* does, with every link on both sides resolved. Posix-separated, because the
|
|
1174
|
+
* resolved paths it is built from are; a caller that splits it must split on `/`
|
|
1175
|
+
* alone, which is also the only separator a real name can never contain.
|
|
1176
|
+
*
|
|
1177
|
+
* Both sides fall back to their literal path when they cannot be resolved, so an
|
|
1178
|
+
* unresolvable path reads as an escape rather than as a contained one.
|
|
1179
|
+
*/
|
|
1180
|
+
async function resolvedRelativePath({ rootPath, targetPath }) {
|
|
1181
|
+
const [realRootPath, realTargetPath] = await Promise.all([realFileIdentity(rootPath), realFileIdentity(targetPath)]);
|
|
1182
|
+
return posix.relative(realRootPath, realTargetPath);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Whether a posix relative path -- one {@link resolvedRelativePath} returned, or any
|
|
1186
|
+
* built the same way -- leads out of the root it was taken against.
|
|
1187
|
+
*
|
|
1188
|
+
* Exported so a caller that already holds the relative path can ask this of the very
|
|
1189
|
+
* path it goes on to read, instead of resolving both sides a second time and judging
|
|
1190
|
+
* a result it then has to trust is the same one.
|
|
1191
|
+
*/
|
|
1192
|
+
function posixRelativePathEscapesRoot(relativePath) {
|
|
1193
|
+
return relativePath === ".." || relativePath.startsWith("../") || posix.isAbsolute(relativePath);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* How many trailing segments `filePath` and `identity` have in common.
|
|
1197
|
+
*
|
|
1198
|
+
* A path that walked through no link at all shares all of its own segments with
|
|
1199
|
+
* the file's identity; one that walked through a link named differently from
|
|
1200
|
+
* its target parts from the identity at that segment and shares only what
|
|
1201
|
+
* follows it. Counting from the end rather than testing the two for equality is
|
|
1202
|
+
* what lets the comparison hold for a path that is not itself resolved: a glob
|
|
1203
|
+
* rooted at a directory that is a link of its own gives every candidate the
|
|
1204
|
+
* same unresolved prefix, and only the segments below it decide.
|
|
1205
|
+
*/
|
|
1206
|
+
function sharedTrailingSegments(filePath, identity) {
|
|
1207
|
+
const left = splitPathSegments(nativePathToPosix(filePath));
|
|
1208
|
+
const right = splitPathSegments(identity);
|
|
1209
|
+
let shared = 0;
|
|
1210
|
+
while (shared < left.length && shared < right.length && left[left.length - 1 - shared] === right[right.length - 1 - shared]) shared++;
|
|
1211
|
+
return shared;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1032
1214
|
* Pick the one path that represents a file among the paths that resolve to it.
|
|
1033
1215
|
*
|
|
1034
|
-
* The path that walked through no link at all wins outright: it
|
|
1035
|
-
*
|
|
1036
|
-
* callers see, rather than an alias that happens to sort first -- a
|
|
1037
|
-
* `aaa` pointing at `zzz` must not make `zzz/x.md` disappear, and a
|
|
1038
|
-
* `sub/note.md` with the same file reached back through the cycle.
|
|
1216
|
+
* The path that walked through no link at all wins outright: it shares every one of its
|
|
1217
|
+
* segments with the file's identity, which no alias does. That keeps the real location of
|
|
1218
|
+
* a file as the path callers see, rather than an alias that happens to sort first -- a
|
|
1219
|
+
* directory link named `aaa` pointing at `zzz` must not make `zzz/x.md` disappear, and a
|
|
1220
|
+
* cycle must not replace `sub/note.md` with the same file reached back through the cycle.
|
|
1039
1221
|
* Failing that, the fewest dot-prefixed segments wins: when only links are on offer, the
|
|
1040
1222
|
* named one represents the entry rather than a hidden alias that a hidden-entry rule may
|
|
1041
1223
|
* then drop, taking the named path's content with it. `candidates` arrives in sorted
|
|
@@ -1043,8 +1225,9 @@ async function realFileIdentity(filePath) {
|
|
|
1043
1225
|
*/
|
|
1044
1226
|
function chooseRepresentative(candidates, identity) {
|
|
1045
1227
|
return candidates.reduce((best, candidate) => {
|
|
1046
|
-
|
|
1047
|
-
|
|
1228
|
+
const bestShared = sharedTrailingSegments(best, identity);
|
|
1229
|
+
const candidateShared = sharedTrailingSegments(candidate, identity);
|
|
1230
|
+
if (candidateShared !== bestShared) return candidateShared > bestShared ? candidate : best;
|
|
1048
1231
|
return countHiddenSegments(candidate) < countHiddenSegments(best) ? candidate : best;
|
|
1049
1232
|
});
|
|
1050
1233
|
}
|
|
@@ -1142,7 +1325,7 @@ async function dedupeNamesByFileIdentity(params) {
|
|
|
1142
1325
|
});
|
|
1143
1326
|
const entriesByIdentity = /* @__PURE__ */ new Map();
|
|
1144
1327
|
for (const [index, entry] of entries.entries()) {
|
|
1145
|
-
const identity = identities[index] ??
|
|
1328
|
+
const identity = identities[index] ?? nativePathToPosix(join(dirPath, entry.name));
|
|
1146
1329
|
const group = entriesByIdentity.get(identity);
|
|
1147
1330
|
if (group === void 0) entriesByIdentity.set(identity, [entry]);
|
|
1148
1331
|
else group.push(entry);
|
|
@@ -1194,6 +1377,94 @@ async function listFileNames(dirPath, options = {}) {
|
|
|
1194
1377
|
nameFilter: options.nameFilter
|
|
1195
1378
|
});
|
|
1196
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* The paths of every file below `dirPath`, relative to it, walked rather than
|
|
1382
|
+
* globbed.
|
|
1383
|
+
*
|
|
1384
|
+
* The recursive counterpart of {@link listFileNames}, and there for the same
|
|
1385
|
+
* reason: globby reads a backslash as a path separator and rewrites it in the
|
|
1386
|
+
* paths it returns, so a file named `back\\slash.md` comes back as
|
|
1387
|
+
* `back/slash.md` — a path that belongs to no file, under a name that belongs
|
|
1388
|
+
* to no file either.
|
|
1389
|
+
*
|
|
1390
|
+
* A root that is not there is an empty root. A root that is there but cannot be
|
|
1391
|
+
* read is reported, so it is never mistaken for an empty one — as is a
|
|
1392
|
+
* directory below it, since a subtree silently missing from the result is the
|
|
1393
|
+
* same mistake one level down.
|
|
1394
|
+
*
|
|
1395
|
+
* A directory link is followed like any other directory, but each real
|
|
1396
|
+
* directory is walked only once, so neither a cycle nor a mesh of links can
|
|
1397
|
+
* make the walk repeat itself -- taking every distinct route through a graph of
|
|
1398
|
+
* aliases would cost one traversal per route, which a handful of links is
|
|
1399
|
+
* enough to make hopeless. The name a twice-reachable directory is reported
|
|
1400
|
+
* under is therefore whichever the walk reaches first, and the walk goes level
|
|
1401
|
+
* by level with each level sorted, so that is the shortest path to it and the
|
|
1402
|
+
* first in sorted order among equals. Note the difference from
|
|
1403
|
+
* {@link findFilesByGlobs}, which resolves each of its results and keeps the
|
|
1404
|
+
* real one: here a link nearer the root than the directory it points at stands
|
|
1405
|
+
* in for it. `nameFilter` narrows the files, not the directories the walk
|
|
1406
|
+
* descends into.
|
|
1407
|
+
*
|
|
1408
|
+
* The cost is one round of `readdir` per directory, taken in sequence, and the
|
|
1409
|
+
* whole of a level is held at once, so residency follows the widest level
|
|
1410
|
+
* rather than the deepest path.
|
|
1411
|
+
*/
|
|
1412
|
+
async function listFilePathsRecursively(dirPath, options = {}) {
|
|
1413
|
+
const { followSymbolicLinks = true, includeHidden = false, nameFilter, deduplicateByFileIdentity = false } = options;
|
|
1414
|
+
if (!await directoryExists(dirPath)) return [];
|
|
1415
|
+
const filePaths = [];
|
|
1416
|
+
const walkedIdentities = /* @__PURE__ */ new Set();
|
|
1417
|
+
let level = [{
|
|
1418
|
+
currentPath: dirPath,
|
|
1419
|
+
prefix: ""
|
|
1420
|
+
}];
|
|
1421
|
+
while (level.length > 0) {
|
|
1422
|
+
const nextLevel = [];
|
|
1423
|
+
for (const { currentPath, prefix } of level.toSorted((a, b) => a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0)) {
|
|
1424
|
+
const identity = await realFileIdentity(currentPath);
|
|
1425
|
+
if (walkedIdentities.has(identity)) continue;
|
|
1426
|
+
walkedIdentities.add(identity);
|
|
1427
|
+
const [fileNames, dirNames] = await Promise.all([listFileNames(currentPath, {
|
|
1428
|
+
followSymbolicLinks,
|
|
1429
|
+
includeHidden,
|
|
1430
|
+
nameFilter
|
|
1431
|
+
}), listSubdirectoryNames(currentPath, {
|
|
1432
|
+
followSymbolicLinks,
|
|
1433
|
+
includeHidden
|
|
1434
|
+
})]);
|
|
1435
|
+
for (const fileName of fileNames) filePaths.push(prefix === "" ? fileName : join(prefix, fileName));
|
|
1436
|
+
for (const dirName of dirNames) nextLevel.push({
|
|
1437
|
+
currentPath: join(currentPath, dirName),
|
|
1438
|
+
prefix: prefix === "" ? dirName : join(prefix, dirName)
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
level = nextLevel;
|
|
1442
|
+
}
|
|
1443
|
+
return deduplicateByFileIdentity ? await deduplicateRelativePathsByFileIdentity({
|
|
1444
|
+
dirPath,
|
|
1445
|
+
relativePaths: filePaths
|
|
1446
|
+
}) : filePaths.toSorted();
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* One path per real file, chosen the way {@link findFilesByGlobs} chooses it.
|
|
1450
|
+
*
|
|
1451
|
+
* The walk de-duplicates the directories it descends into, so it cannot loop,
|
|
1452
|
+
* but it still reports every name it walks past: a file reached under two names
|
|
1453
|
+
* is listed twice. A caller reading the result as a set of files has to fold
|
|
1454
|
+
* those aliases together, and has to fold them the same way the glob does, or
|
|
1455
|
+
* the two disagree about the name a file has.
|
|
1456
|
+
*/
|
|
1457
|
+
async function deduplicateRelativePathsByFileIdentity({ dirPath, relativePaths }) {
|
|
1458
|
+
const candidatesByFile = /* @__PURE__ */ new Map();
|
|
1459
|
+
for (const relativePath of relativePaths.toSorted()) {
|
|
1460
|
+
const absolutePath = join(dirPath, relativePath);
|
|
1461
|
+
const identity = await realFileIdentity(absolutePath);
|
|
1462
|
+
const candidates = candidatesByFile.get(identity);
|
|
1463
|
+
if (candidates === void 0) candidatesByFile.set(identity, [absolutePath]);
|
|
1464
|
+
else candidates.push(absolutePath);
|
|
1465
|
+
}
|
|
1466
|
+
return [...candidatesByFile.entries()].map(([identity, candidates]) => relative(dirPath, chooseRepresentative(candidates, identity))).toSorted();
|
|
1467
|
+
}
|
|
1197
1468
|
async function removeDirectory(dirPath) {
|
|
1198
1469
|
if ([
|
|
1199
1470
|
".",
|
|
@@ -1355,24 +1626,111 @@ var CLIError = class extends Error {
|
|
|
1355
1626
|
//#region src/utils/warned-once.ts
|
|
1356
1627
|
/**
|
|
1357
1628
|
* The messages a once-per-run warning has already emitted in this process.
|
|
1358
|
-
* This lives in its own module,
|
|
1359
|
-
* clear it between tests without pulling `logger.js` into every
|
|
1360
|
-
* graph (which would defeat the module mocks some of those tests
|
|
1629
|
+
* This lives in its own module, importing nothing of rulesync's, so the vitest
|
|
1630
|
+
* setup file can clear it between tests without pulling `logger.js` into every
|
|
1631
|
+
* test's module graph (which would defeat the module mocks some of those tests
|
|
1632
|
+
* install).
|
|
1361
1633
|
*/
|
|
1362
|
-
const
|
|
1634
|
+
const processWideMessages = /* @__PURE__ */ new Set();
|
|
1635
|
+
/**
|
|
1636
|
+
* The set an operation that opened its own scope uses instead.
|
|
1637
|
+
*
|
|
1638
|
+
* The MCP server does not serialize requests, so two runs can be in flight at
|
|
1639
|
+
* once. Sharing one set between them would let the first run spend the token
|
|
1640
|
+
* for a message and leave the second one's result silent about a diagnostic
|
|
1641
|
+
* that applies to it just as much. A scope gives each run its own bookkeeping.
|
|
1642
|
+
*/
|
|
1643
|
+
const scopedMessages = new AsyncLocalStorage();
|
|
1644
|
+
function currentMessages() {
|
|
1645
|
+
return scopedMessages.getStore() ?? processWideMessages;
|
|
1646
|
+
}
|
|
1363
1647
|
/** Whether `message` has not been emitted yet; records it when it has not. */
|
|
1364
1648
|
function claimWarnOnce(message) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1649
|
+
const messages = currentMessages();
|
|
1650
|
+
if (messages.has(message)) return false;
|
|
1651
|
+
messages.add(message);
|
|
1367
1652
|
return true;
|
|
1368
1653
|
}
|
|
1369
|
-
/** Forget which warnings were already emitted, so
|
|
1654
|
+
/** Forget which warnings were already emitted, so the next run starts silent. */
|
|
1370
1655
|
function resetWarnedOnceMessages() {
|
|
1371
|
-
|
|
1656
|
+
currentMessages().clear();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Run `operation` with its own once-per-run bookkeeping, so a concurrent run
|
|
1660
|
+
* neither spends its tokens nor clears its record.
|
|
1661
|
+
*/
|
|
1662
|
+
async function withWarnOnceScope(operation) {
|
|
1663
|
+
return await scopedMessages.run(/* @__PURE__ */ new Set(), operation);
|
|
1372
1664
|
}
|
|
1373
1665
|
//#endregion
|
|
1374
1666
|
//#region src/utils/logger.ts
|
|
1375
1667
|
/**
|
|
1668
|
+
* Formats a log line the way `console.warn` would, so a warning that is handed
|
|
1669
|
+
* back to a caller reads the same as the one that reaches a terminal.
|
|
1670
|
+
*/
|
|
1671
|
+
function formatLogLine({ message, args }) {
|
|
1672
|
+
return args.length === 0 ? message : format(message, ...args);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* How much a collecting logger keeps: at most this many warnings, each at most
|
|
1676
|
+
* this long, and no more than this in total.
|
|
1677
|
+
*
|
|
1678
|
+
* Collected warnings travel to places a console line does not — a `--json`
|
|
1679
|
+
* document that another program parses, an MCP result that an agent reads as
|
|
1680
|
+
* context — and their text quotes files rulesync did not write. So the amount a
|
|
1681
|
+
* repository can push through has to be bounded twice over: a config with
|
|
1682
|
+
* thousands of odd keys is a plausible accident, and a report sized in hundreds
|
|
1683
|
+
* of kilobytes is a generous budget for text aimed at whoever reads it next.
|
|
1684
|
+
* The total is the binding limit; the per-line and per-count limits keep one
|
|
1685
|
+
* enormous warning, or one enormous number of them, from being the whole of it.
|
|
1686
|
+
*/
|
|
1687
|
+
const MAX_COLLECTED_WARNINGS = 100;
|
|
1688
|
+
const MAX_COLLECTED_WARNING_LENGTH = 1e3;
|
|
1689
|
+
const MAX_COLLECTED_TOTAL_LENGTH = 8e3;
|
|
1690
|
+
/**
|
|
1691
|
+
* How many distinct warnings the de-duplication remembers.
|
|
1692
|
+
*
|
|
1693
|
+
* The record has to outlive the reported lines — a line dropped for want of
|
|
1694
|
+
* budget must not be counted again the next time the same diagnostic repeats —
|
|
1695
|
+
* so it grows with the number of distinct warnings a run raises rather than
|
|
1696
|
+
* with the number reported. Bounded for the same reason everything else here
|
|
1697
|
+
* is: past this many, later repeats are counted rather than recognized, which
|
|
1698
|
+
* inflates the trailing count but cannot grow the record without end.
|
|
1699
|
+
*/
|
|
1700
|
+
const MAX_DEDUPLICATED_WARNINGS = 1e3;
|
|
1701
|
+
/**
|
|
1702
|
+
* A bounded list of warning lines.
|
|
1703
|
+
*/
|
|
1704
|
+
var WarningCollection = class {
|
|
1705
|
+
lines = [];
|
|
1706
|
+
seen = /* @__PURE__ */ new Set();
|
|
1707
|
+
totalLength = 0;
|
|
1708
|
+
omitted = 0;
|
|
1709
|
+
add({ message, args }) {
|
|
1710
|
+
const kept = truncateText({
|
|
1711
|
+
text: stripControlCharacters(formatLogLine({
|
|
1712
|
+
message,
|
|
1713
|
+
args
|
|
1714
|
+
})),
|
|
1715
|
+
maxLength: MAX_COLLECTED_WARNING_LENGTH,
|
|
1716
|
+
suffix: "…(truncated)"
|
|
1717
|
+
});
|
|
1718
|
+
if (this.seen.has(kept)) return;
|
|
1719
|
+
if (this.lines.length >= MAX_COLLECTED_WARNINGS || this.totalLength + kept.length > MAX_COLLECTED_TOTAL_LENGTH) {
|
|
1720
|
+
if (this.seen.size < MAX_DEDUPLICATED_WARNINGS) this.seen.add(kept);
|
|
1721
|
+
this.omitted++;
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
this.seen.add(kept);
|
|
1725
|
+
this.lines.push(kept);
|
|
1726
|
+
this.totalLength += kept.length;
|
|
1727
|
+
}
|
|
1728
|
+
toArray() {
|
|
1729
|
+
if (this.omitted === 0) return [...this.lines];
|
|
1730
|
+
return [...this.lines, `… and ${this.omitted} more warning(s) not reported`];
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
/**
|
|
1376
1734
|
* Base class for shared verbose/silent state and configuration logic
|
|
1377
1735
|
*/
|
|
1378
1736
|
var BaseLogger = class {
|
|
@@ -1388,6 +1746,9 @@ var BaseLogger = class {
|
|
|
1388
1746
|
get silent() {
|
|
1389
1747
|
return this._silent;
|
|
1390
1748
|
}
|
|
1749
|
+
get reportsWhileSilent() {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1391
1752
|
configure({ verbose, silent }) {
|
|
1392
1753
|
this._silent = silent;
|
|
1393
1754
|
this._verbose = verbose && !silent;
|
|
@@ -1433,11 +1794,18 @@ var ConsoleLogger = class extends BaseLogger {
|
|
|
1433
1794
|
/**
|
|
1434
1795
|
* JsonLogger - structured JSON output to stdout/stderr
|
|
1435
1796
|
*
|
|
1436
|
-
*
|
|
1797
|
+
* The console output methods (info, success, debug) are no-ops. `warn` is not:
|
|
1798
|
+
* a diagnostic that only reached the console would be invisible to a `--json`
|
|
1799
|
+
* consumer, which reads the document and nothing else, so warnings are
|
|
1800
|
+
* collected and emitted as the document's top-level `warnings` array instead.
|
|
1801
|
+
* Top-level rather than inside `data` so it can never collide with a key a
|
|
1802
|
+
* command captured, and so it survives on the failure document too — the case
|
|
1803
|
+
* where a diagnostic about the input is most likely to explain the failure.
|
|
1437
1804
|
*/
|
|
1438
1805
|
var JsonLogger = class extends BaseLogger {
|
|
1439
1806
|
_jsonOutputDone = false;
|
|
1440
1807
|
_jsonData = {};
|
|
1808
|
+
_warnings = new WarningCollection();
|
|
1441
1809
|
_commandName;
|
|
1442
1810
|
_version;
|
|
1443
1811
|
constructor({ command, version, verbose = false, silent = false }) {
|
|
@@ -1466,6 +1834,8 @@ var JsonLogger = class extends BaseLogger {
|
|
|
1466
1834
|
command: this._commandName,
|
|
1467
1835
|
version: this._version
|
|
1468
1836
|
};
|
|
1837
|
+
const warnings = this._warnings.toArray();
|
|
1838
|
+
if (warnings.length > 0) output.warnings = warnings;
|
|
1469
1839
|
if (success) output.data = this._jsonData;
|
|
1470
1840
|
else if (error) {
|
|
1471
1841
|
output.error = {
|
|
@@ -1481,7 +1851,13 @@ var JsonLogger = class extends BaseLogger {
|
|
|
1481
1851
|
}
|
|
1482
1852
|
info(_message, ..._args) {}
|
|
1483
1853
|
success(_message, ..._args) {}
|
|
1484
|
-
warn(
|
|
1854
|
+
warn(message, ...args) {
|
|
1855
|
+
if (this._silent) return;
|
|
1856
|
+
this._warnings.add({
|
|
1857
|
+
message,
|
|
1858
|
+
args
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1485
1861
|
error(message, code, ..._args) {
|
|
1486
1862
|
if (isEnvTest()) return;
|
|
1487
1863
|
const errorMessage = message instanceof Error ? message.message : message;
|
|
@@ -1507,13 +1883,110 @@ function warnOnConflictingFlags({ verbose, silent, jsonMode }) {
|
|
|
1507
1883
|
console.warn("Both --verbose and --silent specified; --silent takes precedence");
|
|
1508
1884
|
}
|
|
1509
1885
|
/**
|
|
1886
|
+
* Where `fallbackLogger` sends what it is given, scoped to the operation that
|
|
1887
|
+
* adopted it.
|
|
1888
|
+
*
|
|
1889
|
+
* An `AsyncLocalStorage` rather than a plain variable because the target is
|
|
1890
|
+
* per-operation, not per-process: a long-lived MCP server can have two requests
|
|
1891
|
+
* in flight at once, and a save/restore pair would let the first one to finish
|
|
1892
|
+
* hand the still-running request's warnings back to a console nobody reads.
|
|
1893
|
+
* Each operation sees only its own store.
|
|
1894
|
+
*/
|
|
1895
|
+
const fallbackTargetStorage = new AsyncLocalStorage();
|
|
1896
|
+
/**
|
|
1897
|
+
* Where warnings go outside any adopted scope: a plain console logger, which is
|
|
1898
|
+
* what a code path with no logger threaded through would otherwise have used.
|
|
1899
|
+
*/
|
|
1900
|
+
const defaultFallbackTarget = new ConsoleLogger();
|
|
1901
|
+
function currentFallbackTarget() {
|
|
1902
|
+
return fallbackTargetStorage.getStore() ?? defaultFallbackTarget;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* True while the forwarder is inside a call it is forwarding.
|
|
1906
|
+
*
|
|
1907
|
+
* The adopted target is supposed to be something other than the forwarder, but
|
|
1908
|
+
* a wrapper *around* it — `hooks-processor.ts` returns one that prefixes the
|
|
1909
|
+
* tool target onto every warning — passes the identity check in
|
|
1910
|
+
* {@link withFallbackLoggerTarget} and would forward straight back here. A
|
|
1911
|
+
* plain module-level flag is enough because the forwarding is synchronous: the
|
|
1912
|
+
* call returns before anything else can run.
|
|
1913
|
+
*/
|
|
1914
|
+
let forwarding = false;
|
|
1915
|
+
/**
|
|
1916
|
+
* Reads from, or writes to, whichever logger the running operation adopted,
|
|
1917
|
+
* with the wrapper case above cut off at one hop.
|
|
1918
|
+
*
|
|
1919
|
+
* Every member of the forwarder goes through here, not just the two that write:
|
|
1920
|
+
* `warnOnceWithFallback` reads `silent` and `reportsWhileSilent` before it ever
|
|
1921
|
+
* calls `warn`, so a guard on the writing side alone would still be reached
|
|
1922
|
+
* through a getter that never returns.
|
|
1923
|
+
*/
|
|
1924
|
+
function throughFallbackTarget(use) {
|
|
1925
|
+
if (forwarding) return use(defaultFallbackTarget);
|
|
1926
|
+
forwarding = true;
|
|
1927
|
+
try {
|
|
1928
|
+
return use(currentFallbackTarget());
|
|
1929
|
+
} finally {
|
|
1930
|
+
forwarding = false;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1510
1934
|
* Shared fallback logger for code paths that have no command logger threaded
|
|
1511
1935
|
* through (module-level translators, `warnWithFallback(undefined, ...)`).
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1936
|
+
*
|
|
1937
|
+
* It is a thin forwarder rather than a logger of its own so that the operation
|
|
1938
|
+
* currently running can adopt it: `wrapCommand` points it at the command
|
|
1939
|
+
* logger, which is how a warning raised deep in a translator still reaches a
|
|
1940
|
+
* `--json` document or an MCP result instead of being written to a console that
|
|
1941
|
+
* nobody in those modes is reading. Modules that captured a reference to
|
|
1942
|
+
* `fallbackLogger` at import time follow the redirection too, which a
|
|
1943
|
+
* swapped-out binding would not give us.
|
|
1944
|
+
*/
|
|
1945
|
+
const fallbackLogger = {
|
|
1946
|
+
configure(options) {
|
|
1947
|
+
defaultFallbackTarget.configure(options);
|
|
1948
|
+
},
|
|
1949
|
+
get verbose() {
|
|
1950
|
+
return throughFallbackTarget((target) => target.verbose);
|
|
1951
|
+
},
|
|
1952
|
+
get silent() {
|
|
1953
|
+
return throughFallbackTarget((target) => target.silent);
|
|
1954
|
+
},
|
|
1955
|
+
get reportsWhileSilent() {
|
|
1956
|
+
return throughFallbackTarget((target) => target.reportsWhileSilent);
|
|
1957
|
+
},
|
|
1958
|
+
get jsonMode() {
|
|
1959
|
+
return throughFallbackTarget((target) => target.jsonMode);
|
|
1960
|
+
},
|
|
1961
|
+
captureData(_key, _value) {},
|
|
1962
|
+
getJsonData() {
|
|
1963
|
+
return {};
|
|
1964
|
+
},
|
|
1965
|
+
outputJson(_success, _error) {},
|
|
1966
|
+
info(_message, ..._args) {},
|
|
1967
|
+
success(_message, ..._args) {},
|
|
1968
|
+
warn(message, ...args) {
|
|
1969
|
+
throughFallbackTarget((target) => {
|
|
1970
|
+
target.warn(message, ...args);
|
|
1971
|
+
});
|
|
1972
|
+
},
|
|
1973
|
+
error(message, code, ...args) {
|
|
1974
|
+
defaultFallbackTarget.error(message, code, ...args);
|
|
1975
|
+
},
|
|
1976
|
+
debug(_message, ..._args) {}
|
|
1977
|
+
};
|
|
1978
|
+
/**
|
|
1979
|
+
* Run `operation` with `fallbackLogger` pointed at `logger`, so warnings raised
|
|
1980
|
+
* where no logger was threaded through end up in the same place as the rest of
|
|
1981
|
+
* that operation's diagnostics.
|
|
1982
|
+
*
|
|
1983
|
+
* The redirection lasts exactly as long as the operation and is invisible to
|
|
1984
|
+
* anything running beside it.
|
|
1515
1985
|
*/
|
|
1516
|
-
|
|
1986
|
+
async function withFallbackLoggerTarget({ logger, operation }) {
|
|
1987
|
+
if (logger === fallbackLogger) return await operation();
|
|
1988
|
+
return await fallbackTargetStorage.run(logger, () => withWarnOnceScope(operation));
|
|
1989
|
+
}
|
|
1517
1990
|
/**
|
|
1518
1991
|
* Emit a warning through `logger.warn` if a logger is supplied, otherwise
|
|
1519
1992
|
* fall through to the shared `fallbackLogger`. Centralizes the "logger may
|
|
@@ -1531,9 +2004,35 @@ function warnWithFallback(logger, message) {
|
|
|
1531
2004
|
* varies with what the user should do next does not.
|
|
1532
2005
|
*/
|
|
1533
2006
|
function warnOnceWithFallback(logger, message) {
|
|
2007
|
+
const destination = logger ?? fallbackLogger;
|
|
2008
|
+
if (destination.silent && !destination.reportsWhileSilent) return;
|
|
1534
2009
|
if (!claimWarnOnce(message)) return;
|
|
1535
|
-
|
|
2010
|
+
destination.warn(message);
|
|
1536
2011
|
}
|
|
2012
|
+
/**
|
|
2013
|
+
* A `ConsoleLogger` that keeps the warnings it is given.
|
|
2014
|
+
*
|
|
2015
|
+
* A caller with no console to write to — an MCP tool answering over stdio, where
|
|
2016
|
+
* the server's stderr never reaches the agent — can hand this in and put what
|
|
2017
|
+
* was reported into its own result, so a diagnostic about the files it just read
|
|
2018
|
+
* is something the agent can act on rather than something it never hears.
|
|
2019
|
+
*/
|
|
2020
|
+
var WarningCollectingLogger = class extends ConsoleLogger {
|
|
2021
|
+
warnings = new WarningCollection();
|
|
2022
|
+
get reportsWhileSilent() {
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
warn(message, ...args) {
|
|
2026
|
+
this.warnings.add({
|
|
2027
|
+
message,
|
|
2028
|
+
args
|
|
2029
|
+
});
|
|
2030
|
+
super.warn(message, ...args);
|
|
2031
|
+
}
|
|
2032
|
+
getWarnings() {
|
|
2033
|
+
return this.warnings.toArray();
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
1537
2036
|
//#endregion
|
|
1538
2037
|
//#region src/utils/validation.ts
|
|
1539
2038
|
/**
|
|
@@ -4540,6 +5039,17 @@ function parseStrict(content) {
|
|
|
4540
5039
|
return result;
|
|
4541
5040
|
}
|
|
4542
5041
|
/**
|
|
5042
|
+
* The error a source document should fail with when
|
|
5043
|
+
* {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
|
|
5044
|
+
*
|
|
5045
|
+
* Shared by every source that reports them, so the three files a user can
|
|
5046
|
+
* author explain the same removal the same way rather than each inventing its
|
|
5047
|
+
* own wording.
|
|
5048
|
+
*/
|
|
5049
|
+
function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
|
|
5050
|
+
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.`);
|
|
5051
|
+
}
|
|
5052
|
+
/**
|
|
4543
5053
|
* The same parse as {@link parseJsonc}, additionally reporting which
|
|
4544
5054
|
* prototype-pollution keys were removed, as dotted paths
|
|
4545
5055
|
* (`permission.bash.__proto__`).
|
|
@@ -4619,9 +5129,17 @@ async function resolveRulesyncSourceWritePath({ outputRoot, paths }) {
|
|
|
4619
5129
|
//#region src/features/hooks/rulesync-hooks.ts
|
|
4620
5130
|
var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
4621
5131
|
json;
|
|
5132
|
+
/**
|
|
5133
|
+
* Prototype-pollution keys the parser removed. They are dropped before the
|
|
5134
|
+
* schema ever sees them, so without this record a hook keyed `constructor`
|
|
5135
|
+
* would produce neither an error nor an entry in any generated file.
|
|
5136
|
+
*/
|
|
5137
|
+
droppedKeys;
|
|
4622
5138
|
constructor(params) {
|
|
4623
5139
|
super({ ...params });
|
|
4624
|
-
|
|
5140
|
+
const { value, droppedKeys } = parseJsoncReportingDroppedKeys({ content: this.fileContent });
|
|
5141
|
+
this.json = value;
|
|
5142
|
+
this.droppedKeys = droppedKeys;
|
|
4625
5143
|
if (params.validate) {
|
|
4626
5144
|
const result = this.validate();
|
|
4627
5145
|
if (!result.success) throw result.error;
|
|
@@ -4640,6 +5158,13 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
4640
5158
|
};
|
|
4641
5159
|
}
|
|
4642
5160
|
validate() {
|
|
5161
|
+
if (this.droppedKeys.length > 0) return {
|
|
5162
|
+
success: false,
|
|
5163
|
+
error: droppedPollutionKeysError({
|
|
5164
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
5165
|
+
droppedKeys: this.droppedKeys
|
|
5166
|
+
})
|
|
5167
|
+
};
|
|
4643
5168
|
const result = HooksConfigSchema.safeParse(this.json);
|
|
4644
5169
|
if (!result.success) return {
|
|
4645
5170
|
success: false,
|
|
@@ -4986,9 +5511,17 @@ function mergeMcpJsonOverlays({ base, overlay }) {
|
|
|
4986
5511
|
}
|
|
4987
5512
|
var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
4988
5513
|
json;
|
|
5514
|
+
/**
|
|
5515
|
+
* Prototype-pollution keys the parser removed. They are dropped before the
|
|
5516
|
+
* schema ever sees them, so without this record a server named `__proto__`
|
|
5517
|
+
* would produce neither an error nor an entry in any generated file.
|
|
5518
|
+
*/
|
|
5519
|
+
droppedKeys;
|
|
4989
5520
|
constructor(params) {
|
|
4990
5521
|
super(params);
|
|
4991
|
-
|
|
5522
|
+
const { value, droppedKeys } = parseJsoncReportingDroppedKeys({ content: this.fileContent });
|
|
5523
|
+
this.json = value;
|
|
5524
|
+
this.droppedKeys = droppedKeys;
|
|
4992
5525
|
if (params.validate) {
|
|
4993
5526
|
const result = this.validate();
|
|
4994
5527
|
if (!result.success) throw result.error;
|
|
@@ -5010,6 +5543,13 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5010
5543
|
};
|
|
5011
5544
|
}
|
|
5012
5545
|
validate() {
|
|
5546
|
+
if (this.droppedKeys.length > 0) return {
|
|
5547
|
+
success: false,
|
|
5548
|
+
error: droppedPollutionKeysError({
|
|
5549
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
5550
|
+
droppedKeys: this.droppedKeys
|
|
5551
|
+
})
|
|
5552
|
+
};
|
|
5013
5553
|
const result = RulesyncMcpFileSchema.safeParse(this.json);
|
|
5014
5554
|
if (!result.success) return {
|
|
5015
5555
|
success: false,
|
|
@@ -5079,16 +5619,23 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5079
5619
|
}
|
|
5080
5620
|
const fileContent = await readFileContent(filePath);
|
|
5081
5621
|
let parsed;
|
|
5622
|
+
let droppedKeys;
|
|
5082
5623
|
try {
|
|
5083
|
-
|
|
5084
|
-
if (!isRecord$1(
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
if (!result.success) throw result.error;
|
|
5088
|
-
}
|
|
5624
|
+
const result = parseJsoncReportingDroppedKeys({ content: fileContent });
|
|
5625
|
+
if (!isRecord$1(result.value)) throw new Error("Expected a JSON object.");
|
|
5626
|
+
parsed = result.value;
|
|
5627
|
+
droppedKeys = result.droppedKeys;
|
|
5089
5628
|
} catch (error) {
|
|
5090
5629
|
throw new Error(`Invalid MCP source file '${filePath}': ${formatError(error)}`, { cause: error });
|
|
5091
5630
|
}
|
|
5631
|
+
if (validate) {
|
|
5632
|
+
if (droppedKeys.length > 0) throw droppedPollutionKeysError({
|
|
5633
|
+
sourcePath: toPosixPath(relative(process.cwd(), filePath)),
|
|
5634
|
+
droppedKeys
|
|
5635
|
+
});
|
|
5636
|
+
const result = RulesyncMcpFileSchema.safeParse(parsed);
|
|
5637
|
+
if (!result.success) throw new Error(`Invalid MCP source file '${filePath}': ${formatError(result.error)}`, { cause: result.error });
|
|
5638
|
+
}
|
|
5092
5639
|
rootSources.push({
|
|
5093
5640
|
record: parsed,
|
|
5094
5641
|
outputRoot: parent,
|
|
@@ -5334,12 +5881,13 @@ const PermissionActionSchema = z.enum([
|
|
|
5334
5881
|
"deny"
|
|
5335
5882
|
]);
|
|
5336
5883
|
/**
|
|
5337
|
-
* Whether a
|
|
5338
|
-
*
|
|
5339
|
-
* filter removes can never
|
|
5884
|
+
* Whether a key in a permission block — a category name or a pattern — is
|
|
5885
|
+
* blank, that is empty or only whitespace. Shared with the import-side filter
|
|
5886
|
+
* so the key the schema rejects and the key that filter removes can never
|
|
5887
|
+
* drift apart.
|
|
5340
5888
|
*/
|
|
5341
|
-
function
|
|
5342
|
-
return
|
|
5889
|
+
function isBlankPermissionKey(key) {
|
|
5890
|
+
return key.trim().length === 0;
|
|
5343
5891
|
}
|
|
5344
5892
|
/**
|
|
5345
5893
|
* A single permission pattern key.
|
|
@@ -5351,7 +5899,18 @@ function isBlankPermissionPattern(pattern) {
|
|
|
5351
5899
|
* silently ignores it. Rather than let each target decide, reject it here so
|
|
5352
5900
|
* the mistake surfaces once, on the source file.
|
|
5353
5901
|
*/
|
|
5354
|
-
const PermissionPatternSchema = z.string().check(z.refine((pattern) => !
|
|
5902
|
+
const PermissionPatternSchema = z.string().check(z.refine((pattern) => !isBlankPermissionKey(pattern), { message: "Permission pattern must not be blank" }));
|
|
5903
|
+
/**
|
|
5904
|
+
* A permission category key: the name of the tool surface a rules map applies
|
|
5905
|
+
* to (`bash`, `edit`, `webfetch`, ...).
|
|
5906
|
+
*
|
|
5907
|
+
* Blank is rejected for the same reason a blank pattern is, one step up. Every
|
|
5908
|
+
* translator reads categories by name, so `{"": {"git *": "allow"}}` reaches no
|
|
5909
|
+
* tool at all and the rules under it are silently dead — the mistake is only
|
|
5910
|
+
* visible as an entry missing from a generated config. Rejecting it here
|
|
5911
|
+
* surfaces it on the source file instead.
|
|
5912
|
+
*/
|
|
5913
|
+
const PermissionCategorySchema = z.string().check(z.refine((category) => !isBlankPermissionKey(category), { message: "Permission category must not be blank" }));
|
|
5355
5914
|
/**
|
|
5356
5915
|
* Permission rules for a single tool category.
|
|
5357
5916
|
* Keys are glob patterns matching tool input (commands, file paths, etc.).
|
|
@@ -5372,7 +5931,7 @@ const PermissionRulesSchema = z.record(PermissionPatternSchema, PermissionAction
|
|
|
5372
5931
|
* @example
|
|
5373
5932
|
* { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
|
|
5374
5933
|
*/
|
|
5375
|
-
const ToolScopedPermissionSchema = z.record(
|
|
5934
|
+
const ToolScopedPermissionSchema = z.record(PermissionCategorySchema, PermissionRulesSchema);
|
|
5376
5935
|
/**
|
|
5377
5936
|
* Generic tool-scoped override block for tools that have no tool-specific
|
|
5378
5937
|
* override keys of their own; it carries only the canonical tool-scoped
|
|
@@ -5430,7 +5989,7 @@ const OpencodeOverridePermissionValueSchema = z.union([PermissionActionSchema, P
|
|
|
5430
5989
|
* @example
|
|
5431
5990
|
* { "permission": { "external_directory": "deny", "webfetch": "allow" } }
|
|
5432
5991
|
*/
|
|
5433
|
-
const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(
|
|
5992
|
+
const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(PermissionCategorySchema, OpencodeOverridePermissionValueSchema)) });
|
|
5434
5993
|
/**
|
|
5435
5994
|
* Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
|
|
5436
5995
|
* into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
|
|
@@ -5490,7 +6049,7 @@ const ClinePermissionsOverrideSchema = z.looseObject({
|
|
|
5490
6049
|
* @see https://kilo.ai/docs/getting-started/settings/sandboxing
|
|
5491
6050
|
*/
|
|
5492
6051
|
const KiloPermissionsOverrideSchema = z.looseObject({
|
|
5493
|
-
permission: z.optional(z.record(
|
|
6052
|
+
permission: z.optional(z.record(PermissionCategorySchema, OpencodeOverridePermissionValueSchema)),
|
|
5494
6053
|
sandbox: z.optional(z.looseObject({}))
|
|
5495
6054
|
});
|
|
5496
6055
|
/**
|
|
@@ -5561,7 +6120,7 @@ const ClaudecodePermissionsOverrideSchema = z.looseObject({
|
|
|
5561
6120
|
* { "enabled_tools": ["bash", "read_file", "grep"] }
|
|
5562
6121
|
*/
|
|
5563
6122
|
const VibePermissionsOverrideSchema = z.looseObject({
|
|
5564
|
-
permission: z.optional(z.record(
|
|
6123
|
+
permission: z.optional(z.record(PermissionCategorySchema, z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))),
|
|
5565
6124
|
enabled_tools: z.optional(z.array(z.string()))
|
|
5566
6125
|
});
|
|
5567
6126
|
/**
|
|
@@ -6295,12 +6854,12 @@ const ZedPermissionsOverrideSchema = z.looseObject({
|
|
|
6295
6854
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
6296
6855
|
* Values are pattern-to-action mappings for that tool category.
|
|
6297
6856
|
*
|
|
6298
|
-
* The optional
|
|
6299
|
-
* `
|
|
6300
|
-
* `
|
|
6301
|
-
*
|
|
6302
|
-
*
|
|
6303
|
-
*
|
|
6857
|
+
* The optional tool keys below are tool-scoped overrides consumed only by their
|
|
6858
|
+
* respective translator (see the matching `*PermissionsOverrideSchema`); every
|
|
6859
|
+
* other tool reads the shared `permission` block and ignores them. The set of
|
|
6860
|
+
* keys is exactly `permissionsProcessorToolTargetTuple` mapped through
|
|
6861
|
+
* `PERMISSION_OVERRIDE_KEY_ALIASES` — a test asserts that, so this comment does
|
|
6862
|
+
* not enumerate them and go stale.
|
|
6304
6863
|
*
|
|
6305
6864
|
* Additionally, every permissions-capable tool accepts a canonical tool-scoped
|
|
6306
6865
|
* `permission` block under its override key (`{toolname}.permission`, same
|
|
@@ -6318,7 +6877,7 @@ const ZedPermissionsOverrideSchema = z.looseObject({
|
|
|
6318
6877
|
* }
|
|
6319
6878
|
*/
|
|
6320
6879
|
const PermissionsConfigSchema = z.looseObject({
|
|
6321
|
-
permission: z.record(
|
|
6880
|
+
permission: z.record(PermissionCategorySchema, PermissionRulesSchema),
|
|
6322
6881
|
opencode: z.optional(OpencodePermissionsOverrideSchema),
|
|
6323
6882
|
hermes: z.optional(HermesPermissionsOverrideSchema),
|
|
6324
6883
|
cline: z.optional(ClinePermissionsOverrideSchema),
|
|
@@ -6347,7 +6906,9 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
6347
6906
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
6348
6907
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
6349
6908
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
6350
|
-
|
|
6909
|
+
roo: z.optional(CanonicalPermissionsOverrideSchema),
|
|
6910
|
+
rovodev: z.optional(CanonicalPermissionsOverrideSchema),
|
|
6911
|
+
zoocode: z.optional(CanonicalPermissionsOverrideSchema)
|
|
6351
6912
|
});
|
|
6352
6913
|
/**
|
|
6353
6914
|
* Full permissions file schema including optional $schema field.
|
|
@@ -6389,10 +6950,40 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
6389
6950
|
}]
|
|
6390
6951
|
};
|
|
6391
6952
|
}
|
|
6953
|
+
/**
|
|
6954
|
+
* The canonical document an importer produces from a tool's own config.
|
|
6955
|
+
*
|
|
6956
|
+
* Every importer has to run the blank-key filter over what it is about to
|
|
6957
|
+
* write: the canonical schema rejects a blank pattern and a blank category
|
|
6958
|
+
* outright, so a source file carrying either would be refused by the very
|
|
6959
|
+
* next `generate` — and that refusal takes the whole file with it, so one
|
|
6960
|
+
* blank key imported from one tool stops every tool's permissions from being
|
|
6961
|
+
* generated. Building the imported document here rather than calling the
|
|
6962
|
+
* filter beside each `new RulesyncPermissions(...)` is what keeps the next
|
|
6963
|
+
* importer from forgetting it.
|
|
6964
|
+
*
|
|
6965
|
+
* `sourcePath` is the tool config being read, used only to name it if
|
|
6966
|
+
* something is dropped.
|
|
6967
|
+
*/
|
|
6968
|
+
static fromImportedFileContent({ outputRoot, fileContent, sourcePath, logger }) {
|
|
6969
|
+
return new RulesyncPermissions({
|
|
6970
|
+
outputRoot,
|
|
6971
|
+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
6972
|
+
relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
|
|
6973
|
+
fileContent: withoutBlankPermissionKeys({
|
|
6974
|
+
fileContent,
|
|
6975
|
+
sourcePath,
|
|
6976
|
+
logger
|
|
6977
|
+
})
|
|
6978
|
+
});
|
|
6979
|
+
}
|
|
6392
6980
|
validate() {
|
|
6393
6981
|
if (this.droppedKeys.length > 0) return {
|
|
6394
6982
|
success: false,
|
|
6395
|
-
error:
|
|
6983
|
+
error: droppedPollutionKeysError({
|
|
6984
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
6985
|
+
droppedKeys: this.droppedKeys
|
|
6986
|
+
})
|
|
6396
6987
|
};
|
|
6397
6988
|
const result = RulesyncPermissionsFileSchema.safeParse(this.json);
|
|
6398
6989
|
if (!result.success) return {
|
|
@@ -6464,33 +7055,62 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
6464
7055
|
}
|
|
6465
7056
|
};
|
|
6466
7057
|
/**
|
|
6467
|
-
*
|
|
6468
|
-
*
|
|
7058
|
+
* Tool-scoped override keys whose `permission` block maps a category to
|
|
7059
|
+
* something other than a pattern map. Vibe alone keeps
|
|
7060
|
+
* `{ sensitive_patterns: [...] }` objects there, so the keys one level down are
|
|
7061
|
+
* field names and the blank-pattern filter must not walk them. The category
|
|
7062
|
+
* names above them are still category names, and are filtered like any other.
|
|
7063
|
+
*
|
|
7064
|
+
* Typed as `ToolTarget` so a renamed target fails to compile here rather than
|
|
7065
|
+
* silently stopping to match, which would let the filter start deleting Vibe's
|
|
7066
|
+
* fields and reporting them as removed permission patterns. That only holds for
|
|
7067
|
+
* targets whose override key is the target name itself: a target that aliases
|
|
7068
|
+
* to another key (see `PERMISSION_OVERRIDE_KEY_ALIASES`) would have to be listed
|
|
7069
|
+
* under the alias, which this type would reject. None of them is non-pattern-map
|
|
7070
|
+
* today, so add that spelling only when one becomes so.
|
|
7071
|
+
*/
|
|
7072
|
+
const NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS = /* @__PURE__ */ new Set(["vibe"]);
|
|
7073
|
+
/**
|
|
7074
|
+
* Strip every blank key — category or pattern — from an already-parsed
|
|
7075
|
+
* canonical document, reporting how many were dropped from each block.
|
|
6469
7076
|
*
|
|
6470
7077
|
* Both the shared `permission` block and every tool-scoped
|
|
6471
7078
|
* `{toolname}.permission` block are walked, because import produces both:
|
|
6472
7079
|
* OpenCode and Kilo route their tool-only categories into the tool-scoped block
|
|
6473
|
-
* verbatim, so a blank
|
|
6474
|
-
* whose value is not a rules map
|
|
6475
|
-
* the tool-native shapes intact — OpenCode's and Kilo's bare action
|
|
6476
|
-
* (`"external_directory": "deny"`) have no pattern key to inspect,
|
|
6477
|
-
*
|
|
6478
|
-
*
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
7080
|
+
* verbatim, so a blank key in the user's own config lands there. A category
|
|
7081
|
+
* whose value is not a rules map keeps its value exactly as it is, which is what
|
|
7082
|
+
* keeps the tool-native shapes intact — OpenCode's and Kilo's bare action
|
|
7083
|
+
* strings (`"external_directory": "deny"`) have no pattern key to inspect, and
|
|
7084
|
+
* Kilo's `sandbox` is not a `permission` block at all.
|
|
7085
|
+
*
|
|
7086
|
+
* Categories are filtered for the same reason patterns are, one level up: the
|
|
7087
|
+
* canonical schema rejects a blank category, so reproducing one would write a
|
|
7088
|
+
* source file the very next `generate` refuses — and it would refuse the whole
|
|
7089
|
+
* file, taking every tool's permissions generation down with it.
|
|
7090
|
+
*
|
|
7091
|
+
* The patterns inside {@link NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS} blocks
|
|
7092
|
+
* are left alone: they are field names rather than patterns there. Their
|
|
7093
|
+
* category names are still filtered.
|
|
7094
|
+
*/
|
|
7095
|
+
function stripBlankPermissionKeys(config) {
|
|
7096
|
+
const patterns = /* @__PURE__ */ new Map();
|
|
7097
|
+
const categories = /* @__PURE__ */ new Map();
|
|
7098
|
+
const filterBlock = ({ block, blockPath, filterPatterns }) => {
|
|
6483
7099
|
const filtered = {};
|
|
6484
7100
|
for (const [category, rules] of Object.entries(block)) {
|
|
6485
|
-
if (
|
|
7101
|
+
if (isBlankPermissionKey(category)) {
|
|
7102
|
+
categories.set(blockPath, (categories.get(blockPath) ?? 0) + 1);
|
|
7103
|
+
continue;
|
|
7104
|
+
}
|
|
7105
|
+
if (!filterPatterns || !isRecord$1(rules)) {
|
|
6486
7106
|
filtered[category] = rules;
|
|
6487
7107
|
continue;
|
|
6488
7108
|
}
|
|
6489
7109
|
const kept = {};
|
|
6490
7110
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
6491
|
-
if (
|
|
7111
|
+
if (isBlankPermissionKey(pattern)) {
|
|
6492
7112
|
const path = `${blockPath}.${category}`;
|
|
6493
|
-
|
|
7113
|
+
patterns.set(path, (patterns.get(path) ?? 0) + 1);
|
|
6494
7114
|
continue;
|
|
6495
7115
|
}
|
|
6496
7116
|
kept[pattern] = action;
|
|
@@ -6503,25 +7123,41 @@ function stripBlankPermissionPatterns(config) {
|
|
|
6503
7123
|
const next = { ...config };
|
|
6504
7124
|
if (isRecord$1(config.permission)) next.permission = filterBlock({
|
|
6505
7125
|
block: config.permission,
|
|
6506
|
-
blockPath: "permission"
|
|
7126
|
+
blockPath: "permission",
|
|
7127
|
+
filterPatterns: true
|
|
6507
7128
|
});
|
|
6508
7129
|
for (const [key, value] of Object.entries(config)) {
|
|
6509
7130
|
if (key === "permission" || !isRecord$1(value) || !isRecord$1(value.permission)) continue;
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
7131
|
+
const permission = filterBlock({
|
|
7132
|
+
block: value.permission,
|
|
7133
|
+
blockPath: `${key}.permission`,
|
|
7134
|
+
filterPatterns: !NON_PATTERN_MAP_PERMISSION_OVERRIDE_KEYS.has(key)
|
|
7135
|
+
});
|
|
7136
|
+
if (!(Object.keys(permission).length === 0 && Object.keys(value.permission).length > 0)) {
|
|
7137
|
+
next[key] = {
|
|
7138
|
+
...value,
|
|
7139
|
+
permission
|
|
7140
|
+
};
|
|
7141
|
+
continue;
|
|
7142
|
+
}
|
|
7143
|
+
const { permission: _emptied, ...rest } = value;
|
|
7144
|
+
if (Object.keys(rest).length === 0) {
|
|
7145
|
+
delete next[key];
|
|
7146
|
+
continue;
|
|
7147
|
+
}
|
|
7148
|
+
next[key] = rest;
|
|
6517
7149
|
}
|
|
6518
7150
|
return {
|
|
6519
7151
|
config: next,
|
|
6520
|
-
removed
|
|
7152
|
+
removed: {
|
|
7153
|
+
patterns,
|
|
7154
|
+
categories
|
|
7155
|
+
}
|
|
6521
7156
|
};
|
|
6522
7157
|
}
|
|
7158
|
+
const summarizeDroppedCounts = (counts) => [...counts.entries()].map(([path, count]) => `${count} in ${JSON.stringify(path)}`).join(", ");
|
|
6523
7159
|
/**
|
|
6524
|
-
* Report the dropped
|
|
7160
|
+
* Report the dropped keys.
|
|
6525
7161
|
*
|
|
6526
7162
|
* Dropping an entry silently is the failure mode a permissions source must not
|
|
6527
7163
|
* have. A blanket blank pattern can read as "deny everything by default";
|
|
@@ -6532,27 +7168,36 @@ function stripBlankPermissionPatterns(config) {
|
|
|
6532
7168
|
* `logger` is optional because the import direction (`toRulesyncPermissions`)
|
|
6533
7169
|
* takes no logger parameter; the shared `fallbackLogger` is configured from the
|
|
6534
7170
|
* CLI flags and the resolved config, so `silent` is still honored.
|
|
7171
|
+
*
|
|
7172
|
+
* `sourcePath` names the tool config the keys came out of. A single import run
|
|
7173
|
+
* reads many tools, and the block paths alone (`permission.bash`) are the same
|
|
7174
|
+
* for all of them, so without it the user is told something was dropped but not
|
|
7175
|
+
* from where.
|
|
6535
7176
|
*/
|
|
6536
|
-
function
|
|
6537
|
-
|
|
7177
|
+
function warnAboutDroppedKeys({ removed, sourcePath, logger }) {
|
|
7178
|
+
const source = sourcePath === void 0 ? "a tool's permission configuration" : JSON.stringify(sourcePath);
|
|
7179
|
+
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.`);
|
|
7180
|
+
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.`);
|
|
6538
7181
|
}
|
|
6539
7182
|
/**
|
|
6540
|
-
* Drop blank permission
|
|
7183
|
+
* Drop blank permission keys from a canonical document produced by import.
|
|
6541
7184
|
*
|
|
6542
|
-
* The canonical schema rejects a blank pattern
|
|
6543
|
-
* has
|
|
6544
|
-
* pattern (Roo Code, for instance, keeps only
|
|
6545
|
-
* `cmd.trim().length > 0`). Reproducing
|
|
6546
|
-
* would therefore write a source file that the
|
|
6547
|
-
*
|
|
7185
|
+
* The canonical schema rejects a blank pattern and a blank category outright,
|
|
7186
|
+
* and every tool that has a blank pattern in its own config already treats it as
|
|
7187
|
+
* something other than a real pattern (Roo Code, for instance, keeps only
|
|
7188
|
+
* entries passing `cmd.trim().length > 0`). Reproducing either in
|
|
7189
|
+
* `.rulesync/permissions.jsonc` would therefore write a source file that the
|
|
7190
|
+
* very next `generate` refuses — the whole file, not just that entry — so they
|
|
7191
|
+
* are removed here instead, and reported.
|
|
6548
7192
|
*/
|
|
6549
|
-
function
|
|
7193
|
+
function withoutBlankPermissionKeys({ fileContent, sourcePath, logger }) {
|
|
6550
7194
|
const parsed = parseJsonc(fileContent);
|
|
6551
7195
|
if (!isRecord$1(parsed)) return fileContent;
|
|
6552
|
-
const { config, removed } =
|
|
6553
|
-
if (removed.size === 0) return fileContent;
|
|
6554
|
-
|
|
7196
|
+
const { config, removed } = stripBlankPermissionKeys(parsed);
|
|
7197
|
+
if (removed.patterns.size === 0 && removed.categories.size === 0) return fileContent;
|
|
7198
|
+
warnAboutDroppedKeys({
|
|
6555
7199
|
removed,
|
|
7200
|
+
sourcePath,
|
|
6556
7201
|
logger
|
|
6557
7202
|
});
|
|
6558
7203
|
return JSON.stringify(config, null, 2);
|
|
@@ -6561,14 +7206,15 @@ function withoutBlankPermissionPatterns({ fileContent, logger }) {
|
|
|
6561
7206
|
* The same filter over an already-parsed document, for callers that validate a
|
|
6562
7207
|
* canonical block before it is ever serialized. Hermes Agent stores its
|
|
6563
7208
|
* rulesync provenance inside its own config and parses it back on import; left
|
|
6564
|
-
* unfiltered, one blank pattern would fail `safeParse` and discard
|
|
6565
|
-
* provenance block without a word.
|
|
7209
|
+
* unfiltered, one blank pattern or category would fail `safeParse` and discard
|
|
7210
|
+
* the entire provenance block without a word.
|
|
6566
7211
|
*/
|
|
6567
|
-
function
|
|
6568
|
-
const { config: filtered, removed } =
|
|
6569
|
-
if (removed.size === 0) return config;
|
|
6570
|
-
|
|
7212
|
+
function withoutBlankPermissionKeysIn({ config, sourcePath, logger }) {
|
|
7213
|
+
const { config: filtered, removed } = stripBlankPermissionKeys(config);
|
|
7214
|
+
if (removed.patterns.size === 0 && removed.categories.size === 0) return config;
|
|
7215
|
+
warnAboutDroppedKeys({
|
|
6571
7216
|
removed,
|
|
7217
|
+
sourcePath,
|
|
6572
7218
|
logger
|
|
6573
7219
|
});
|
|
6574
7220
|
return filtered;
|
|
@@ -6717,6 +7363,24 @@ var RulesyncRule = class RulesyncRule extends RulesyncFile {
|
|
|
6717
7363
|
//#endregion
|
|
6718
7364
|
//#region src/types/ai-dir.ts
|
|
6719
7365
|
/**
|
|
7366
|
+
* Whether `name` is a path rather than a single name.
|
|
7367
|
+
*
|
|
7368
|
+
* Both separators are rejected on every platform, which is why neither
|
|
7369
|
+
* `path.sep` nor the platform is consulted: a backslash is a legal character in
|
|
7370
|
+
* a POSIX name, but a name carrying one is a path the moment it reaches
|
|
7371
|
+
* Windows, and `AiDir` names travel between the two — most tools take one
|
|
7372
|
+
* straight from a skill's frontmatter.
|
|
7373
|
+
*
|
|
7374
|
+
* Exported because the checks that run *before* a name reaches `AiDir` — so
|
|
7375
|
+
* that an unusable name is reported and skipped rather than thrown over — have
|
|
7376
|
+
* to reject exactly the set this guard does. Two spellings of one rule drift
|
|
7377
|
+
* apart the moment either is tightened, and the pre-filter drifting narrower
|
|
7378
|
+
* turns a reported name back into a failed run.
|
|
7379
|
+
*/
|
|
7380
|
+
function containsPathSeparator(name) {
|
|
7381
|
+
return name.includes("/") || name.includes("\\");
|
|
7382
|
+
}
|
|
7383
|
+
/**
|
|
6720
7384
|
* Directories that hold credentials. Excluding these protects something, so
|
|
6721
7385
|
* their exclusion is reported rather than silent.
|
|
6722
7386
|
*/
|
|
@@ -7176,7 +7840,7 @@ var AiDir = class AiDir {
|
|
|
7176
7840
|
*/
|
|
7177
7841
|
global;
|
|
7178
7842
|
constructor({ outputRoot = process.cwd(), relativeDirPath, dirName, mainFile, otherFiles = [], global = false }) {
|
|
7179
|
-
if (
|
|
7843
|
+
if (containsPathSeparator(dirName)) throw new Error(`Directory name cannot contain path separators: dirName="${dirName}"`);
|
|
7180
7844
|
if (dirName === "" || dirName === "." || dirName === "..") throw new Error(`Directory name cannot be empty, ".", or "..": dirName="${dirName}"`);
|
|
7181
7845
|
this.outputRoot = outputRoot;
|
|
7182
7846
|
this.relativeDirPath = relativeDirPath;
|
|
@@ -7237,7 +7901,7 @@ var AiDir = class AiDir {
|
|
|
7237
7901
|
const mainFile = this.getMainFile();
|
|
7238
7902
|
if (mainFile === void 0) return;
|
|
7239
7903
|
const name = mainFile.name;
|
|
7240
|
-
if (name === "" || name === "." || name === ".." ||
|
|
7904
|
+
if (name === "" || name === "." || name === ".." || containsPathSeparator(name)) return;
|
|
7241
7905
|
return path.join(this.getDirPath(), name);
|
|
7242
7906
|
}
|
|
7243
7907
|
getDirPath() {
|
|
@@ -7568,7 +8232,7 @@ var AiDir = class AiDir {
|
|
|
7568
8232
|
* differ from the path the run claims, which turns it into an orphan.
|
|
7569
8233
|
*/
|
|
7570
8234
|
function isUnsafeSkillDirName(name) {
|
|
7571
|
-
return name === "" || name === "." || name === ".." ||
|
|
8235
|
+
return name === "" || name === "." || name === ".." || containsPathSeparator(name) || name.endsWith(".") || name.endsWith(" ");
|
|
7572
8236
|
}
|
|
7573
8237
|
const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
7574
8238
|
name: z.string().check(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" })),
|
|
@@ -7828,6 +8492,16 @@ const RulesyncSubagentFrontmatterSchema = z.looseObject({
|
|
|
7828
8492
|
zoocode: z.optional(z.looseObject({
|
|
7829
8493
|
/** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
|
|
7830
8494
|
allowedMcpServers: z.optional(z.array(z.string())) })),
|
|
8495
|
+
zcode: z.optional(z.looseObject({
|
|
8496
|
+
model: z.optional(z.string()),
|
|
8497
|
+
thoughtLevel: z.optional(z.string()),
|
|
8498
|
+
color: z.optional(z.string()),
|
|
8499
|
+
tools: z.optional(z.array(z.string())),
|
|
8500
|
+
disallowedTools: z.optional(z.array(z.string())),
|
|
8501
|
+
maxTurns: z.optional(z.number().check(z.int(), z.positive())),
|
|
8502
|
+
injectAgentsMd: z.optional(z.boolean()),
|
|
8503
|
+
mcpServers: z.optional(z.array(z.string()))
|
|
8504
|
+
})),
|
|
7831
8505
|
vibe: z.optional(z.looseObject({
|
|
7832
8506
|
agent_type: z.optional(z.enum(["agent", "subagent"])),
|
|
7833
8507
|
display_name: z.optional(z.string()),
|
|
@@ -7915,10 +8589,12 @@ var RulesyncSubagent = class RulesyncSubagent extends RulesyncFile {
|
|
|
7915
8589
|
* lets a directory be *created* with a backslash in its name, so such a
|
|
7916
8590
|
* directory can sit in a skills root that nothing here can name — it is
|
|
7917
8591
|
* reported rather than passed on, since the alternative is a candidate built
|
|
7918
|
-
* from a name that belongs to no directory at all.
|
|
8592
|
+
* from a name that belongs to no directory at all. The test is `AiDir`'s own,
|
|
8593
|
+
* so this pre-filter cannot come to accept a name the guard behind it throws
|
|
8594
|
+
* over.
|
|
7919
8595
|
*/
|
|
7920
8596
|
function isAddressableSkillName(name) {
|
|
7921
|
-
return !
|
|
8597
|
+
return !containsPathSeparator(name);
|
|
7922
8598
|
}
|
|
7923
8599
|
/**
|
|
7924
8600
|
* The names of the skill directories directly under `skillsRoot`.
|
|
@@ -9309,18 +9985,168 @@ function splitCheckFile({ fileContent, fallbackName }) {
|
|
|
9309
9985
|
});
|
|
9310
9986
|
}
|
|
9311
9987
|
//#endregion
|
|
9988
|
+
//#region src/features/checks/aggregated-tool-check.ts
|
|
9989
|
+
/**
|
|
9990
|
+
* Shared skeleton for the checks adapters whose output is a single aggregated
|
|
9991
|
+
* instruction file — one file whose sections are the marked blocks
|
|
9992
|
+
* `aggregated-check-file.ts` renders and splits, rather than a directory with a
|
|
9993
|
+
* file per check.
|
|
9994
|
+
*
|
|
9995
|
+
* That module already held the rendering and the splitting; what is here is the
|
|
9996
|
+
* adapter around it, which was near-verbatim in Cursor Bugbot, Rovo Dev and
|
|
9997
|
+
* Factory Droid. A subclass supplies {@link ToolCheck.getSettablePaths} and
|
|
9998
|
+
* {@link getAggregatedCheckConfig}, and inherits the rest — so a fix to how
|
|
9999
|
+
* these files are read or written lands once instead of three times.
|
|
10000
|
+
*
|
|
10001
|
+
* `fromRulesyncCheck` is refused here rather than implemented: sections share
|
|
10002
|
+
* one file, so an output cannot be produced from one check in isolation and the
|
|
10003
|
+
* processor calls {@link fromRulesyncChecks} instead.
|
|
10004
|
+
*
|
|
10005
|
+
* Not declared `abstract`, even though nothing instantiates it directly: the
|
|
10006
|
+
* statics below build the subclass with `new this(...)`, which an abstract
|
|
10007
|
+
* constructor type forbids. What a subclass owes is enforced the way the rest
|
|
10008
|
+
* of this codebase enforces it on a static — {@link getAggregatedCheckConfig}
|
|
10009
|
+
* throws until it is overridden.
|
|
10010
|
+
*/
|
|
10011
|
+
var AggregatedToolCheck = class extends ToolCheck {
|
|
10012
|
+
/**
|
|
10013
|
+
* The per-tool values the shared skeleton reads. Thrown rather than abstract
|
|
10014
|
+
* because TypeScript has no abstract statics; a subclass that forgets it
|
|
10015
|
+
* fails on its first use rather than silently taking a default.
|
|
10016
|
+
*/
|
|
10017
|
+
static getAggregatedCheckConfig() {
|
|
10018
|
+
throw new Error("Please implement this method in the subclass.");
|
|
10019
|
+
}
|
|
10020
|
+
/**
|
|
10021
|
+
* The settable paths with the file name required. An aggregated adapter names
|
|
10022
|
+
* the one file it writes — that is what keeps consumers which would otherwise
|
|
10023
|
+
* claim the whole tool directory, the gitignore derivation among them,
|
|
10024
|
+
* narrowed to it — so a missing name is a mistake in the subclass rather than
|
|
10025
|
+
* a case to fall back for.
|
|
10026
|
+
*/
|
|
10027
|
+
static getAggregatedPaths({ global = false } = {}) {
|
|
10028
|
+
const paths = this.getSettablePaths({ global });
|
|
10029
|
+
if (!paths.relativeFilePath) throw new Error(`${this.name} writes one aggregated file, so getSettablePaths must name it.`);
|
|
10030
|
+
return {
|
|
10031
|
+
relativeDirPath: paths.relativeDirPath,
|
|
10032
|
+
relativeFilePath: paths.relativeFilePath
|
|
10033
|
+
};
|
|
10034
|
+
}
|
|
10035
|
+
static getAggregatedFilePath({ outputRoot, global = false }) {
|
|
10036
|
+
const paths = this.getAggregatedPaths({ global });
|
|
10037
|
+
return {
|
|
10038
|
+
...paths,
|
|
10039
|
+
filePath: join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)
|
|
10040
|
+
};
|
|
10041
|
+
}
|
|
10042
|
+
static isTargetedByRulesyncCheck(rulesyncCheck) {
|
|
10043
|
+
return this.isTargetedByRulesyncCheckDefault({
|
|
10044
|
+
rulesyncCheck,
|
|
10045
|
+
toolTarget: this.getAggregatedCheckConfig().toolTarget
|
|
10046
|
+
});
|
|
10047
|
+
}
|
|
10048
|
+
/**
|
|
10049
|
+
* Ownership guard the processor consults before it deletes anything for this
|
|
10050
|
+
* tool. Every one of these paths is one a user may well have written by hand,
|
|
10051
|
+
* whether because the tool documents it as the place to write review
|
|
10052
|
+
* instructions or because it is shared with something else the tool loads, so
|
|
10053
|
+
* anything in it that rulesync did not write is not rulesync's to remove — dropping the last check targeting the tool must not
|
|
10054
|
+
* take somebody's review instructions with it. Deletion is therefore allowed
|
|
10055
|
+
* only for a file that is nothing but generated sections: one that carries no
|
|
10056
|
+
* marker at all, or that carries hand-written text ahead of the first marker,
|
|
10057
|
+
* stays.
|
|
10058
|
+
*/
|
|
10059
|
+
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
10060
|
+
const { filePath } = this.getAggregatedFilePath({ outputRoot });
|
|
10061
|
+
const fileContent = await readFileContentOrNull(filePath);
|
|
10062
|
+
if (fileContent === null) return true;
|
|
10063
|
+
return isOnlyGeneratedSections(fileContent);
|
|
10064
|
+
}
|
|
10065
|
+
static fromRulesyncCheck(_params) {
|
|
10066
|
+
const { displayName } = this.getAggregatedCheckConfig();
|
|
10067
|
+
throw new Error(`${displayName} checks are built from all checks at once; use fromRulesyncChecks.`);
|
|
10068
|
+
}
|
|
10069
|
+
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
10070
|
+
if (rulesyncChecks.length === 0) return [];
|
|
10071
|
+
const config = this.getAggregatedCheckConfig();
|
|
10072
|
+
const { relativeDirPath, relativeFilePath, filePath } = this.getAggregatedFilePath({
|
|
10073
|
+
outputRoot,
|
|
10074
|
+
global
|
|
10075
|
+
});
|
|
10076
|
+
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) {
|
|
10077
|
+
if (config.handWrittenPreamble === "skip") {
|
|
10078
|
+
logger?.warn(config.handWrittenWarning({
|
|
10079
|
+
filePath,
|
|
10080
|
+
displayName: config.displayName,
|
|
10081
|
+
toolTarget: config.toolTarget
|
|
10082
|
+
}));
|
|
10083
|
+
return [];
|
|
10084
|
+
}
|
|
10085
|
+
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.`);
|
|
10086
|
+
}
|
|
10087
|
+
return [new this({
|
|
10088
|
+
outputRoot,
|
|
10089
|
+
relativeDirPath,
|
|
10090
|
+
relativeFilePath,
|
|
10091
|
+
fileContent: renderCheckFile(rulesyncChecks),
|
|
10092
|
+
global
|
|
10093
|
+
})];
|
|
10094
|
+
}
|
|
10095
|
+
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
10096
|
+
const { relativeDirPath, relativeFilePath, filePath } = this.getAggregatedFilePath({
|
|
10097
|
+
outputRoot,
|
|
10098
|
+
global
|
|
10099
|
+
});
|
|
10100
|
+
return new this({
|
|
10101
|
+
outputRoot,
|
|
10102
|
+
relativeDirPath,
|
|
10103
|
+
relativeFilePath,
|
|
10104
|
+
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
10105
|
+
global
|
|
10106
|
+
});
|
|
10107
|
+
}
|
|
10108
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
10109
|
+
return new this({
|
|
10110
|
+
outputRoot,
|
|
10111
|
+
relativeDirPath,
|
|
10112
|
+
relativeFilePath,
|
|
10113
|
+
fileContent: "",
|
|
10114
|
+
validate: false,
|
|
10115
|
+
global
|
|
10116
|
+
});
|
|
10117
|
+
}
|
|
10118
|
+
validate() {
|
|
10119
|
+
return {
|
|
10120
|
+
success: true,
|
|
10121
|
+
error: null
|
|
10122
|
+
};
|
|
10123
|
+
}
|
|
10124
|
+
toRulesyncCheck() {
|
|
10125
|
+
const first = this.toRulesyncChecks()[0];
|
|
10126
|
+
if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
10127
|
+
return first;
|
|
10128
|
+
}
|
|
10129
|
+
toRulesyncChecks() {
|
|
10130
|
+
const config = this.constructor.getAggregatedCheckConfig();
|
|
10131
|
+
const fileContent = this.getFileContent();
|
|
10132
|
+
return splitCheckFile({
|
|
10133
|
+
fileContent: config.transformImportedContent?.(fileContent) ?? fileContent,
|
|
10134
|
+
fallbackName: config.fallbackCheckName
|
|
10135
|
+
});
|
|
10136
|
+
}
|
|
10137
|
+
};
|
|
10138
|
+
//#endregion
|
|
9312
10139
|
//#region src/features/checks/cursor-check.ts
|
|
9313
|
-
const FALLBACK_CHECK_NAME$2 = "bugbot";
|
|
9314
10140
|
/**
|
|
9315
10141
|
* Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
|
|
9316
10142
|
*
|
|
9317
10143
|
* Bugbot takes one aggregated instruction file per directory rather than a file
|
|
9318
10144
|
* per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
|
|
9319
|
-
* the repository-root `.cursor/BUGBOT.md` — hence
|
|
9320
|
-
*
|
|
9321
|
-
* an HTML-comment marker carrying the
|
|
9322
|
-
*
|
|
9323
|
-
* body is empty).
|
|
10145
|
+
* the repository-root `.cursor/BUGBOT.md` — hence `fromRulesyncChecks` rather
|
|
10146
|
+
* than the usual per-check conversion, which {@link AggregatedToolCheck}
|
|
10147
|
+
* provides. Each check becomes one section: an HTML-comment marker carrying the
|
|
10148
|
+
* check name, an `## <name>` heading, and the check body as the instruction
|
|
10149
|
+
* text (the `description` is used when the body is empty).
|
|
9324
10150
|
*
|
|
9325
10151
|
* Bugbot reads the file as free prose, so a check's `severity` and `tools` have
|
|
9326
10152
|
* no equivalent there: they are not written and do not come back on import. So
|
|
@@ -9336,97 +10162,26 @@ const FALLBACK_CHECK_NAME$2 = "bugbot";
|
|
|
9336
10162
|
* before the first marker — and a hand-written file with no markers at all —
|
|
9337
10163
|
* becomes a single `bugbot` check, so nothing in the file is dropped. A file
|
|
9338
10164
|
* holding anything rulesync did not write is never deleted either (see
|
|
9339
|
-
*
|
|
9340
|
-
* replace it — import first to keep what is there, which is warned about.
|
|
10165
|
+
* `canDeleteAuxiliaryFiles` on the base), though generating checks for Cursor
|
|
10166
|
+
* does replace it — import first to keep what is there, which is warned about.
|
|
9341
10167
|
*
|
|
9342
10168
|
* @see https://cursor.com/docs/bugbot
|
|
9343
10169
|
*/
|
|
9344
|
-
var CursorCheck = class
|
|
10170
|
+
var CursorCheck = class extends AggregatedToolCheck {
|
|
9345
10171
|
static getSettablePaths(_options = {}) {
|
|
9346
10172
|
return {
|
|
9347
10173
|
relativeDirPath: CURSOR_DIR,
|
|
9348
10174
|
relativeFilePath: CURSOR_BUGBOT_FILE_NAME
|
|
9349
10175
|
};
|
|
9350
10176
|
}
|
|
9351
|
-
static
|
|
9352
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9353
|
-
rulesyncCheck,
|
|
9354
|
-
toolTarget: "cursor"
|
|
9355
|
-
});
|
|
9356
|
-
}
|
|
9357
|
-
/**
|
|
9358
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9359
|
-
* tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
|
|
9360
|
-
* to hand-write, so anything in it that rulesync did not write is not
|
|
9361
|
-
* rulesync's to remove — dropping the last check targeting Cursor must not
|
|
9362
|
-
* take somebody's hand-written review instructions with it. Deletion is
|
|
9363
|
-
* therefore allowed only for a file that is nothing but generated sections:
|
|
9364
|
-
* one that carries no marker at all, or that carries hand-written text ahead
|
|
9365
|
-
* of the first marker, stays.
|
|
9366
|
-
*/
|
|
9367
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9368
|
-
const paths = CursorCheck.getSettablePaths();
|
|
9369
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
|
|
9370
|
-
if (fileContent === null) return true;
|
|
9371
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9372
|
-
}
|
|
9373
|
-
static fromRulesyncCheck(_params) {
|
|
9374
|
-
throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9375
|
-
}
|
|
9376
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9377
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9378
|
-
const paths = CursorCheck.getSettablePaths({ global });
|
|
9379
|
-
const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
|
|
9380
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9381
|
-
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.`);
|
|
9382
|
-
const fileContent = renderCheckFile(rulesyncChecks);
|
|
9383
|
-
return [new CursorCheck({
|
|
9384
|
-
outputRoot,
|
|
9385
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9386
|
-
relativeFilePath,
|
|
9387
|
-
fileContent,
|
|
9388
|
-
global
|
|
9389
|
-
})];
|
|
9390
|
-
}
|
|
9391
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9392
|
-
const paths = CursorCheck.getSettablePaths({ global });
|
|
9393
|
-
const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
|
|
9394
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9395
|
-
return new CursorCheck({
|
|
9396
|
-
outputRoot,
|
|
9397
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9398
|
-
relativeFilePath,
|
|
9399
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9400
|
-
global
|
|
9401
|
-
});
|
|
9402
|
-
}
|
|
9403
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9404
|
-
return new CursorCheck({
|
|
9405
|
-
outputRoot,
|
|
9406
|
-
relativeDirPath,
|
|
9407
|
-
relativeFilePath,
|
|
9408
|
-
fileContent: "",
|
|
9409
|
-
validate: false,
|
|
9410
|
-
global
|
|
9411
|
-
});
|
|
9412
|
-
}
|
|
9413
|
-
validate() {
|
|
10177
|
+
static getAggregatedCheckConfig() {
|
|
9414
10178
|
return {
|
|
9415
|
-
|
|
9416
|
-
|
|
10179
|
+
displayName: "Cursor",
|
|
10180
|
+
toolTarget: "cursor",
|
|
10181
|
+
fallbackCheckName: "bugbot",
|
|
10182
|
+
handWrittenPreamble: "replace"
|
|
9417
10183
|
};
|
|
9418
10184
|
}
|
|
9419
|
-
toRulesyncCheck() {
|
|
9420
|
-
const first = this.toRulesyncChecks()[0];
|
|
9421
|
-
if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
9422
|
-
return first;
|
|
9423
|
-
}
|
|
9424
|
-
toRulesyncChecks() {
|
|
9425
|
-
return splitCheckFile({
|
|
9426
|
-
fileContent: this.getFileContent(),
|
|
9427
|
-
fallbackName: FALLBACK_CHECK_NAME$2
|
|
9428
|
-
});
|
|
9429
|
-
}
|
|
9430
10185
|
};
|
|
9431
10186
|
//#endregion
|
|
9432
10187
|
//#region src/constants/factorydroid-paths.ts
|
|
@@ -9463,7 +10218,6 @@ const FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME = "review-guidelines";
|
|
|
9463
10218
|
const FACTORYDROID_REVIEW_GUIDELINES_DIR_PATH = join(FACTORYDROID_SKILLS_DIR_PATH, FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME);
|
|
9464
10219
|
//#endregion
|
|
9465
10220
|
//#region src/features/checks/factorydroid-check.ts
|
|
9466
|
-
const FALLBACK_CHECK_NAME$1 = FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME;
|
|
9467
10221
|
/**
|
|
9468
10222
|
* Drop the YAML frontmatter of a hand-authored `review-guidelines` skill before
|
|
9469
10223
|
* the file is split into checks.
|
|
@@ -9498,6 +10252,9 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9498
10252
|
content = rest.slice(closing.index + closing[0].length).replace(LEADING_BLANK_LINE_PATTERN, "");
|
|
9499
10253
|
}
|
|
9500
10254
|
}
|
|
10255
|
+
function handWrittenWarning({ filePath, displayName, toolTarget }) {
|
|
10256
|
+
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.`;
|
|
10257
|
+
}
|
|
9501
10258
|
/**
|
|
9502
10259
|
* Checks adapter for Factory Droid's code-review guidelines
|
|
9503
10260
|
* (`.factory/skills/review-guidelines/SKILL.md`).
|
|
@@ -9506,9 +10263,10 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9506
10263
|
* "repository-specific review guidelines" from a skill named
|
|
9507
10264
|
* `review-guidelines` and injects them into every review run. That makes the
|
|
9508
10265
|
* output a single aggregated file like Cursor Bugbot's and Rovo Dev's, so every
|
|
9509
|
-
* `.rulesync/checks/*.md` targeting Factory Droid collapses into it via
|
|
9510
|
-
* {@link
|
|
9511
|
-
* `aggregated-check-file.ts` for the marker convention the
|
|
10266
|
+
* `.rulesync/checks/*.md` targeting Factory Droid collapses into it via the
|
|
10267
|
+
* `fromRulesyncChecks` on {@link AggregatedToolCheck}, each check written as a
|
|
10268
|
+
* marked section (see `aggregated-check-file.ts` for the marker convention the
|
|
10269
|
+
* three aggregated adapters share).
|
|
9512
10270
|
*
|
|
9513
10271
|
* The file is plain Markdown with no frontmatter, matching Factory's documented
|
|
9514
10272
|
* example. Frontmatter would also be self-defeating here: `renderCheckFile`
|
|
@@ -9526,8 +10284,9 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9526
10284
|
* The output lives inside the same `.factory/skills/` tree the `skills` feature
|
|
9527
10285
|
* writes, so a user-authored `review-guidelines` skill collides with it. The
|
|
9528
10286
|
* path has one owner rather than a merge rule, and the owner is this feature:
|
|
9529
|
-
*
|
|
9530
|
-
*
|
|
10287
|
+
* generating leaves a file holding anything rulesync did not write untouched —
|
|
10288
|
+
* the `skip` policy below — and the base's `canDeleteAuxiliaryFiles` refuses to
|
|
10289
|
+
* remove one.
|
|
9531
10290
|
* Their content is somebody's own writing and rulesync cannot reconstruct it,
|
|
9532
10291
|
* so neither direction guesses. That is stricter than Cursor Bugbot's
|
|
9533
10292
|
* replace-and-warn, and deliberately: `.cursor/BUGBOT.md` is a path only the
|
|
@@ -9537,92 +10296,23 @@ function stripSkillFrontmatter(fileContent) {
|
|
|
9537
10296
|
*
|
|
9538
10297
|
* @see https://docs.factory.ai/software-factory/code-review-ci
|
|
9539
10298
|
*/
|
|
9540
|
-
var FactorydroidCheck = class
|
|
10299
|
+
var FactorydroidCheck = class extends AggregatedToolCheck {
|
|
9541
10300
|
static getSettablePaths(_options = {}) {
|
|
9542
10301
|
return {
|
|
9543
10302
|
relativeDirPath: FACTORYDROID_REVIEW_GUIDELINES_DIR_PATH,
|
|
9544
10303
|
relativeFilePath: SKILL_FILE_NAME
|
|
9545
10304
|
};
|
|
9546
10305
|
}
|
|
9547
|
-
static
|
|
9548
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9549
|
-
rulesyncCheck,
|
|
9550
|
-
toolTarget: "factorydroid"
|
|
9551
|
-
});
|
|
9552
|
-
}
|
|
9553
|
-
/**
|
|
9554
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9555
|
-
* tool. `review-guidelines` is an ordinary skill directory a user may have
|
|
9556
|
-
* authored by hand, so dropping the last check targeting Factory Droid must
|
|
9557
|
-
* not take their review instructions with it. Deletion is allowed only for a
|
|
9558
|
-
* file that is nothing but generated sections.
|
|
9559
|
-
*/
|
|
9560
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9561
|
-
const paths = FactorydroidCheck.getSettablePaths();
|
|
9562
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "SKILL.md"));
|
|
9563
|
-
if (fileContent === null) return true;
|
|
9564
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9565
|
-
}
|
|
9566
|
-
static fromRulesyncCheck(_params) {
|
|
9567
|
-
throw new Error("Factory Droid checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9568
|
-
}
|
|
9569
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9570
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9571
|
-
const paths = FactorydroidCheck.getSettablePaths({ global });
|
|
9572
|
-
const relativeFilePath = paths.relativeFilePath ?? "SKILL.md";
|
|
9573
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9574
|
-
if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) {
|
|
9575
|
-
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.`);
|
|
9576
|
-
return [];
|
|
9577
|
-
}
|
|
9578
|
-
const fileContent = renderCheckFile(rulesyncChecks);
|
|
9579
|
-
return [new FactorydroidCheck({
|
|
9580
|
-
outputRoot,
|
|
9581
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9582
|
-
relativeFilePath,
|
|
9583
|
-
fileContent,
|
|
9584
|
-
global
|
|
9585
|
-
})];
|
|
9586
|
-
}
|
|
9587
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9588
|
-
const paths = FactorydroidCheck.getSettablePaths({ global });
|
|
9589
|
-
const relativeFilePath = paths.relativeFilePath ?? "SKILL.md";
|
|
9590
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9591
|
-
return new FactorydroidCheck({
|
|
9592
|
-
outputRoot,
|
|
9593
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9594
|
-
relativeFilePath,
|
|
9595
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9596
|
-
global
|
|
9597
|
-
});
|
|
9598
|
-
}
|
|
9599
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9600
|
-
return new FactorydroidCheck({
|
|
9601
|
-
outputRoot,
|
|
9602
|
-
relativeDirPath,
|
|
9603
|
-
relativeFilePath,
|
|
9604
|
-
fileContent: "",
|
|
9605
|
-
validate: false,
|
|
9606
|
-
global
|
|
9607
|
-
});
|
|
9608
|
-
}
|
|
9609
|
-
validate() {
|
|
10306
|
+
static getAggregatedCheckConfig() {
|
|
9610
10307
|
return {
|
|
9611
|
-
|
|
9612
|
-
|
|
10308
|
+
displayName: "Factory Droid",
|
|
10309
|
+
toolTarget: "factorydroid",
|
|
10310
|
+
fallbackCheckName: FACTORYDROID_REVIEW_GUIDELINES_DIR_NAME,
|
|
10311
|
+
handWrittenPreamble: "skip",
|
|
10312
|
+
handWrittenWarning,
|
|
10313
|
+
transformImportedContent: stripSkillFrontmatter
|
|
9613
10314
|
};
|
|
9614
10315
|
}
|
|
9615
|
-
toRulesyncCheck() {
|
|
9616
|
-
const first = this.toRulesyncChecks()[0];
|
|
9617
|
-
if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
9618
|
-
return first;
|
|
9619
|
-
}
|
|
9620
|
-
toRulesyncChecks() {
|
|
9621
|
-
return splitCheckFile({
|
|
9622
|
-
fileContent: stripSkillFrontmatter(this.getFileContent()),
|
|
9623
|
-
fallbackName: FALLBACK_CHECK_NAME$1
|
|
9624
|
-
});
|
|
9625
|
-
}
|
|
9626
10316
|
};
|
|
9627
10317
|
//#endregion
|
|
9628
10318
|
//#region src/constants/hermesagent-paths.ts
|
|
@@ -9881,7 +10571,6 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
9881
10571
|
};
|
|
9882
10572
|
//#endregion
|
|
9883
10573
|
//#region src/features/checks/rovodev-check.ts
|
|
9884
|
-
const FALLBACK_CHECK_NAME = "review-agent";
|
|
9885
10574
|
/**
|
|
9886
10575
|
* Checks adapter for Rovo Dev CLI's code-review custom instructions
|
|
9887
10576
|
* (`.rovodev/.review-agent.md`).
|
|
@@ -9890,9 +10579,9 @@ const FALLBACK_CHECK_NAME = "review-agent";
|
|
|
9890
10579
|
* `.rovodev/` folder — no frontmatter, and note the leading dot in the file
|
|
9891
10580
|
* name. Like Cursor Bugbot it is a single aggregated file rather than a file
|
|
9892
10581
|
* per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
|
|
9893
|
-
* it via {@link
|
|
9894
|
-
* section (see `aggregated-check-file.ts` for the
|
|
9895
|
-
* adapters share).
|
|
10582
|
+
* it via the `fromRulesyncChecks` on {@link AggregatedToolCheck}, with each
|
|
10583
|
+
* check written as a marked section (see `aggregated-check-file.ts` for the
|
|
10584
|
+
* marker convention the three aggregated adapters share).
|
|
9896
10585
|
*
|
|
9897
10586
|
* Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
|
|
9898
10587
|
* have no equivalent there: they are not written and do not come back on
|
|
@@ -9904,88 +10593,21 @@ const FALLBACK_CHECK_NAME = "review-agent";
|
|
|
9904
10593
|
*
|
|
9905
10594
|
* @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
|
|
9906
10595
|
*/
|
|
9907
|
-
var RovodevCheck = class
|
|
10596
|
+
var RovodevCheck = class extends AggregatedToolCheck {
|
|
9908
10597
|
static getSettablePaths(_options = {}) {
|
|
9909
10598
|
return {
|
|
9910
10599
|
relativeDirPath: ROVODEV_DIR,
|
|
9911
10600
|
relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
|
|
9912
10601
|
};
|
|
9913
10602
|
}
|
|
9914
|
-
static
|
|
9915
|
-
return this.isTargetedByRulesyncCheckDefault({
|
|
9916
|
-
rulesyncCheck,
|
|
9917
|
-
toolTarget: "rovodev"
|
|
9918
|
-
});
|
|
9919
|
-
}
|
|
9920
|
-
/**
|
|
9921
|
-
* Ownership guard the processor consults before it deletes anything for this
|
|
9922
|
-
* tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
|
|
9923
|
-
* to hand-write, so anything in it that rulesync did not write is not
|
|
9924
|
-
* rulesync's to remove — dropping the last check targeting Rovo Dev must not
|
|
9925
|
-
* take somebody's hand-written review instructions with it.
|
|
9926
|
-
*/
|
|
9927
|
-
static async canDeleteAuxiliaryFiles({ outputRoot }) {
|
|
9928
|
-
const paths = RovodevCheck.getSettablePaths();
|
|
9929
|
-
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
|
|
9930
|
-
if (fileContent === null) return true;
|
|
9931
|
-
return isOnlyGeneratedSections(fileContent);
|
|
9932
|
-
}
|
|
9933
|
-
static fromRulesyncCheck(_params) {
|
|
9934
|
-
throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
|
|
9935
|
-
}
|
|
9936
|
-
static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
|
|
9937
|
-
if (rulesyncChecks.length === 0) return [];
|
|
9938
|
-
const paths = RovodevCheck.getSettablePaths({ global });
|
|
9939
|
-
const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
|
|
9940
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9941
|
-
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.`);
|
|
9942
|
-
return [new RovodevCheck({
|
|
9943
|
-
outputRoot,
|
|
9944
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9945
|
-
relativeFilePath,
|
|
9946
|
-
fileContent: renderCheckFile(rulesyncChecks),
|
|
9947
|
-
global
|
|
9948
|
-
})];
|
|
9949
|
-
}
|
|
9950
|
-
static async fromFile({ outputRoot = process.cwd(), global = false }) {
|
|
9951
|
-
const paths = RovodevCheck.getSettablePaths({ global });
|
|
9952
|
-
const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
|
|
9953
|
-
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
9954
|
-
return new RovodevCheck({
|
|
9955
|
-
outputRoot,
|
|
9956
|
-
relativeDirPath: paths.relativeDirPath,
|
|
9957
|
-
relativeFilePath,
|
|
9958
|
-
fileContent: await readFileContentOrNull(filePath) ?? "",
|
|
9959
|
-
global
|
|
9960
|
-
});
|
|
9961
|
-
}
|
|
9962
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
9963
|
-
return new RovodevCheck({
|
|
9964
|
-
outputRoot,
|
|
9965
|
-
relativeDirPath,
|
|
9966
|
-
relativeFilePath,
|
|
9967
|
-
fileContent: "",
|
|
9968
|
-
validate: false,
|
|
9969
|
-
global
|
|
9970
|
-
});
|
|
9971
|
-
}
|
|
9972
|
-
validate() {
|
|
10603
|
+
static getAggregatedCheckConfig() {
|
|
9973
10604
|
return {
|
|
9974
|
-
|
|
9975
|
-
|
|
10605
|
+
displayName: "Rovo Dev",
|
|
10606
|
+
toolTarget: "rovodev",
|
|
10607
|
+
fallbackCheckName: "review-agent",
|
|
10608
|
+
handWrittenPreamble: "replace"
|
|
9976
10609
|
};
|
|
9977
10610
|
}
|
|
9978
|
-
toRulesyncCheck() {
|
|
9979
|
-
const first = this.toRulesyncChecks()[0];
|
|
9980
|
-
if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
|
|
9981
|
-
return first;
|
|
9982
|
-
}
|
|
9983
|
-
toRulesyncChecks() {
|
|
9984
|
-
return splitCheckFile({
|
|
9985
|
-
fileContent: this.getFileContent(),
|
|
9986
|
-
fallbackName: FALLBACK_CHECK_NAME
|
|
9987
|
-
});
|
|
9988
|
-
}
|
|
9989
10611
|
};
|
|
9990
10612
|
//#endregion
|
|
9991
10613
|
//#region src/constants/codexcli-paths.ts
|
|
@@ -11193,7 +11815,7 @@ var ChecksProcessor = class extends FeatureProcessor {
|
|
|
11193
11815
|
this.logger.debug(`Rulesync checks directory not found: ${checksDir}`);
|
|
11194
11816
|
return [];
|
|
11195
11817
|
}
|
|
11196
|
-
const mdFiles = (await
|
|
11818
|
+
const mdFiles = (await listDirectoryEntryNames(checksDir)).filter((file) => file.endsWith(".md"));
|
|
11197
11819
|
if (mdFiles.length === 0) {
|
|
11198
11820
|
this.logger.debug(`No markdown files found in rulesync checks directory: ${checksDir}`);
|
|
11199
11821
|
return [];
|
|
@@ -16015,6 +16637,7 @@ const ZCODE_SKILLS_DIR_PATH = join(ZCODE_DIR, "skills");
|
|
|
16015
16637
|
const ZCODE_CONFIG_FILE_NAME = "config.json";
|
|
16016
16638
|
const ZCODE_GLOBAL_CONFIG_DIR_PATH = join(ZCODE_DIR, "cli");
|
|
16017
16639
|
const ZCODE_MCP_SERVERS_KEY = "servers";
|
|
16640
|
+
const ZCODE_AGENTS_DIR_PATH = join(ZCODE_DIR, "agents");
|
|
16018
16641
|
//#endregion
|
|
16019
16642
|
//#region src/features/commands/zcode-command.ts
|
|
16020
16643
|
/**
|
|
@@ -16977,6 +17600,52 @@ function compact(obj) {
|
|
|
16977
17600
|
return result;
|
|
16978
17601
|
}
|
|
16979
17602
|
//#endregion
|
|
17603
|
+
//#region src/utils/quote-value.ts
|
|
17604
|
+
/**
|
|
17605
|
+
* How much of a value read off disk a diagnostic quotes.
|
|
17606
|
+
*
|
|
17607
|
+
* Enough to recognize which entry is meant, and no more. A warning names the
|
|
17608
|
+
* offending value so the reader can find it, but the values these warnings
|
|
17609
|
+
* quote come from files rulesync did not write — a tool's own settings, a
|
|
17610
|
+
* machine-local overrides file, a repository fetched from elsewhere — and they
|
|
17611
|
+
* no longer stop at a terminal: they travel into a `--json` document another
|
|
17612
|
+
* program parses and into an MCP result an agent reads as context. A command
|
|
17613
|
+
* line or a header is the shape most likely to carry a credential, and a long
|
|
17614
|
+
* value is the shape most likely to carry instructions aimed at the agent.
|
|
17615
|
+
*/
|
|
17616
|
+
const MAX_QUOTED_VALUE_LENGTH = 60;
|
|
17617
|
+
/**
|
|
17618
|
+
* A short, quotable rendering of a value for a diagnostic.
|
|
17619
|
+
*
|
|
17620
|
+
* Serialized rather than interpolated, because an unquoted value is what lets a
|
|
17621
|
+
* crafted one read as a second line; stripped of the control characters
|
|
17622
|
+
* `JSON.stringify` leaves intact (it escapes C0 only, not the C1 range or the
|
|
17623
|
+
* bidirectional overrides); and truncated.
|
|
17624
|
+
*/
|
|
17625
|
+
function quoteValueForWarning(value) {
|
|
17626
|
+
return truncateText({
|
|
17627
|
+
text: stripControlCharacters(serialize(value)),
|
|
17628
|
+
maxLength: MAX_QUOTED_VALUE_LENGTH,
|
|
17629
|
+
suffix: "…(truncated)"
|
|
17630
|
+
});
|
|
17631
|
+
}
|
|
17632
|
+
function serialize(value) {
|
|
17633
|
+
try {
|
|
17634
|
+
return JSON.stringify(value, stripStrings) ?? String(value);
|
|
17635
|
+
} catch {
|
|
17636
|
+
return `[unserializable ${typeof value}]`;
|
|
17637
|
+
}
|
|
17638
|
+
}
|
|
17639
|
+
/**
|
|
17640
|
+
* Strip the control characters out of every string before `JSON.stringify`
|
|
17641
|
+
* sees it, not only out of the document it produces: `JSON.stringify` escapes
|
|
17642
|
+
* a C0 character into the six literal characters `\u001b`, which no later pass
|
|
17643
|
+
* over the output can recognize as a control character again.
|
|
17644
|
+
*/
|
|
17645
|
+
function stripStrings(_key, value) {
|
|
17646
|
+
return typeof value === "string" ? stripControlCharacters(value) : value;
|
|
17647
|
+
}
|
|
17648
|
+
//#endregion
|
|
16980
17649
|
//#region src/features/hooks/tool-hooks-converter.ts
|
|
16981
17650
|
function isToolMatcherEntry(x) {
|
|
16982
17651
|
if (x === null || typeof x !== "object") return false;
|
|
@@ -17078,7 +17747,7 @@ function emitPassthroughFields({ def, hookType, eventName, fields, isValid, warn
|
|
|
17078
17747
|
if (!isValid({
|
|
17079
17748
|
value,
|
|
17080
17749
|
canonical
|
|
17081
|
-
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${
|
|
17750
|
+
})) warn?.(`Dropping "${canonical}" from a "${hookType}" hook on "${eventName}": ${quoteValueForWarning(value)} is not a value this tool can express as "${tool}".`);
|
|
17082
17751
|
}
|
|
17083
17752
|
return Object.fromEntries(fields.filter(({ canonical, commandOnly }) => isFieldApplicable({
|
|
17084
17753
|
commandOnly,
|
|
@@ -17173,7 +17842,7 @@ function describeScalarConstraint({ canonical, value }) {
|
|
|
17173
17842
|
if (issue === void 0) return `it is not a value the canonical "${canonical}" field accepts.`;
|
|
17174
17843
|
return `it does not satisfy the canonical "${canonical}" field: ${issue.message}.`;
|
|
17175
17844
|
}
|
|
17176
|
-
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${
|
|
17845
|
+
const describeInvalidScalar = ({ tool, canonical, value }) => `Dropping "${tool}" (${quoteValueForWarning(value)}) while importing a hook: ${describeScalarConstraint({
|
|
17177
17846
|
canonical,
|
|
17178
17847
|
value
|
|
17179
17848
|
})} Importing it would fail validation on the next run.`;
|
|
@@ -17200,7 +17869,7 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
17200
17869
|
if (first === void 0) continue;
|
|
17201
17870
|
const firstStable = stableJson(first);
|
|
17202
17871
|
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
17203
|
-
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${
|
|
17872
|
+
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.`);
|
|
17204
17873
|
emitted[tool] = first;
|
|
17205
17874
|
}
|
|
17206
17875
|
return emitted;
|
|
@@ -17687,7 +18356,7 @@ function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
|
17687
18356
|
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
17688
18357
|
const value = entry[tool];
|
|
17689
18358
|
if (subdividesGroup !== true || value === void 0) continue;
|
|
17690
|
-
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${
|
|
18359
|
+
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.`;
|
|
17691
18360
|
}
|
|
17692
18361
|
}
|
|
17693
18362
|
/**
|
|
@@ -17704,7 +18373,7 @@ function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
|
17704
18373
|
value,
|
|
17705
18374
|
canonical: field
|
|
17706
18375
|
})) continue;
|
|
17707
|
-
return `Skipping a hook while importing: its "${field}" (${
|
|
18376
|
+
return `Skipping a hook while importing: its "${field}" (${quoteValueForWarning(value)}) is unusable — ${describeScalarConstraint({
|
|
17708
18377
|
canonical: field,
|
|
17709
18378
|
value
|
|
17710
18379
|
})} Keeping the hook without it would change what it does, so the whole hook is skipped.`;
|
|
@@ -18026,7 +18695,22 @@ async function readSettingsWithLocalOverlay({ outputRoot, relativeDirPath, baseF
|
|
|
18026
18695
|
}
|
|
18027
18696
|
/** Quotes a name read off disk, the way every other such name is logged. */
|
|
18028
18697
|
function quoteKey(key) {
|
|
18029
|
-
return
|
|
18698
|
+
return quoteValueForWarning(key);
|
|
18699
|
+
}
|
|
18700
|
+
/**
|
|
18701
|
+
* How many keys the warning names before it stops counting.
|
|
18702
|
+
*
|
|
18703
|
+
* The keys come from a file rulesync did not write, and the warning now travels
|
|
18704
|
+
* into `--json` documents and MCP results as well as onto a console. A settings
|
|
18705
|
+
* file with hundreds of top-level keys is unusual but not impossible, and the
|
|
18706
|
+
* point of the sentence is to make the reader open the file — naming the first
|
|
18707
|
+
* few does that as well as naming all of them.
|
|
18708
|
+
*/
|
|
18709
|
+
const MAX_LISTED_KEYS = 20;
|
|
18710
|
+
function listKeys(keys) {
|
|
18711
|
+
const named = keys.slice(0, MAX_LISTED_KEYS).map(quoteKey).join(", ");
|
|
18712
|
+
const rest = keys.length - MAX_LISTED_KEYS;
|
|
18713
|
+
return rest > 0 ? `${named} and ${rest} more` : named;
|
|
18030
18714
|
}
|
|
18031
18715
|
/**
|
|
18032
18716
|
* Name the settings the machine-local file contributed, so nobody publishes one
|
|
@@ -18046,8 +18730,8 @@ function warnAboutLocalKeys({ localParsed, configPath, toolLabel, sensitiveKeys,
|
|
|
18046
18730
|
const keys = Object.keys(localParsed);
|
|
18047
18731
|
if (keys.length === 0) return;
|
|
18048
18732
|
const flagged = keys.filter((key) => sensitiveKeys.includes(key));
|
|
18049
|
-
const guardrailSentence = flagged.length === 0 ? "" : ` ${flagged
|
|
18050
|
-
warnOnceWithFallback(logger, `${toolLabel}: ${configPath} is a machine-local overrides file, and importing read ${keys
|
|
18733
|
+
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.`;
|
|
18734
|
+
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}`);
|
|
18051
18735
|
}
|
|
18052
18736
|
//#endregion
|
|
18053
18737
|
//#region src/utils/augmentcode-settings.ts
|
|
@@ -22742,6 +23426,9 @@ function withToolTargetPrefix({ logger, toolTarget }) {
|
|
|
22742
23426
|
get silent() {
|
|
22743
23427
|
return logger.silent;
|
|
22744
23428
|
},
|
|
23429
|
+
get reportsWhileSilent() {
|
|
23430
|
+
return logger.reportsWhileSilent;
|
|
23431
|
+
},
|
|
22745
23432
|
get jsonMode() {
|
|
22746
23433
|
return logger.jsonMode;
|
|
22747
23434
|
},
|
|
@@ -28856,7 +29543,7 @@ function convertFromMusecodeFormat(musecodeMcp) {
|
|
|
28856
29543
|
if (key === "mode") {
|
|
28857
29544
|
const mode = asMusecodeMode(value);
|
|
28858
29545
|
if (mode === void 0) {
|
|
28859
|
-
warnWithFallback(void 0, `Muse Code MCP: dropping mode ${
|
|
29546
|
+
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.`);
|
|
28860
29547
|
continue;
|
|
28861
29548
|
}
|
|
28862
29549
|
converted.musecodeMode = mode;
|
|
@@ -30060,7 +30747,7 @@ function pointerLabels(global) {
|
|
|
30060
30747
|
async function warnAtDocumentedDefault({ existing, outputRoot, logger }) {
|
|
30061
30748
|
const { pointer, configLabel, mcpLabel } = pointerLabels(true);
|
|
30062
30749
|
const displaced = await describeDisplacedGlobalServers({ outputRoot });
|
|
30063
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${
|
|
30750
|
+
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}".`));
|
|
30064
30751
|
}
|
|
30065
30752
|
/**
|
|
30066
30753
|
* Announce a pointer that was just written. Warned rather than noted in global
|
|
@@ -30117,10 +30804,10 @@ async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outp
|
|
|
30117
30804
|
return false;
|
|
30118
30805
|
}
|
|
30119
30806
|
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
30120
|
-
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} is ${
|
|
30807
|
+
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.`);
|
|
30121
30808
|
return false;
|
|
30122
30809
|
}
|
|
30123
|
-
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath as ${
|
|
30810
|
+
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}".`);
|
|
30124
30811
|
return false;
|
|
30125
30812
|
}
|
|
30126
30813
|
/**
|
|
@@ -31740,11 +32427,10 @@ var ToolPermissions = class extends ToolFile {
|
|
|
31740
32427
|
throw new Error("Please implement this method in the subclass.");
|
|
31741
32428
|
}
|
|
31742
32429
|
toRulesyncPermissionsDefault({ fileContent }) {
|
|
31743
|
-
return
|
|
32430
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
31744
32431
|
outputRoot: this.outputRoot,
|
|
31745
|
-
|
|
31746
|
-
|
|
31747
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent })
|
|
32432
|
+
fileContent,
|
|
32433
|
+
sourcePath: this.getRelativePathFromCwd()
|
|
31748
32434
|
});
|
|
31749
32435
|
}
|
|
31750
32436
|
static async fromFile(_params) {
|
|
@@ -35502,7 +36188,7 @@ function asCursorPermissionEntryArray(value, logger, fieldLabel) {
|
|
|
35502
36188
|
if (!Array.isArray(value)) return [];
|
|
35503
36189
|
const result = [];
|
|
35504
36190
|
for (const item of value) if (typeof item === "string") result.push(item);
|
|
35505
|
-
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${
|
|
36191
|
+
else logger?.warn(`Cursor CLI permissions${fieldLabel ? `.${fieldLabel}` : ""} contains a non-string entry; dropping ${quoteValueForWarning(item)}.`);
|
|
35506
36192
|
return result;
|
|
35507
36193
|
}
|
|
35508
36194
|
/**
|
|
@@ -37356,7 +38042,10 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
37356
38042
|
fileContent: this.getFileContent()
|
|
37357
38043
|
});
|
|
37358
38044
|
const rawProvenance = (isRecord$1(config.permissions) ? config.permissions : {}).rulesync;
|
|
37359
|
-
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(isRecord$1(rawProvenance) ?
|
|
38045
|
+
const parsedProvenance = RulesyncPermissionsFileSchema.safeParse(isRecord$1(rawProvenance) ? withoutBlankPermissionKeysIn({
|
|
38046
|
+
config: rawProvenance,
|
|
38047
|
+
sourcePath: this.getRelativePathFromCwd()
|
|
38048
|
+
}) : rawProvenance);
|
|
37360
38049
|
const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
|
|
37361
38050
|
const permission = clonePermissionBlock(provenance.permission);
|
|
37362
38051
|
reconcileCommandAllowlist({
|
|
@@ -37384,14 +38073,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
37384
38073
|
permission,
|
|
37385
38074
|
...Object.keys(hermes).length > 0 && { hermes }
|
|
37386
38075
|
};
|
|
37387
|
-
return
|
|
38076
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
37388
38077
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
37389
38078
|
nativeOutputRoot: this.outputRoot,
|
|
37390
38079
|
global: this.global
|
|
37391
38080
|
}),
|
|
37392
|
-
|
|
37393
|
-
|
|
37394
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent: JSON.stringify(imported, null, 2) })
|
|
38081
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
38082
|
+
fileContent: JSON.stringify(imported, null, 2)
|
|
37395
38083
|
});
|
|
37396
38084
|
}
|
|
37397
38085
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
@@ -38292,17 +38980,16 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
|
|
|
38292
38980
|
...nativeRules.length > 0 && { rules: nativeRules },
|
|
38293
38981
|
...tools && { tools }
|
|
38294
38982
|
};
|
|
38295
|
-
return
|
|
38983
|
+
return RulesyncPermissions.fromImportedFileContent({
|
|
38296
38984
|
outputRoot: getKimiCodeRulesyncOutputRoot({
|
|
38297
38985
|
nativeOutputRoot: this.outputRoot,
|
|
38298
38986
|
global: this.global
|
|
38299
38987
|
}),
|
|
38300
|
-
|
|
38301
|
-
|
|
38302
|
-
fileContent: withoutBlankPermissionPatterns({ fileContent: JSON.stringify({
|
|
38988
|
+
sourcePath: this.getRelativePathFromCwd(),
|
|
38989
|
+
fileContent: JSON.stringify({
|
|
38303
38990
|
permission,
|
|
38304
38991
|
...Object.keys(toolOverride).length > 0 && { "kimi-code": toolOverride }
|
|
38305
|
-
}, null, 2)
|
|
38992
|
+
}, null, 2)
|
|
38306
38993
|
});
|
|
38307
38994
|
}
|
|
38308
38995
|
static forDeletion({ outputRoot = process.cwd() }) {
|
|
@@ -40876,6 +41563,28 @@ function pickSecurityPolicies(source, report) {
|
|
|
40876
41563
|
* Ambiguous-width characters are counted as one column, which is what a
|
|
40877
41564
|
* terminal running a Latin font does.
|
|
40878
41565
|
*
|
|
41566
|
+
* Two of the ranges are here for a narrower reason: a name the skill prompt can
|
|
41567
|
+
* offer may not be counted narrower here than the prompt's own renderer counts
|
|
41568
|
+
* it, or a label that fits the budget wraps anyway and paints the second row
|
|
41569
|
+
* the budget exists to prevent. `@inquirer/core` measures with
|
|
41570
|
+
* `fast-string-width`, which takes the whole of `Script=Hangul` as wide and
|
|
41571
|
+
* every `Emoji_Modifier_Base` as an emoji. So the Hangul jamo are taken to
|
|
41572
|
+
* U+11FF rather than stopping at the leading consonants, and the modifier bases
|
|
41573
|
+
* are named beside the emoji: U+261D, U+26F9 and the two hands of U+270C–U+270D
|
|
41574
|
+
* are `Emoji` without being `Emoji_Presentation`, and were the only characters
|
|
41575
|
+
* outside Hangul this counted at one column while the renderer counted two.
|
|
41576
|
+
*
|
|
41577
|
+
* The rule is over the names that can reach the prompt, which is a smaller set
|
|
41578
|
+
* than the characters that exist. The renderer counts a tab at eight columns and
|
|
41579
|
+
* the Hangul fillers at two, where this counts one and none: a name carrying
|
|
41580
|
+
* either is refused outright by `hasDeceptiveHiddenCharacters` — the tab as a
|
|
41581
|
+
* control character, the fillers as characters that draw as nothing — and never
|
|
41582
|
+
* becomes a row to be measured. The two joiners are the invisible characters
|
|
41583
|
+
* that check lets through, so they are counted below rather than left to the
|
|
41584
|
+
* zero-width rule. Where this is used to lay out text of the tool's own rather
|
|
41585
|
+
* than to bound an untrusted name, the difference is a column of alignment and
|
|
41586
|
+
* not a forged row.
|
|
41587
|
+
*
|
|
40879
41588
|
* The wide planes are taken whole rather than range by range — Tangut, Khitan
|
|
40880
41589
|
* and Nushu together are U+17000–U+18DFF, and the kana supplements are
|
|
40881
41590
|
* U+1AFF0–U+1B2FF — because a gap between two of them is exactly the character
|
|
@@ -40888,7 +41597,7 @@ function pickSecurityPolicies(source, report) {
|
|
|
40888
41597
|
* is canonically the ordinary ideograph U+8C48, and a range that starts there
|
|
40889
41598
|
* instead silently swallows thirty thousand code points that are not wide.
|
|
40890
41599
|
*/
|
|
40891
|
-
const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|[\u2329\u232a\u2630-\u2637\u268a-\u268f\u4dc0-\u4dff]|[\u1100-\
|
|
41600
|
+
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;
|
|
40892
41601
|
/**
|
|
40893
41602
|
* U+FE0F VARIATION SELECTOR-16, which takes no width of its own but asks the
|
|
40894
41603
|
* character before it to be drawn as an emoji — that is, in two columns rather
|
|
@@ -40902,6 +41611,27 @@ const COMBINING_MARK_PATTERN = /[\p{Mn}\p{Me}]/u;
|
|
|
40902
41611
|
/** The characters that take no width at all, marks aside. */
|
|
40903
41612
|
const ZERO_WIDTH_CHARACTERS_PATTERN = /[\p{Cf}\p{Default_Ignorable_Code_Point}]/u;
|
|
40904
41613
|
/**
|
|
41614
|
+
* The zero-width joiner and its non-joining twin, which the renderer spends a
|
|
41615
|
+
* column on apiece.
|
|
41616
|
+
*
|
|
41617
|
+
* A terminal draws neither, and every other character that draws as nothing is
|
|
41618
|
+
* counted at nothing here. These two are the exception because they are the
|
|
41619
|
+
* only invisible characters `hasDeceptiveHiddenCharacters` lets through — a
|
|
41620
|
+
* Persian or Indic name spells a word with one, and an emoji is a chain of them
|
|
41621
|
+
* — so they are the only ones an attacker can put in a name that reaches the
|
|
41622
|
+
* prompt. `fast-string-width`, which is where the prompt's own wrapping is
|
|
41623
|
+
* decided, counts each of them as a column, and 40 of them in a name is 40
|
|
41624
|
+
* columns of budget this would otherwise hand over for free: enough for a name
|
|
41625
|
+
* measured at 39 columns to be drawn at 77 and wrap a forged row underneath
|
|
41626
|
+
* itself.
|
|
41627
|
+
*
|
|
41628
|
+
* The cost is that an emoji built from a chain is overstated by a column per
|
|
41629
|
+
* joiner, on top of the two columns per component it is already overstated by.
|
|
41630
|
+
* Overstating shortens a label that did not need it; understating lets one
|
|
41631
|
+
* wrap.
|
|
41632
|
+
*/
|
|
41633
|
+
const RENDERER_COUNTED_JOINERS = /\u200c|\u200d/u;
|
|
41634
|
+
/**
|
|
40905
41635
|
* How many marks a single character is allowed to carry for free.
|
|
40906
41636
|
*
|
|
40907
41637
|
* A written language stacks two or three at most — a Devanagari vowel sign and
|
|
@@ -40929,6 +41659,7 @@ function widthInContext(params) {
|
|
|
40929
41659
|
const { character, precedingMarks } = params;
|
|
40930
41660
|
if (character === EMOJI_PRESENTATION_SELECTOR) return 1;
|
|
40931
41661
|
if (isCombiningMark(character)) return precedingMarks < FREE_MARKS_PER_CHARACTER ? 0 : 1;
|
|
41662
|
+
if (RENDERER_COUNTED_JOINERS.test(character)) return 1;
|
|
40932
41663
|
if (ZERO_WIDTH_CHARACTERS_PATTERN.test(character)) return 0;
|
|
40933
41664
|
return WIDE_CHARACTERS_PATTERN.test(character) ? 2 : 1;
|
|
40934
41665
|
}
|
|
@@ -40951,6 +41682,8 @@ function displayWidthOf(text) {
|
|
|
40951
41682
|
}
|
|
40952
41683
|
return width;
|
|
40953
41684
|
}
|
|
41685
|
+
/** The mark a cut string ends in. */
|
|
41686
|
+
const SHORTENING_ELLIPSIS = "…";
|
|
40954
41687
|
/**
|
|
40955
41688
|
* Cut `text` down to at most `budget` columns, marking the cut with an ellipsis.
|
|
40956
41689
|
*
|
|
@@ -40976,7 +41709,7 @@ function shortenToWidth(params) {
|
|
|
40976
41709
|
width += characterWidth;
|
|
40977
41710
|
marks = isCombiningMark(character) ? marks + 1 : 0;
|
|
40978
41711
|
}
|
|
40979
|
-
return `${kept.join("")}
|
|
41712
|
+
return `${kept.join("")}${SHORTENING_ELLIPSIS}`;
|
|
40980
41713
|
}
|
|
40981
41714
|
//#endregion
|
|
40982
41715
|
//#region src/features/permissions/vibe-permissions.ts
|
|
@@ -44764,6 +45497,83 @@ const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
|
|
|
44764
45497
|
];
|
|
44765
45498
|
//#endregion
|
|
44766
45499
|
//#region src/features/skills/claudecode-skill.ts
|
|
45500
|
+
/**
|
|
45501
|
+
* The `.claude/skills` tail every scanned root is expected to end with, written
|
|
45502
|
+
* posix-separated so it can be compared against a resolved relative path, and
|
|
45503
|
+
* split into its segments so what sits above it can be taken apart.
|
|
45504
|
+
*
|
|
45505
|
+
* Split here rather than through the shared helper: this is evaluated as the
|
|
45506
|
+
* module loads, before a test that mocks the file utilities can supply one.
|
|
45507
|
+
*/
|
|
45508
|
+
const CLAUDECODE_SKILLS_DIR_SEGMENTS = CLAUDECODE_SKILLS_DIR_PATH.split(sep);
|
|
45509
|
+
const CLAUDECODE_SKILLS_DIR_POSIX_PATH = CLAUDECODE_SKILLS_DIR_SEGMENTS.join("/");
|
|
45510
|
+
/**
|
|
45511
|
+
* The segment that puts `relativeDirPath` inside a tree the nested scan
|
|
45512
|
+
* excludes, or `undefined` when none does. These are the same three rules the
|
|
45513
|
+
* scan states as glob `ignore` patterns below -- dependency trees at any depth,
|
|
45514
|
+
* build and vendoring directories at the project root, hidden directories other
|
|
45515
|
+
* than the `.claude` being matched -- and the two have to be kept in step.
|
|
45516
|
+
*
|
|
45517
|
+
* Saying them twice is what a second pass costs. globby matches its patterns
|
|
45518
|
+
* against the path it reports, before the `..` a rewritten directory name
|
|
45519
|
+
* carries is folded away and before a link in the path is resolved. A root
|
|
45520
|
+
* reported at `x/../node_modules/.claude/skills` matches none of the patterns
|
|
45521
|
+
* and then leads to the dependency tree they name, so the decision has to be
|
|
45522
|
+
* taken again on the path that is really read.
|
|
45523
|
+
*
|
|
45524
|
+
* The segments passed are the ones above the `.claude/skills` tail, taken from
|
|
45525
|
+
* the resolved path: a directory name may hold a backslash -- the whole reason a
|
|
45526
|
+
* path can arrive here misspelled -- so the split that produced them has to be on
|
|
45527
|
+
* `/`, the one separator no name can contain. The tail itself is the part the
|
|
45528
|
+
* glob matched and is not judged; `.claude` is hidden by definition and every
|
|
45529
|
+
* root the scan reports ends with it.
|
|
45530
|
+
*/
|
|
45531
|
+
function excludedNestedScanSegment(segments) {
|
|
45532
|
+
return segments.find((segment, index) => NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.includes(segment) || index === 0 && NESTED_SCAN_EXCLUDED_ROOT_DIRS.includes(segment) || isHiddenPathSegment(segment));
|
|
45533
|
+
}
|
|
45534
|
+
/**
|
|
45535
|
+
* Whether a nested skills directory the scan reported can be used as an import
|
|
45536
|
+
* root: either the reason it cannot, or the path it resolves to relative to the
|
|
45537
|
+
* project, which the caller uses to tell two spellings of one root apart.
|
|
45538
|
+
*
|
|
45539
|
+
* A recursive glob cannot be swapped for a walk the way a flat one can, so the
|
|
45540
|
+
* path it hands back is checked instead. globby reads a backslash as a path
|
|
45541
|
+
* separator and rewrites it, so a root below a directory really named
|
|
45542
|
+
* `back\\slash` is reported at `back/slash`. Where that leads decides what to
|
|
45543
|
+
* do with it, and the spelling alone does not say: `back/slash` usually answers
|
|
45544
|
+
* to nothing, but `x\\..\\..\\outside` is reported at `x/../../outside`, which
|
|
45545
|
+
* climbs out of the project through the real sibling `x/`, and `a\\b` at `a/b`,
|
|
45546
|
+
* which may be a symbolic link out of the project that the scan — it passes
|
|
45547
|
+
* `followSymbolicLinks: false` — never meant to reach. Both are refused by
|
|
45548
|
+
* resolving the path rather than reading it.
|
|
45549
|
+
*
|
|
45550
|
+
* What is deliberately not refused is a rewritten path that stays inside the
|
|
45551
|
+
* project, such as `x\\..\\y` reported at `x/../y`. It names a real directory
|
|
45552
|
+
* `y`, and the scan reports that directory under this spelling *instead of* its
|
|
45553
|
+
* own, so refusing it would lose `y`'s skills rather than protect anything. The
|
|
45554
|
+
* skills under the directory that was really named are unreachable either way:
|
|
45555
|
+
* no path the scan can report leads back to a name holding a backslash.
|
|
45556
|
+
*
|
|
45557
|
+
* That last shape is the one case the scan cannot warn about. `a\\b` reported
|
|
45558
|
+
* at `a/b`, where `a/b` is itself a real directory, is indistinguishable from
|
|
45559
|
+
* the ordinary root `a/b` -- both are spelled the same and both are there -- so
|
|
45560
|
+
* the skills under `a\\b` are dropped without a word. Nothing in the path says
|
|
45561
|
+
* a second directory was ever involved.
|
|
45562
|
+
*/
|
|
45563
|
+
async function checkNestedSkillsRoot({ outputRoot, dirPath }) {
|
|
45564
|
+
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." };
|
|
45565
|
+
const realRelativeDirPath = await resolvedRelativePath({
|
|
45566
|
+
rootPath: outputRoot,
|
|
45567
|
+
targetPath: dirPath
|
|
45568
|
+
});
|
|
45569
|
+
if (posixRelativePathEscapesRoot(realRelativeDirPath)) return { reason: "it resolves outside the project." };
|
|
45570
|
+
const segments = realRelativeDirPath.split("/");
|
|
45571
|
+
const aboveTailSegments = segments.slice(0, -CLAUDECODE_SKILLS_DIR_SEGMENTS.length);
|
|
45572
|
+
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.` };
|
|
45573
|
+
const excludedSegment = excludedNestedScanSegment(aboveTailSegments);
|
|
45574
|
+
if (excludedSegment !== void 0) return { reason: `it resolves inside ${JSON.stringify(stripControlCharacters(excludedSegment))}, which the nested scan excludes.` };
|
|
45575
|
+
return { realRelativeDirPath };
|
|
45576
|
+
}
|
|
44767
45577
|
const ClaudecodeSkillFrontmatterSchema = z.looseObject({
|
|
44768
45578
|
name: z.string(),
|
|
44769
45579
|
description: z.string(),
|
|
@@ -45012,10 +45822,10 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
45012
45822
|
*
|
|
45013
45823
|
* @see https://code.claude.com/docs/en/skills
|
|
45014
45824
|
*/
|
|
45015
|
-
static async getConfiguredImportRoots({ outputRoot, global = false }) {
|
|
45825
|
+
static async getConfiguredImportRoots({ outputRoot, global = false, logger }) {
|
|
45016
45826
|
if (global) return [];
|
|
45017
45827
|
const root = toPosixPath(outputRoot);
|
|
45018
|
-
|
|
45828
|
+
const filteredDirPaths = filterOutPathsInGitIgnoredDirectories({
|
|
45019
45829
|
rootDir: outputRoot,
|
|
45020
45830
|
filePaths: await findFilesByGlobs([`${root}/*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`], {
|
|
45021
45831
|
type: "dir",
|
|
@@ -45026,10 +45836,27 @@ var ClaudecodeSkill = class extends ToolSkill {
|
|
|
45026
45836
|
...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
|
|
45027
45837
|
]
|
|
45028
45838
|
})
|
|
45029
|
-
}).toSorted()
|
|
45030
|
-
|
|
45031
|
-
|
|
45032
|
-
|
|
45839
|
+
}).toSorted();
|
|
45840
|
+
const roots = [];
|
|
45841
|
+
const seenRealRelativeDirPaths = /* @__PURE__ */ new Set([CLAUDECODE_SKILLS_DIR_POSIX_PATH]);
|
|
45842
|
+
for (const dirPath of filteredDirPaths) {
|
|
45843
|
+
const scannedDirPath = resolve(dirPath);
|
|
45844
|
+
const check = await checkNestedSkillsRoot({
|
|
45845
|
+
outputRoot,
|
|
45846
|
+
dirPath: scannedDirPath
|
|
45847
|
+
});
|
|
45848
|
+
if ("reason" in check) {
|
|
45849
|
+
logger?.warn(`Skipping the nested Claude Code skills directory ${JSON.stringify(stripControlCharacters(scannedDirPath))}: ${check.reason} Its skills are not imported.`);
|
|
45850
|
+
continue;
|
|
45851
|
+
}
|
|
45852
|
+
if (seenRealRelativeDirPaths.has(check.realRelativeDirPath)) continue;
|
|
45853
|
+
seenRealRelativeDirPaths.add(check.realRelativeDirPath);
|
|
45854
|
+
roots.push({
|
|
45855
|
+
outputRoot,
|
|
45856
|
+
relativeDirPath: relative(outputRoot, scannedDirPath)
|
|
45857
|
+
});
|
|
45858
|
+
}
|
|
45859
|
+
return roots;
|
|
45033
45860
|
}
|
|
45034
45861
|
getFrontmatter() {
|
|
45035
45862
|
return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
@@ -49916,7 +50743,8 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
49916
50743
|
const paths = factory.class.getSettablePaths({ global: this.global });
|
|
49917
50744
|
const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
|
|
49918
50745
|
outputRoot: this.outputRoot,
|
|
49919
|
-
global: this.global
|
|
50746
|
+
global: this.global,
|
|
50747
|
+
logger: this.logger
|
|
49920
50748
|
}) : [];
|
|
49921
50749
|
const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
|
|
49922
50750
|
const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
|
|
@@ -54632,6 +55460,152 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
54632
55460
|
}
|
|
54633
55461
|
};
|
|
54634
55462
|
//#endregion
|
|
55463
|
+
//#region src/features/subagents/zcode-subagent.ts
|
|
55464
|
+
const ZcodeSubagentFrontmatterSchema = z.looseObject({
|
|
55465
|
+
name: z.string(),
|
|
55466
|
+
description: z.optional(z.string()),
|
|
55467
|
+
model: z.optional(z.string()),
|
|
55468
|
+
thoughtLevel: z.optional(z.string()),
|
|
55469
|
+
color: z.optional(z.string()),
|
|
55470
|
+
tools: z.optional(z.array(z.string())),
|
|
55471
|
+
disallowedTools: z.optional(z.array(z.string())),
|
|
55472
|
+
maxTurns: z.optional(z.number().check(z.int(), z.positive())),
|
|
55473
|
+
injectAgentsMd: z.optional(z.boolean()),
|
|
55474
|
+
mcpServers: z.optional(z.array(z.string()))
|
|
55475
|
+
});
|
|
55476
|
+
/**
|
|
55477
|
+
* ZCode subagents.
|
|
55478
|
+
*
|
|
55479
|
+
* Each subagent is one Markdown file with YAML frontmatter, named after the
|
|
55480
|
+
* agent, under `~/.zcode/agents/`.
|
|
55481
|
+
*
|
|
55482
|
+
* Global scope only. The current Beta "manages global / user-level subagents
|
|
55483
|
+
* stored under `~/.zcode/agents/`", and creating or editing workspace /
|
|
55484
|
+
* project-level subagents "is not available yet" — so this adapter is
|
|
55485
|
+
* registered with `supportsProject: false` and never writes into a project's
|
|
55486
|
+
* own `.zcode/`. The relative path is nonetheless spelled against
|
|
55487
|
+
* {@link ZCODE_AGENTS_DIR_PATH} so the workspace scope needs nothing more than
|
|
55488
|
+
* flipping that flag if ZCode ships it.
|
|
55489
|
+
*
|
|
55490
|
+
* @see https://zcode.z.ai/en/docs/subagents
|
|
55491
|
+
*/
|
|
55492
|
+
var ZcodeSubagent = class ZcodeSubagent extends ToolSubagent {
|
|
55493
|
+
frontmatter;
|
|
55494
|
+
body;
|
|
55495
|
+
constructor({ frontmatter, body, fileContent, ...rest }) {
|
|
55496
|
+
if (rest.validate !== false) {
|
|
55497
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
55498
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
55499
|
+
}
|
|
55500
|
+
super({
|
|
55501
|
+
...rest,
|
|
55502
|
+
fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter)
|
|
55503
|
+
});
|
|
55504
|
+
this.frontmatter = frontmatter;
|
|
55505
|
+
this.body = body;
|
|
55506
|
+
}
|
|
55507
|
+
static getSettablePaths(_options = {}) {
|
|
55508
|
+
return { relativeDirPath: ZCODE_AGENTS_DIR_PATH };
|
|
55509
|
+
}
|
|
55510
|
+
getFrontmatter() {
|
|
55511
|
+
return this.frontmatter;
|
|
55512
|
+
}
|
|
55513
|
+
getBody() {
|
|
55514
|
+
return this.body;
|
|
55515
|
+
}
|
|
55516
|
+
toRulesyncSubagent() {
|
|
55517
|
+
const { name, description, ...rest } = this.frontmatter;
|
|
55518
|
+
return new RulesyncSubagent({
|
|
55519
|
+
outputRoot: ".",
|
|
55520
|
+
frontmatter: {
|
|
55521
|
+
targets: ["*"],
|
|
55522
|
+
name,
|
|
55523
|
+
description,
|
|
55524
|
+
...Object.keys(rest).length > 0 && { zcode: rest }
|
|
55525
|
+
},
|
|
55526
|
+
body: this.body,
|
|
55527
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
55528
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
55529
|
+
validate: true
|
|
55530
|
+
});
|
|
55531
|
+
}
|
|
55532
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
55533
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
55534
|
+
const zcodeSection = rulesyncFrontmatter.zcode ?? {};
|
|
55535
|
+
const zcodeFrontmatter = {
|
|
55536
|
+
name: rulesyncFrontmatter.name,
|
|
55537
|
+
description: rulesyncFrontmatter.description,
|
|
55538
|
+
...zcodeSection
|
|
55539
|
+
};
|
|
55540
|
+
const body = rulesyncSubagent.getBody();
|
|
55541
|
+
const fileContent = stringifyFrontmatter(body, zcodeFrontmatter, { avoidBlockScalars: true });
|
|
55542
|
+
const paths = this.getSettablePaths({ global });
|
|
55543
|
+
return new ZcodeSubagent({
|
|
55544
|
+
outputRoot,
|
|
55545
|
+
frontmatter: zcodeFrontmatter,
|
|
55546
|
+
body,
|
|
55547
|
+
relativeDirPath: paths.relativeDirPath,
|
|
55548
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
55549
|
+
fileContent,
|
|
55550
|
+
validate,
|
|
55551
|
+
global
|
|
55552
|
+
});
|
|
55553
|
+
}
|
|
55554
|
+
validate() {
|
|
55555
|
+
if (!this.frontmatter) return {
|
|
55556
|
+
success: true,
|
|
55557
|
+
error: null
|
|
55558
|
+
};
|
|
55559
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
55560
|
+
if (result.success) return {
|
|
55561
|
+
success: true,
|
|
55562
|
+
error: null
|
|
55563
|
+
};
|
|
55564
|
+
else return {
|
|
55565
|
+
success: false,
|
|
55566
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
55567
|
+
};
|
|
55568
|
+
}
|
|
55569
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
55570
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
55571
|
+
rulesyncSubagent,
|
|
55572
|
+
toolTarget: "zcode"
|
|
55573
|
+
});
|
|
55574
|
+
}
|
|
55575
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
55576
|
+
const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
|
|
55577
|
+
const filePath = join(outputRoot, dirPath, relativeFilePath);
|
|
55578
|
+
const fileContent = await readFileContent(filePath);
|
|
55579
|
+
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
55580
|
+
const result = ZcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
55581
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
55582
|
+
return new ZcodeSubagent({
|
|
55583
|
+
outputRoot,
|
|
55584
|
+
relativeDirPath: dirPath,
|
|
55585
|
+
relativeFilePath,
|
|
55586
|
+
frontmatter: result.data,
|
|
55587
|
+
body: content.trim(),
|
|
55588
|
+
fileContent,
|
|
55589
|
+
validate,
|
|
55590
|
+
global
|
|
55591
|
+
});
|
|
55592
|
+
}
|
|
55593
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
55594
|
+
return new ZcodeSubagent({
|
|
55595
|
+
outputRoot,
|
|
55596
|
+
relativeDirPath,
|
|
55597
|
+
relativeFilePath,
|
|
55598
|
+
frontmatter: {
|
|
55599
|
+
name: "",
|
|
55600
|
+
description: ""
|
|
55601
|
+
},
|
|
55602
|
+
body: "",
|
|
55603
|
+
fileContent: "",
|
|
55604
|
+
validate: false
|
|
55605
|
+
});
|
|
55606
|
+
}
|
|
55607
|
+
};
|
|
55608
|
+
//#endregion
|
|
54635
55609
|
//#region src/features/subagents/zoocode-subagent.ts
|
|
54636
55610
|
/**
|
|
54637
55611
|
* Subagent (custom-mode) generator for **Zoo Code**, the community
|
|
@@ -54696,6 +55670,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54696
55670
|
["agentsmd", {
|
|
54697
55671
|
class: AgentsmdSubagent,
|
|
54698
55672
|
meta: {
|
|
55673
|
+
supportsProject: true,
|
|
54699
55674
|
supportsSimulated: true,
|
|
54700
55675
|
supportsGlobal: false,
|
|
54701
55676
|
filePattern: "*.md"
|
|
@@ -54704,6 +55679,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54704
55679
|
["antigravity-cli", {
|
|
54705
55680
|
class: AntigravityCliSubagent,
|
|
54706
55681
|
meta: {
|
|
55682
|
+
supportsProject: true,
|
|
54707
55683
|
supportsSimulated: false,
|
|
54708
55684
|
supportsGlobal: true,
|
|
54709
55685
|
filePattern: "*.md"
|
|
@@ -54712,6 +55688,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54712
55688
|
["antigravity-ide", {
|
|
54713
55689
|
class: AntigravityIdeSubagent,
|
|
54714
55690
|
meta: {
|
|
55691
|
+
supportsProject: true,
|
|
54715
55692
|
supportsSimulated: false,
|
|
54716
55693
|
supportsGlobal: true,
|
|
54717
55694
|
filePattern: "*.md"
|
|
@@ -54720,6 +55697,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54720
55697
|
["antigravity-plugin", {
|
|
54721
55698
|
class: AntigravityPluginSubagent,
|
|
54722
55699
|
meta: {
|
|
55700
|
+
supportsProject: true,
|
|
54723
55701
|
supportsSimulated: false,
|
|
54724
55702
|
supportsGlobal: false,
|
|
54725
55703
|
filePattern: "*.md"
|
|
@@ -54728,6 +55706,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54728
55706
|
["augmentcode", {
|
|
54729
55707
|
class: AugmentcodeSubagent,
|
|
54730
55708
|
meta: {
|
|
55709
|
+
supportsProject: true,
|
|
54731
55710
|
supportsSimulated: false,
|
|
54732
55711
|
supportsGlobal: true,
|
|
54733
55712
|
filePattern: "*.md"
|
|
@@ -54736,6 +55715,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54736
55715
|
["claudecode", {
|
|
54737
55716
|
class: ClaudecodeSubagent,
|
|
54738
55717
|
meta: {
|
|
55718
|
+
supportsProject: true,
|
|
54739
55719
|
supportsSimulated: false,
|
|
54740
55720
|
supportsGlobal: true,
|
|
54741
55721
|
filePattern: "*.md"
|
|
@@ -54744,6 +55724,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54744
55724
|
["claudecode-plugin", {
|
|
54745
55725
|
class: ClaudecodePluginSubagent,
|
|
54746
55726
|
meta: {
|
|
55727
|
+
supportsProject: true,
|
|
54747
55728
|
supportsSimulated: false,
|
|
54748
55729
|
supportsGlobal: false,
|
|
54749
55730
|
filePattern: "*.md"
|
|
@@ -54752,6 +55733,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54752
55733
|
["claudecode-legacy", {
|
|
54753
55734
|
class: ClaudecodeSubagent,
|
|
54754
55735
|
meta: {
|
|
55736
|
+
supportsProject: true,
|
|
54755
55737
|
supportsSimulated: false,
|
|
54756
55738
|
supportsGlobal: true,
|
|
54757
55739
|
filePattern: "*.md"
|
|
@@ -54760,6 +55742,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54760
55742
|
["cline", {
|
|
54761
55743
|
class: ClineSubagent,
|
|
54762
55744
|
meta: {
|
|
55745
|
+
supportsProject: true,
|
|
54763
55746
|
supportsSimulated: false,
|
|
54764
55747
|
supportsGlobal: true,
|
|
54765
55748
|
filePattern: "*.{yaml,yml}"
|
|
@@ -54768,6 +55751,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54768
55751
|
["codexcli", {
|
|
54769
55752
|
class: CodexCliSubagent,
|
|
54770
55753
|
meta: {
|
|
55754
|
+
supportsProject: true,
|
|
54771
55755
|
supportsSimulated: false,
|
|
54772
55756
|
supportsGlobal: true,
|
|
54773
55757
|
filePattern: "*.toml"
|
|
@@ -54776,6 +55760,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54776
55760
|
["copilot", {
|
|
54777
55761
|
class: CopilotSubagent,
|
|
54778
55762
|
meta: {
|
|
55763
|
+
supportsProject: true,
|
|
54779
55764
|
supportsSimulated: false,
|
|
54780
55765
|
supportsGlobal: true,
|
|
54781
55766
|
filePattern: "*.md"
|
|
@@ -54784,6 +55769,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54784
55769
|
["copilotcli", {
|
|
54785
55770
|
class: CopilotcliSubagent,
|
|
54786
55771
|
meta: {
|
|
55772
|
+
supportsProject: true,
|
|
54787
55773
|
supportsSimulated: false,
|
|
54788
55774
|
supportsGlobal: true,
|
|
54789
55775
|
filePattern: "*.agent.md"
|
|
@@ -54792,6 +55778,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54792
55778
|
["cursor", {
|
|
54793
55779
|
class: CursorSubagent,
|
|
54794
55780
|
meta: {
|
|
55781
|
+
supportsProject: true,
|
|
54795
55782
|
supportsSimulated: false,
|
|
54796
55783
|
supportsGlobal: true,
|
|
54797
55784
|
filePattern: "*.md"
|
|
@@ -54800,6 +55787,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54800
55787
|
["deepagents", {
|
|
54801
55788
|
class: DeepagentsSubagent,
|
|
54802
55789
|
meta: {
|
|
55790
|
+
supportsProject: true,
|
|
54803
55791
|
supportsSimulated: false,
|
|
54804
55792
|
supportsGlobal: true,
|
|
54805
55793
|
filePattern: join("*", "AGENTS.md")
|
|
@@ -54808,6 +55796,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54808
55796
|
["devin", {
|
|
54809
55797
|
class: DevinSubagent,
|
|
54810
55798
|
meta: {
|
|
55799
|
+
supportsProject: true,
|
|
54811
55800
|
supportsSimulated: false,
|
|
54812
55801
|
supportsGlobal: true,
|
|
54813
55802
|
filePattern: join("*", "AGENT.md")
|
|
@@ -54816,6 +55805,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54816
55805
|
["factorydroid", {
|
|
54817
55806
|
class: FactorydroidSubagent,
|
|
54818
55807
|
meta: {
|
|
55808
|
+
supportsProject: true,
|
|
54819
55809
|
supportsSimulated: false,
|
|
54820
55810
|
supportsGlobal: true,
|
|
54821
55811
|
filePattern: "*.md"
|
|
@@ -54824,6 +55814,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54824
55814
|
["goose", {
|
|
54825
55815
|
class: GooseSubagent,
|
|
54826
55816
|
meta: {
|
|
55817
|
+
supportsProject: true,
|
|
54827
55818
|
supportsSimulated: false,
|
|
54828
55819
|
supportsGlobal: true,
|
|
54829
55820
|
filePattern: "*.md"
|
|
@@ -54832,6 +55823,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54832
55823
|
["hermesagent", {
|
|
54833
55824
|
class: HermesagentSubagent,
|
|
54834
55825
|
meta: {
|
|
55826
|
+
supportsProject: true,
|
|
54835
55827
|
supportsGlobal: true,
|
|
54836
55828
|
supportsSimulated: false,
|
|
54837
55829
|
filePattern: "*.json"
|
|
@@ -54840,6 +55832,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54840
55832
|
["grokcli", {
|
|
54841
55833
|
class: GrokcliSubagent,
|
|
54842
55834
|
meta: {
|
|
55835
|
+
supportsProject: true,
|
|
54843
55836
|
supportsSimulated: false,
|
|
54844
55837
|
supportsGlobal: true,
|
|
54845
55838
|
filePattern: "*.md"
|
|
@@ -54848,6 +55841,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54848
55841
|
["junie", {
|
|
54849
55842
|
class: JunieSubagent,
|
|
54850
55843
|
meta: {
|
|
55844
|
+
supportsProject: true,
|
|
54851
55845
|
supportsSimulated: false,
|
|
54852
55846
|
supportsGlobal: true,
|
|
54853
55847
|
filePattern: "*.md"
|
|
@@ -54856,6 +55850,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54856
55850
|
["kiro", {
|
|
54857
55851
|
class: KiroSubagent,
|
|
54858
55852
|
meta: {
|
|
55853
|
+
supportsProject: true,
|
|
54859
55854
|
supportsSimulated: false,
|
|
54860
55855
|
supportsGlobal: false,
|
|
54861
55856
|
filePattern: "*.json"
|
|
@@ -54864,6 +55859,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54864
55859
|
["kiro-cli", {
|
|
54865
55860
|
class: KiroCliSubagent,
|
|
54866
55861
|
meta: {
|
|
55862
|
+
supportsProject: true,
|
|
54867
55863
|
supportsSimulated: false,
|
|
54868
55864
|
supportsGlobal: true,
|
|
54869
55865
|
filePattern: "*.json"
|
|
@@ -54872,6 +55868,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54872
55868
|
["kiro-ide", {
|
|
54873
55869
|
class: KiroIdeSubagent,
|
|
54874
55870
|
meta: {
|
|
55871
|
+
supportsProject: true,
|
|
54875
55872
|
supportsSimulated: false,
|
|
54876
55873
|
supportsGlobal: true,
|
|
54877
55874
|
filePattern: "*.md"
|
|
@@ -54880,6 +55877,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54880
55877
|
["kilo", {
|
|
54881
55878
|
class: KiloSubagent,
|
|
54882
55879
|
meta: {
|
|
55880
|
+
supportsProject: true,
|
|
54883
55881
|
supportsSimulated: false,
|
|
54884
55882
|
supportsGlobal: true,
|
|
54885
55883
|
filePattern: "*.md"
|
|
@@ -54888,6 +55886,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54888
55886
|
["kimi-code", {
|
|
54889
55887
|
class: KimiCodeSubagent,
|
|
54890
55888
|
meta: {
|
|
55889
|
+
supportsProject: true,
|
|
54891
55890
|
supportsSimulated: false,
|
|
54892
55891
|
supportsGlobal: true,
|
|
54893
55892
|
filePattern: join("**", "*.md")
|
|
@@ -54896,6 +55895,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54896
55895
|
["opencode", {
|
|
54897
55896
|
class: OpenCodeSubagent,
|
|
54898
55897
|
meta: {
|
|
55898
|
+
supportsProject: true,
|
|
54899
55899
|
supportsSimulated: false,
|
|
54900
55900
|
supportsGlobal: true,
|
|
54901
55901
|
filePattern: "*.md"
|
|
@@ -54904,6 +55904,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54904
55904
|
["qwencode", {
|
|
54905
55905
|
class: QwencodeSubagent,
|
|
54906
55906
|
meta: {
|
|
55907
|
+
supportsProject: true,
|
|
54907
55908
|
supportsSimulated: false,
|
|
54908
55909
|
supportsGlobal: true,
|
|
54909
55910
|
filePattern: "*.md"
|
|
@@ -54912,6 +55913,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54912
55913
|
["reasonix", {
|
|
54913
55914
|
class: ReasonixSubagent,
|
|
54914
55915
|
meta: {
|
|
55916
|
+
supportsProject: true,
|
|
54915
55917
|
supportsSimulated: false,
|
|
54916
55918
|
supportsGlobal: true,
|
|
54917
55919
|
filePattern: join("*", "SKILL.md")
|
|
@@ -54920,6 +55922,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54920
55922
|
["roo", {
|
|
54921
55923
|
class: RooSubagent,
|
|
54922
55924
|
meta: {
|
|
55925
|
+
supportsProject: true,
|
|
54923
55926
|
supportsSimulated: false,
|
|
54924
55927
|
supportsGlobal: false,
|
|
54925
55928
|
filePattern: ".roomodes"
|
|
@@ -54928,6 +55931,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54928
55931
|
["zoocode", {
|
|
54929
55932
|
class: ZoocodeSubagent,
|
|
54930
55933
|
meta: {
|
|
55934
|
+
supportsProject: true,
|
|
54931
55935
|
supportsSimulated: false,
|
|
54932
55936
|
supportsGlobal: false,
|
|
54933
55937
|
filePattern: ".roomodes"
|
|
@@ -54936,6 +55940,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54936
55940
|
["rovodev", {
|
|
54937
55941
|
class: RovodevSubagent,
|
|
54938
55942
|
meta: {
|
|
55943
|
+
supportsProject: true,
|
|
54939
55944
|
supportsSimulated: false,
|
|
54940
55945
|
supportsGlobal: true,
|
|
54941
55946
|
filePattern: "*.md"
|
|
@@ -54944,6 +55949,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54944
55949
|
["takt", {
|
|
54945
55950
|
class: TaktSubagent,
|
|
54946
55951
|
meta: {
|
|
55952
|
+
supportsProject: true,
|
|
54947
55953
|
supportsSimulated: false,
|
|
54948
55954
|
supportsGlobal: true,
|
|
54949
55955
|
filePattern: "*.md"
|
|
@@ -54952,10 +55958,20 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
54952
55958
|
["vibe", {
|
|
54953
55959
|
class: VibeSubagent,
|
|
54954
55960
|
meta: {
|
|
55961
|
+
supportsProject: true,
|
|
54955
55962
|
supportsSimulated: false,
|
|
54956
55963
|
supportsGlobal: true,
|
|
54957
55964
|
filePattern: "*.toml"
|
|
54958
55965
|
}
|
|
55966
|
+
}],
|
|
55967
|
+
["zcode", {
|
|
55968
|
+
class: ZcodeSubagent,
|
|
55969
|
+
meta: {
|
|
55970
|
+
supportsProject: false,
|
|
55971
|
+
supportsSimulated: false,
|
|
55972
|
+
supportsGlobal: true,
|
|
55973
|
+
filePattern: "*.md"
|
|
55974
|
+
}
|
|
54959
55975
|
}]
|
|
54960
55976
|
]);
|
|
54961
55977
|
const defaultGetFactory$1 = (target) => {
|
|
@@ -54964,7 +55980,9 @@ const defaultGetFactory$1 = (target) => {
|
|
|
54964
55980
|
return factory;
|
|
54965
55981
|
};
|
|
54966
55982
|
const allToolTargetKeys$1 = [...toolSubagentFactories.keys()];
|
|
54967
|
-
const subagentsProcessorToolTargets = allToolTargetKeys$1
|
|
55983
|
+
const subagentsProcessorToolTargets = allToolTargetKeys$1.filter((target) => {
|
|
55984
|
+
return toolSubagentFactories.get(target)?.meta.supportsProject ?? false;
|
|
55985
|
+
});
|
|
54968
55986
|
const subagentsProcessorToolTargetsSimulated = allToolTargetKeys$1.filter((target) => {
|
|
54969
55987
|
return toolSubagentFactories.get(target)?.meta.supportsSimulated ?? false;
|
|
54970
55988
|
});
|
|
@@ -55064,7 +56082,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
55064
56082
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
55065
56083
|
return [];
|
|
55066
56084
|
}
|
|
55067
|
-
const mdFiles = (await
|
|
56085
|
+
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
55068
56086
|
if (mdFiles.length === 0) {
|
|
55069
56087
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
55070
56088
|
return [];
|
|
@@ -63610,6 +64628,6 @@ async function importChecksCore(params) {
|
|
|
63610
64628
|
return writtenCount;
|
|
63611
64629
|
}
|
|
63612
64630
|
//#endregion
|
|
63613
|
-
export {
|
|
64631
|
+
export { SHARED_USER_MANAGED_CONFIG_PATHS as $, MAX_FILE_SIZE as $t, groupSpellingsByCaseFoldedIdentity as A, DEPRECATED_FEATURE_REPLACEMENTS as An, isFileSystemError as At, RulesyncPermissions as B, removeFile as Bt, CLAUDECODE_SKILLS_DIR_PATH as C, RULESYNC_RULES_RELATIVE_DIR_PATH as Cn, createTempDirectory as Ct, FACTORYDROID_DIR as D, parseCommaSeparatedList as Dn, getFileSize as Dt, CODEXCLI_DIR as E, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as En, fileExists as Et, RulesyncSubagentFrontmatterSchema as F, stripControlCharactersKeepingLineFeeds as Fn, pathEscapesRoot as Ft, resolveRulesyncSourceWritePath as G, toPosixPath as Gt, RulesyncIgnore as H, removeTempDirectory as Ht, RulesyncSkill as I, stripHiddenCharacters as In, readFileContent as It, RulesyncCommandFrontmatterSchema as J, ALL_TOOL_TARGETS as Jt, parseJsonc as K, writeFileBuffer as Kt, RulesyncSkillFrontmatterSchema as L, readFileContentOrNull as Lt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as M, truncateText as Mn, listDirectoryEntryNames as Mt, getLocalSkillDirNames as N, hasDeceptiveHiddenCharacters as Nn, listFilePathsRecursively as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES as On, getHomeDirectory as Ot, RulesyncSubagent as P, stripControlCharacters as Pn, listSubdirectoryNames as Pt, loadYaml as Q, CURATED_RULES_FEATURE_SUBDIR as Qt, RulesyncRule as R, removeDirectory as Rt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as S, RULESYNC_RELATIVE_DIR_PATH as Sn, checkPathTraversal as St, CODEXCLI_BASH_RULES_FILE_NAME as T, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Tn, ensureDir as Tt, RulesyncHooks as U, resolvePath as Ut, RulesyncMcp as V, removeFileStrict as Vt, getRulesyncSourceCandidates as W, runWithDirectoryRollback as Wt, RulesyncCheckFrontmatterSchema as X, PACKAGING_TOOL_TARGETS as Xt, RulesyncCheck as Y, ALL_TOOL_TARGETS_WITH_WILDCARD as Yt, stringifyFrontmatter as Z, ToolTargetSchema as Zt, QWENCODE_DIR as _, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as _n, CLIError as _t, getProcessorRegistryEntry as a, RULESYNC_CONFIG_SCHEMA_URL as an, ConfigFileSchema as at, CLAUDECODE_LOCAL_RULE_FILE_NAME as b, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as bn, assertTreeContainsNoSymlinks as bt, RulesProcessor as c, RULESYNC_HOOKS_FILE_NAME as cn, findControlCharacter as ct, displayWidthOf as d, RULESYNC_IGNORE_RELATIVE_FILE_PATH as dn, WarningCollectingLogger as dt, RULESYNC_AIIGNORE_FILE_NAME as en, SKILL_FILE_NAME as et, shortenToWidth as f, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as fn, fallbackLogger as ft, CommandsProcessor as g, RULESYNC_MCP_SCHEMA_URL as gn, withWarnOnceScope as gt, HooksProcessor as h, RULESYNC_MCP_RELATIVE_FILE_PATH as hn, resetWarnedOnceMessages as ht, inspectInputRoots as i, RULESYNC_CONFIG_RELATIVE_FILE_PATH as in, CONFLICTING_TARGET_PAIRS as it, AUGMENTCODE_DIR as j, formatError as jn, isSymlink as jt, caseFoldIdentity as k, ALL_FEATURES_WITH_WILDCARD as kn, isFileNotFoundError as kt, SubagentsProcessor as l, RULESYNC_HOOKS_LEGACY_FILE_NAME as ln, ConsoleLogger as lt, IgnoreProcessor as m, RULESYNC_MCP_LEGACY_FILE_NAME as mn, withFallbackLoggerTarget as mt, formatSourceLoadFailure as n, RULESYNC_CHECKS_RELATIVE_DIR_PATH as nn, mergeInputRootConfigs as nt, convertFromTool as o, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as on, GITIGNORE_DESTINATION_KEY as ot, McpProcessor as p, RULESYNC_MCP_FILE_NAME as pn, warnOnConflictingFlags as pt, RulesyncCommand as q, writeFileContent as qt, generate as r, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as rn, resolveEffectiveInputRoots as rt, isPackagingToolTarget as s, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as sn, SourceEntrySchema as st, importFromTool as t, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as tn, ConfigResolver as tt, SkillsProcessor as u, RULESYNC_HOOKS_RELATIVE_FILE_PATH as un, JsonLogger as ut, QWENCODE_LOCAL_RULE_FILE_NAME as v, RULESYNC_PERMISSIONS_FILE_NAME as vn, ErrorCodes as vt, ChecksProcessor as w, RULESYNC_SKILLS_RELATIVE_DIR_PATH as wn, directoryExists as wt, CLAUDECODE_MEMORIES_DIR_NAME as x, RULESYNC_PERMISSIONS_SCHEMA_URL as xn, assertWritablePathInsideRoot as xt, CLAUDECODE_DIR as y, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as yn, assertDirectoryIfExists as yt, RulesyncRuleFrontmatterSchema as z, removeDirectoryStrict as zt };
|
|
63614
64632
|
|
|
63615
|
-
//# sourceMappingURL=import-
|
|
64633
|
+
//# sourceMappingURL=import-DijDR24m.js.map
|