rulesync 16.21.0 → 16.22.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 +2 -0
- package/dist/cli/index.cjs +118 -35
- package/dist/cli/index.js +118 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-5_n-Y8N6.js → import-BKqbq4Ut.js} +2098 -665
- package/dist/import-BKqbq4Ut.js.map +1 -0
- package/dist/{import-7UmDF35I.cjs → import-CUyeYGxZ.cjs} +2131 -680
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +37 -19
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +37 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-5_n-Y8N6.js.map +0 -1
|
@@ -221,6 +221,35 @@ function hasDeceptiveHiddenCharacters(text) {
|
|
|
221
221
|
return !joinsCharacter(characters[index - 1]) || !joinsCharacter(characters[index + 1]);
|
|
222
222
|
});
|
|
223
223
|
}
|
|
224
|
+
/** A mark that draws around the character before it — a circle, a square, a keycap box. */
|
|
225
|
+
const ENCLOSING_MARK_PATTERN = /\p{Me}/u;
|
|
226
|
+
/**
|
|
227
|
+
* Whether `text` carries an enclosing mark that is not the keycap of an emoji
|
|
228
|
+
* keycap sequence.
|
|
229
|
+
*
|
|
230
|
+
* An enclosing mark (`\p{Me}`: U+20DD COMBINING ENCLOSING CIRCLE, U+20E3
|
|
231
|
+
* COMBINING ENCLOSING KEYCAP, the Cyrillic and Vedic ones) is drawn over the
|
|
232
|
+
* character before it and takes no column of its own, so `pdf` with one after
|
|
233
|
+
* it occupies the three columns of `pdf` and is a fourth directory underneath.
|
|
234
|
+
* Unlike a joiner or a variation selector it is not invisible — the box is
|
|
235
|
+
* drawn — which is why `hasDeceptiveHiddenCharacters` does not refuse it and
|
|
236
|
+
* why it is a question for the confusable-name note instead: the row is drawn,
|
|
237
|
+
* only not the way its name reads. The one place an enclosing mark belongs in
|
|
238
|
+
* a name is the keycap sequence of UTS #51, which `isKeycapSequence` matches
|
|
239
|
+
* whole; every other one is left over.
|
|
240
|
+
*
|
|
241
|
+
* Restricted to `\p{Me}` on purpose: a non-spacing mark (`\p{Mn}`) is how
|
|
242
|
+
* Devanagari, Arabic and Vietnamese write, and folding those would mark
|
|
243
|
+
* ordinary names in every one of them.
|
|
244
|
+
*/
|
|
245
|
+
function hasEnclosingMarkOutsideKeycap(text) {
|
|
246
|
+
const characters = [...text];
|
|
247
|
+
return characters.some((character, index) => ENCLOSING_MARK_PATTERN.test(character) && !isKeycapSequence({
|
|
248
|
+
base: characters[index - 2],
|
|
249
|
+
selector: characters[index - 1] ?? "",
|
|
250
|
+
following: character
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
224
253
|
//#endregion
|
|
225
254
|
//#region src/utils/truncate.ts
|
|
226
255
|
/**
|
|
@@ -444,9 +473,11 @@ const rulesProcessorToolTargetTuple = [
|
|
|
444
473
|
"claudecode",
|
|
445
474
|
"claudecode-legacy",
|
|
446
475
|
"cline",
|
|
476
|
+
"codebuddy",
|
|
447
477
|
"codexcli",
|
|
448
478
|
"copilot",
|
|
449
479
|
"copilotcli",
|
|
480
|
+
"crush",
|
|
450
481
|
"cursor",
|
|
451
482
|
"deepagents",
|
|
452
483
|
"factorydroid",
|
|
@@ -482,6 +513,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
482
513
|
"claudecode",
|
|
483
514
|
"claudecode-legacy",
|
|
484
515
|
"cline",
|
|
516
|
+
"crush",
|
|
485
517
|
"cursor",
|
|
486
518
|
"hermesagent",
|
|
487
519
|
"junie",
|
|
@@ -623,6 +655,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
623
655
|
"codexcli",
|
|
624
656
|
"copilot",
|
|
625
657
|
"copilotcli",
|
|
658
|
+
"crush",
|
|
626
659
|
"cursor",
|
|
627
660
|
"deepagents",
|
|
628
661
|
"factorydroid",
|
|
@@ -3327,6 +3360,42 @@ var RulesyncFile = class extends AiFile {
|
|
|
3327
3360
|
}
|
|
3328
3361
|
};
|
|
3329
3362
|
//#endregion
|
|
3363
|
+
//#region src/utils/prototype-pollution.ts
|
|
3364
|
+
/**
|
|
3365
|
+
* Keys that, if walked into when constructing or merging objects from
|
|
3366
|
+
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
3367
|
+
* chain) and propagate state to every other object in the runtime. Any code
|
|
3368
|
+
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
3369
|
+
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
3370
|
+
*/
|
|
3371
|
+
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
3372
|
+
"__proto__",
|
|
3373
|
+
"constructor",
|
|
3374
|
+
"prototype"
|
|
3375
|
+
]);
|
|
3376
|
+
function isPrototypePollutionKey(key) {
|
|
3377
|
+
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Returns a shallow copy of a record's own entries with every
|
|
3381
|
+
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
3382
|
+
*
|
|
3383
|
+
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
3384
|
+
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
3385
|
+
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
3386
|
+
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
3387
|
+
* prototype). Walking the entries through this helper severs that path while
|
|
3388
|
+
* preserving every legitimate key.
|
|
3389
|
+
*/
|
|
3390
|
+
function omitPrototypePollutionKeys(record) {
|
|
3391
|
+
const sanitized = {};
|
|
3392
|
+
for (const [key, value] of Object.entries(record)) {
|
|
3393
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
3394
|
+
sanitized[key] = value;
|
|
3395
|
+
}
|
|
3396
|
+
return sanitized;
|
|
3397
|
+
}
|
|
3398
|
+
//#endregion
|
|
3330
3399
|
//#region src/utils/type-guards.ts
|
|
3331
3400
|
/**
|
|
3332
3401
|
* Type guard to check if a value is a plain object (Record<string, unknown>).
|
|
@@ -3391,67 +3460,192 @@ function loadYaml(content) {
|
|
|
3391
3460
|
}
|
|
3392
3461
|
//#endregion
|
|
3393
3462
|
//#region src/utils/frontmatter.ts
|
|
3394
|
-
|
|
3463
|
+
/**
|
|
3464
|
+
* Upper bound on the number of values a frontmatter document may expand to
|
|
3465
|
+
* once every YAML alias is written out.
|
|
3466
|
+
*
|
|
3467
|
+
* A YAML alias makes one parsed container reachable from many keys, and the
|
|
3468
|
+
* cleaners below copy each reachable value, so a small file with a few levels
|
|
3469
|
+
* of nested aliases (an "alias bomb") can expand into megabytes of output or
|
|
3470
|
+
* exhaust the heap. Counting every visited value against this budget turns
|
|
3471
|
+
* that into an error instead. Real frontmatter is a handful of keys; even a
|
|
3472
|
+
* generous skill manifest stays orders of magnitude below the limit.
|
|
3473
|
+
*/
|
|
3474
|
+
const MAX_FRONTMATTER_VALUES = 1e5;
|
|
3475
|
+
/**
|
|
3476
|
+
* Upper bound on the total character count of string leaves a frontmatter
|
|
3477
|
+
* document may expand to.
|
|
3478
|
+
*
|
|
3479
|
+
* {@link MAX_FRONTMATTER_VALUES} bounds how many values are visited, but a
|
|
3480
|
+
* single long string aliased thousands of times still fits that budget while
|
|
3481
|
+
* the duplicated output balloons: one scalar of a few KB, chained through a
|
|
3482
|
+
* handful of aliases within the value budget, can multiply into a document
|
|
3483
|
+
* many megabytes larger than it started. Charging every visited string's
|
|
3484
|
+
* length against this separate budget bounds that output regardless of how
|
|
3485
|
+
* many aliases point at it.
|
|
3486
|
+
*/
|
|
3487
|
+
const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
3488
|
+
/**
|
|
3489
|
+
* Upper bound on the raw character length of the `---`-delimited frontmatter
|
|
3490
|
+
* block itself, checked before it is ever handed to the YAML parser.
|
|
3491
|
+
*
|
|
3492
|
+
* The budgets above only bound the *parsed* document — the walk over
|
|
3493
|
+
* `matter()`'s output — but a complex YAML key (an array or mapping used as a
|
|
3494
|
+
* mapping key) is joined into a string by js-yaml while it parses, and a
|
|
3495
|
+
* mapping with many such keys can cost real memory before that walk ever
|
|
3496
|
+
* starts, or even before `matter()` returns. Capping the raw block size keeps
|
|
3497
|
+
* that parse-time cost bounded regardless of what the block contains. Real
|
|
3498
|
+
* frontmatter blocks are a few hundred bytes at most; even a large project
|
|
3499
|
+
* manifest stays well under this.
|
|
3500
|
+
*/
|
|
3501
|
+
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3502
|
+
/** Charge string content (a string leaf or an object key) against the character budget. */
|
|
3503
|
+
function chargeStringChars({ options, chars }) {
|
|
3504
|
+
options.budget.stringCharsRemaining -= chars;
|
|
3505
|
+
if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3506
|
+
}
|
|
3507
|
+
function consumeBudget({ options, stringChars = 0 }) {
|
|
3508
|
+
options.budget.remaining -= 1;
|
|
3509
|
+
if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3510
|
+
chargeStringChars({
|
|
3511
|
+
options,
|
|
3512
|
+
chars: stringChars
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
/** Enter one more container level, throwing if the depth cap is exceeded. */
|
|
3516
|
+
function enterContainer({ options, container }) {
|
|
3517
|
+
options.depth += 1;
|
|
3518
|
+
if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3519
|
+
options.ancestors.add(container);
|
|
3520
|
+
}
|
|
3521
|
+
/** Leave a container level entered via {@link enterContainer}. */
|
|
3522
|
+
function leaveContainer({ options, container }) {
|
|
3523
|
+
options.ancestors.delete(container);
|
|
3524
|
+
options.depth -= 1;
|
|
3525
|
+
}
|
|
3526
|
+
/**
|
|
3527
|
+
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3528
|
+
* string leaf is charged by its own length instead).
|
|
3529
|
+
*
|
|
3530
|
+
* js-yaml's default schema resolves `!!binary` scalars to a `Uint8Array`, and
|
|
3531
|
+
* its dumper writes one back out as base64 — roughly 4 output characters per
|
|
3532
|
+
* 3 input bytes. Without this, an aliased binary blob would walk the budget
|
|
3533
|
+
* for free even though it can dominate the emitted document's size.
|
|
3534
|
+
*/
|
|
3535
|
+
function estimateLeafChars(value) {
|
|
3536
|
+
if (value instanceof Uint8Array) return Math.ceil(value.byteLength / 3) * 4;
|
|
3537
|
+
return 0;
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Copy one parsed value, dropping nullish leaves and cyclic references.
|
|
3541
|
+
*
|
|
3542
|
+
* Every alias is still written out as an independent copy, as gray-matter's
|
|
3543
|
+
* default YAML engine would otherwise serialize shared references as `&ref_0`
|
|
3544
|
+
* anchors that simplified frontmatter parsers cannot read; the expansion is
|
|
3545
|
+
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3546
|
+
*/
|
|
3547
|
+
function deepCleanValue(value, options) {
|
|
3548
|
+
consumeBudget({
|
|
3549
|
+
options,
|
|
3550
|
+
stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
|
|
3551
|
+
});
|
|
3395
3552
|
if (value === null || value === void 0) return;
|
|
3396
|
-
if (
|
|
3397
|
-
if (
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3553
|
+
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3554
|
+
if (Array.isArray(value)) {
|
|
3555
|
+
if (options.ancestors.has(value)) return;
|
|
3556
|
+
enterContainer({
|
|
3557
|
+
options,
|
|
3558
|
+
container: value
|
|
3559
|
+
});
|
|
3560
|
+
const cleanedArray = [];
|
|
3561
|
+
for (const item of value) {
|
|
3562
|
+
const cleaned = deepCleanValue(item, options);
|
|
3563
|
+
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3402
3564
|
}
|
|
3565
|
+
leaveContainer({
|
|
3566
|
+
options,
|
|
3567
|
+
container: value
|
|
3568
|
+
});
|
|
3569
|
+
return cleanedArray;
|
|
3570
|
+
}
|
|
3571
|
+
if (isPlainObject$1(value)) {
|
|
3572
|
+
if (options.ancestors.has(value)) return;
|
|
3573
|
+
enterContainer({
|
|
3574
|
+
options,
|
|
3575
|
+
container: value
|
|
3576
|
+
});
|
|
3577
|
+
const result = cleanOwnEntries(value, options);
|
|
3578
|
+
leaveContainer({
|
|
3579
|
+
options,
|
|
3580
|
+
container: value
|
|
3581
|
+
});
|
|
3403
3582
|
return result;
|
|
3404
3583
|
}
|
|
3405
3584
|
return value;
|
|
3406
3585
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3586
|
+
/**
|
|
3587
|
+
* Copy the cleaned own entries of a parsed object into a fresh record.
|
|
3588
|
+
*
|
|
3589
|
+
* A YAML parser defines a `__proto__:` key as an own property, and assigning
|
|
3590
|
+
* it back with bracket notation would instead replace the new record's
|
|
3591
|
+
* prototype, whose members zod's loose object schemas then promote to real
|
|
3592
|
+
* keys. So a fetched skill could hide `allowed-tools` under an innocuous
|
|
3593
|
+
* looking `__proto__:` block. That key, `constructor` and `prototype` are
|
|
3594
|
+
* therefore dropped rather than copied, and cannot be used as frontmatter
|
|
3595
|
+
* keys.
|
|
3596
|
+
*/
|
|
3597
|
+
function cleanOwnEntries(obj, options) {
|
|
3409
3598
|
const result = {};
|
|
3410
3599
|
for (const [key, val] of Object.entries(obj)) {
|
|
3411
|
-
|
|
3600
|
+
chargeStringChars({
|
|
3601
|
+
options,
|
|
3602
|
+
chars: key.length
|
|
3603
|
+
});
|
|
3604
|
+
const cleaned = deepCleanValue(val, options);
|
|
3605
|
+
if (isPrototypePollutionKey(key)) continue;
|
|
3412
3606
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
3413
3607
|
}
|
|
3414
3608
|
return result;
|
|
3415
3609
|
}
|
|
3416
|
-
function
|
|
3417
|
-
if (
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3610
|
+
function deepCleanObject(obj, options) {
|
|
3611
|
+
if (!obj || typeof obj !== "object") return {};
|
|
3612
|
+
return cleanOwnEntries(obj, {
|
|
3613
|
+
...options,
|
|
3614
|
+
ancestors: new WeakSet([obj]),
|
|
3615
|
+
budget: {
|
|
3616
|
+
remaining: MAX_FRONTMATTER_VALUES,
|
|
3617
|
+
stringCharsRemaining: MAX_FRONTMATTER_STRING_CHARS
|
|
3618
|
+
},
|
|
3619
|
+
depth: 1
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
/** Drop null and undefined values, recursively. */
|
|
3623
|
+
function deepRemoveNullishObject(obj) {
|
|
3624
|
+
return deepCleanObject(obj, {});
|
|
3429
3625
|
}
|
|
3626
|
+
/** Drop nullish values and collapse every string onto a single line. */
|
|
3430
3627
|
function deepFlattenStringsObject(obj) {
|
|
3431
|
-
|
|
3432
|
-
const result = {};
|
|
3433
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
3434
|
-
const cleaned = deepFlattenStringsValue(val);
|
|
3435
|
-
if (cleaned !== void 0) result[key] = cleaned;
|
|
3436
|
-
}
|
|
3437
|
-
return result;
|
|
3628
|
+
return deepCleanObject(obj, { transformString: (value) => value.replace(/\n+/g, " ").trim() });
|
|
3438
3629
|
}
|
|
3439
3630
|
function stringifyFrontmatter(body, frontmatter, options) {
|
|
3440
3631
|
const { avoidBlockScalars = false } = options ?? {};
|
|
3441
3632
|
const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
|
|
3442
|
-
|
|
3633
|
+
const file = { content: body };
|
|
3634
|
+
if (avoidBlockScalars) return matter.stringify(file, cleanFrontmatter, { engines: { yaml: {
|
|
3443
3635
|
parse: (input) => loadYaml(input) ?? {},
|
|
3444
3636
|
stringify: (data) => dump(data, { lineWidth: -1 })
|
|
3445
3637
|
} } });
|
|
3446
|
-
return matter.stringify(
|
|
3638
|
+
return matter.stringify(file, cleanFrontmatter);
|
|
3447
3639
|
}
|
|
3448
3640
|
function parseFrontmatter(content, filePath) {
|
|
3449
3641
|
let frontmatter;
|
|
3450
3642
|
let body;
|
|
3451
3643
|
let hasFrontmatter;
|
|
3452
3644
|
try {
|
|
3645
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3646
|
+
if (bounds && bounds.blockEnd - bounds.blockStart > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
|
|
3453
3647
|
const result = matter(content, {});
|
|
3454
|
-
frontmatter = result.data;
|
|
3648
|
+
frontmatter = deepRemoveNullishObject(result.data);
|
|
3455
3649
|
body = result.content;
|
|
3456
3650
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
3457
3651
|
} catch (error) {
|
|
@@ -3459,7 +3653,7 @@ function parseFrontmatter(content, filePath) {
|
|
|
3459
3653
|
throw error;
|
|
3460
3654
|
}
|
|
3461
3655
|
return {
|
|
3462
|
-
frontmatter
|
|
3656
|
+
frontmatter,
|
|
3463
3657
|
body,
|
|
3464
3658
|
hasFrontmatter
|
|
3465
3659
|
};
|
|
@@ -3508,17 +3702,34 @@ function repairFrontmatterLine(line) {
|
|
|
3508
3702
|
};
|
|
3509
3703
|
}
|
|
3510
3704
|
/**
|
|
3511
|
-
*
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3705
|
+
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
3706
|
+
* without parsing it. Shared by the size guard in {@link parseFrontmatter} and
|
|
3707
|
+
* the YAML repair pass below, so both agree on exactly what gray-matter would
|
|
3708
|
+
* treat as the block: gray-matter ends it at the first `\n---`, with no
|
|
3709
|
+
* requirement that the delimiter be alone on its line, so a stricter pattern
|
|
3710
|
+
* here would run past gray-matter's delimiter and act on text that is really
|
|
3711
|
+
* the body.
|
|
3514
3712
|
*/
|
|
3515
|
-
function
|
|
3713
|
+
function findFrontmatterBlockBounds(content) {
|
|
3516
3714
|
const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
|
|
3517
3715
|
if (!opening) return;
|
|
3518
3716
|
const blockStart = opening[0].length;
|
|
3519
3717
|
const closing = /\r?\n---/.exec(content.slice(blockStart));
|
|
3520
3718
|
if (!closing) return;
|
|
3521
|
-
|
|
3719
|
+
return {
|
|
3720
|
+
blockStart,
|
|
3721
|
+
blockEnd: blockStart + closing.index
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Quote the unquoted scalars that make a frontmatter block unparseable, or
|
|
3726
|
+
* return `undefined` when there is nothing to repair. Only the frontmatter
|
|
3727
|
+
* block is rewritten; the body is passed through untouched.
|
|
3728
|
+
*/
|
|
3729
|
+
function repairMalformedFrontmatterYaml(content) {
|
|
3730
|
+
const bounds = findFrontmatterBlockBounds(content);
|
|
3731
|
+
if (!bounds) return;
|
|
3732
|
+
const { blockStart, blockEnd } = bounds;
|
|
3522
3733
|
const block = content.slice(blockStart, blockEnd);
|
|
3523
3734
|
const repairedLines = block.split("\n").map(repairFrontmatterLine);
|
|
3524
3735
|
const repairedBlock = repairedLines.map(({ line }) => line).join("\n");
|
|
@@ -5197,42 +5408,6 @@ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
|
|
|
5197
5408
|
*/
|
|
5198
5409
|
const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
5199
5410
|
//#endregion
|
|
5200
|
-
//#region src/utils/prototype-pollution.ts
|
|
5201
|
-
/**
|
|
5202
|
-
* Keys that, if walked into when constructing or merging objects from
|
|
5203
|
-
* untrusted input, can mutate `Object.prototype` (or otherwise the prototype
|
|
5204
|
-
* chain) and propagate state to every other object in the runtime. Any code
|
|
5205
|
-
* that copies arbitrary user-supplied keys into a fresh object — frontmatter
|
|
5206
|
-
* parsing, MCP config conversion, settings round-trip — should skip these.
|
|
5207
|
-
*/
|
|
5208
|
-
const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
5209
|
-
"__proto__",
|
|
5210
|
-
"constructor",
|
|
5211
|
-
"prototype"
|
|
5212
|
-
]);
|
|
5213
|
-
function isPrototypePollutionKey(key) {
|
|
5214
|
-
return PROTOTYPE_POLLUTION_KEYS.has(key);
|
|
5215
|
-
}
|
|
5216
|
-
/**
|
|
5217
|
-
* Returns a shallow copy of a record's own entries with every
|
|
5218
|
-
* prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
|
|
5219
|
-
*
|
|
5220
|
-
* Use when copying a nested, user-supplied string map — an MCP server's `env`
|
|
5221
|
-
* or `headers` table — into freshly generated config. Carrying such a map by
|
|
5222
|
-
* reference, or re-assigning its keys via bracket notation, would let a literal
|
|
5223
|
-
* `__proto__` key ride along (and re-assigning it would mutate the target's
|
|
5224
|
-
* prototype). Walking the entries through this helper severs that path while
|
|
5225
|
-
* preserving every legitimate key.
|
|
5226
|
-
*/
|
|
5227
|
-
function omitPrototypePollutionKeys(record) {
|
|
5228
|
-
const sanitized = {};
|
|
5229
|
-
for (const [key, value] of Object.entries(record)) {
|
|
5230
|
-
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
5231
|
-
sanitized[key] = value;
|
|
5232
|
-
}
|
|
5233
|
-
return sanitized;
|
|
5234
|
-
}
|
|
5235
|
-
//#endregion
|
|
5236
5411
|
//#region src/utils/jsonc.ts
|
|
5237
5412
|
/**
|
|
5238
5413
|
* Rebuild the parsed value from its own enumerable entries, dropping
|
|
@@ -5983,6 +6158,17 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
5983
6158
|
const fallbackDirPath = overrideDirPath ?? paths.recommended.relativeDirPath;
|
|
5984
6159
|
throw new RulesyncSourceNotFoundError(`No ${join(outputRoot, fallbackDirPath, paths.recommended.relativeFilePath)} found.`);
|
|
5985
6160
|
}
|
|
6161
|
+
/**
|
|
6162
|
+
* Return one server exactly as authored, before `getMcpServers()` strips
|
|
6163
|
+
* rulesync- and tool-specific fields. Keep this lookup here so every target
|
|
6164
|
+
* that re-merges one of those fields shares the same own-property and
|
|
6165
|
+
* prototype-pollution guards.
|
|
6166
|
+
*/
|
|
6167
|
+
getRawMcpServer(name) {
|
|
6168
|
+
if (isPrototypePollutionKey(name)) return void 0;
|
|
6169
|
+
const mcpServers = isRecord$1(this.json) ? this.json.mcpServers : void 0;
|
|
6170
|
+
return isRecord$1(mcpServers) && Object.hasOwn(mcpServers, name) ? mcpServers[name] : void 0;
|
|
6171
|
+
}
|
|
5986
6172
|
getMcpServers() {
|
|
5987
6173
|
const mcpServers = this.json.mcpServers ?? {};
|
|
5988
6174
|
const entries = Object.entries(mcpServers);
|
|
@@ -7609,6 +7795,11 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
7609
7795
|
globs: z.optional(z.array(z.string())),
|
|
7610
7796
|
agentsmd: z.optional(z.looseObject({ subprojectPath: z.optional(z.string()) })),
|
|
7611
7797
|
claudecode: z.optional(z.looseObject({ paths: z.optional(z.array(z.string())) })),
|
|
7798
|
+
codebuddy: z.optional(z.looseObject({
|
|
7799
|
+
paths: z.optional(z.array(z.string())),
|
|
7800
|
+
alwaysApply: z.optional(z.boolean()),
|
|
7801
|
+
description: z.optional(z.string())
|
|
7802
|
+
})),
|
|
7612
7803
|
cursor: z.optional(z.looseObject({
|
|
7613
7804
|
alwaysApply: z.optional(z.boolean()),
|
|
7614
7805
|
description: z.optional(z.string()),
|
|
@@ -7650,7 +7841,8 @@ const RulesyncRuleFrontmatterSchema = z.object({
|
|
|
7650
7841
|
name: z.optional(z.string()),
|
|
7651
7842
|
extends: z.optional(z.string()),
|
|
7652
7843
|
facet: z.optional(z.enum(["policies", "output-contracts"]))
|
|
7653
|
-
}))
|
|
7844
|
+
})),
|
|
7845
|
+
factorydroid: z.optional(z.looseObject({ channel: z.optional(z.enum(["design"])) }))
|
|
7654
7846
|
});
|
|
7655
7847
|
/**
|
|
7656
7848
|
* The `agentsmd.subprojectPath` every consumer should act on, resolved once so
|
|
@@ -8765,15 +8957,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8765
8957
|
})),
|
|
8766
8958
|
opencode: z.optional(z.looseObject({
|
|
8767
8959
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8768
|
-
license: z.optional(z.
|
|
8769
|
-
compatibility: z.optional(z.
|
|
8770
|
-
metadata: z.optional(z.
|
|
8960
|
+
license: z.optional(z.unknown()),
|
|
8961
|
+
compatibility: z.optional(z.unknown()),
|
|
8962
|
+
metadata: z.optional(z.unknown())
|
|
8771
8963
|
})),
|
|
8772
8964
|
kilo: z.optional(z.looseObject({
|
|
8773
8965
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8774
|
-
license: z.optional(z.
|
|
8775
|
-
compatibility: z.optional(z.
|
|
8776
|
-
metadata: z.optional(z.
|
|
8966
|
+
license: z.optional(z.unknown()),
|
|
8967
|
+
compatibility: z.optional(z.unknown()),
|
|
8968
|
+
metadata: z.optional(z.unknown())
|
|
8777
8969
|
})),
|
|
8778
8970
|
kiro: z.optional(z.looseObject({
|
|
8779
8971
|
license: z.optional(z.string()),
|
|
@@ -8782,9 +8974,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8782
8974
|
})),
|
|
8783
8975
|
deepagents: z.optional(z.looseObject({
|
|
8784
8976
|
"allowed-tools": z.optional(z.array(z.string())),
|
|
8785
|
-
license: z.optional(z.
|
|
8786
|
-
compatibility: z.optional(z.
|
|
8787
|
-
metadata: z.optional(z.
|
|
8977
|
+
license: z.optional(z.unknown()),
|
|
8978
|
+
compatibility: z.optional(z.unknown()),
|
|
8979
|
+
metadata: z.optional(z.unknown())
|
|
8788
8980
|
})),
|
|
8789
8981
|
copilot: z.optional(z.looseObject({
|
|
8790
8982
|
license: z.optional(z.string()),
|
|
@@ -8891,6 +9083,13 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
8891
9083
|
takt: z.optional(z.looseObject({
|
|
8892
9084
|
name: z.optional(z.string()),
|
|
8893
9085
|
extends: z.optional(z.string())
|
|
9086
|
+
})),
|
|
9087
|
+
crush: z.optional(z.looseObject({
|
|
9088
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
9089
|
+
"user-invocable": z.optional(z.boolean()),
|
|
9090
|
+
license: z.optional(z.string()),
|
|
9091
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
9092
|
+
metadata: z.optional(z.looseObject({}))
|
|
8894
9093
|
}))
|
|
8895
9094
|
});
|
|
8896
9095
|
/**
|
|
@@ -9116,7 +9315,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
9116
9315
|
*
|
|
9117
9316
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
9118
9317
|
* default that applies to every tool supporting the flag (claudecode, copilot,
|
|
9119
|
-
* copilotcli, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9318
|
+
* copilotcli, crush, cursor, zed, pi, qwencode, grokcli, factorydroid). Each tool's own section may override that
|
|
9120
9319
|
* default with a per-target value. A defined section value (including `false`)
|
|
9121
9320
|
* always wins over the root default.
|
|
9122
9321
|
*
|
|
@@ -9135,7 +9334,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
9135
9334
|
*
|
|
9136
9335
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
9137
9336
|
* that applies to every tool supporting the flag (claudecode, copilot,
|
|
9138
|
-
* copilotcli, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9337
|
+
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid). Each tool's own section may override that default with a
|
|
9139
9338
|
* per-target value. A defined section value (including `false`) always wins
|
|
9140
9339
|
* over the root default.
|
|
9141
9340
|
*
|
|
@@ -9600,8 +9799,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
9600
9799
|
* This only deletes files that are no longer in the rulesync source, not files that will be overwritten.
|
|
9601
9800
|
*/
|
|
9602
9801
|
async removeOrphanAiFiles(existingFiles, generatedFiles) {
|
|
9603
|
-
const generatedPaths = new Set(generatedFiles.map((f) => f.getFilePath()));
|
|
9604
|
-
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(f.getFilePath()));
|
|
9802
|
+
const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
|
|
9803
|
+
const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
|
|
9605
9804
|
for (const aiFile of orphanFiles) {
|
|
9606
9805
|
const filePath = aiFile.getFilePath();
|
|
9607
9806
|
const loggedPath = stripControlCharacters(filePath);
|
|
@@ -10728,6 +10927,15 @@ const FACTORYDROID_COMMANDS_DIR_PATH = join(FACTORYDROID_DIR, "commands");
|
|
|
10728
10927
|
const FACTORYDROID_SKILLS_DIR_PATH = join(FACTORYDROID_DIR, "skills");
|
|
10729
10928
|
const FACTORYDROID_DROIDS_DIR_PATH = join(FACTORYDROID_DIR, "droids");
|
|
10730
10929
|
const FACTORYDROID_RULE_FILE_NAME = "AGENTS.md";
|
|
10930
|
+
/**
|
|
10931
|
+
* Factory Droid's design-guidelines instruction file: "Always-on design-system,
|
|
10932
|
+
* UX, visual, and interaction guidance", loaded separately from `AGENTS.md`'s
|
|
10933
|
+
* coding guidelines. Project scope only — Factory's docs describe root and
|
|
10934
|
+
* nested `DESIGN.md` files like `AGENTS.md`, but document no personal/global
|
|
10935
|
+
* home-directory equivalent.
|
|
10936
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
10937
|
+
*/
|
|
10938
|
+
const FACTORYDROID_DESIGN_FILE_NAME = "DESIGN.md";
|
|
10731
10939
|
const FACTORYDROID_MCP_FILE_NAME = "mcp.json";
|
|
10732
10940
|
const FACTORYDROID_SETTINGS_FILE_NAME = "settings.json";
|
|
10733
10941
|
const FACTORYDROID_HOOKS_FILE_NAME = "hooks.json";
|
|
@@ -15627,6 +15835,10 @@ function toAllowedToolsArray(value) {
|
|
|
15627
15835
|
* The spec types `compatibility` as a free-form string. An object from a legacy
|
|
15628
15836
|
* rulesync input is flattened to `key: value` pairs instead of being emitted as
|
|
15629
15837
|
* a YAML mapping, which conformant clients reject.
|
|
15838
|
+
*
|
|
15839
|
+
* Exported for `CrushSkill`, which requires the same bare-string shape (Crush's
|
|
15840
|
+
* Go struct types `Compatibility` as a plain `string`) and reuses this
|
|
15841
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15630
15842
|
*/
|
|
15631
15843
|
function toCompatibilityString(value) {
|
|
15632
15844
|
if (typeof value === "string") return value;
|
|
@@ -15635,6 +15847,10 @@ function toCompatibilityString(value) {
|
|
|
15635
15847
|
/**
|
|
15636
15848
|
* The spec types `metadata` as "a map from string keys to string values", so
|
|
15637
15849
|
* non-string values (e.g. a YAML number `version: 1`) are stringified.
|
|
15850
|
+
*
|
|
15851
|
+
* Exported for `CrushSkill`, which requires the same `map[string]string`
|
|
15852
|
+
* shape (Crush's Go struct types `Metadata` that way) and reuses this
|
|
15853
|
+
* implementation rather than maintaining a second, divergent copy.
|
|
15638
15854
|
*/
|
|
15639
15855
|
function toStringMetadata(metadata) {
|
|
15640
15856
|
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [key, stringifyValue(value)]));
|
|
@@ -18645,7 +18861,13 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18645
18861
|
if (!matchByBasename || flatOnly && dirname(key) !== ".") return [key];
|
|
18646
18862
|
return [key, basename(key)];
|
|
18647
18863
|
};
|
|
18648
|
-
const
|
|
18864
|
+
const claimedKeys = new ClaimedIdentities();
|
|
18865
|
+
const primarySource = paths.relativeDirPath;
|
|
18866
|
+
const secondarySource = "a secondary source";
|
|
18867
|
+
for (const command of toolCommands) for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18868
|
+
identity: candidate,
|
|
18869
|
+
source: primarySource
|
|
18870
|
+
});
|
|
18649
18871
|
const additionalCommands = await factory.class.loadAdditionalImportFiles({
|
|
18650
18872
|
outputRoot: this.outputRoot,
|
|
18651
18873
|
global: this.global,
|
|
@@ -18653,11 +18875,26 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
18653
18875
|
});
|
|
18654
18876
|
for (const command of additionalCommands) {
|
|
18655
18877
|
const key = command.getRelativeFilePath();
|
|
18656
|
-
|
|
18657
|
-
|
|
18878
|
+
const collision = [...new Set(keysOf(command, true))].map((candidate) => {
|
|
18879
|
+
const claimed = claimedKeys.claim({
|
|
18880
|
+
identity: candidate,
|
|
18881
|
+
source: secondarySource
|
|
18882
|
+
});
|
|
18883
|
+
return claimed === null ? void 0 : {
|
|
18884
|
+
candidate,
|
|
18885
|
+
claimed
|
|
18886
|
+
};
|
|
18887
|
+
}).find((hit) => hit !== void 0);
|
|
18888
|
+
if (collision) {
|
|
18889
|
+
const { candidate, claimed } = collision;
|
|
18890
|
+
if (claimed.spelling === candidate) this.logger.warn(`Duplicate ${this.toolTarget} command "${stripControlCharacters(key)}" from ${secondarySource}; keeping the one already loaded.`);
|
|
18891
|
+
else this.logger.warn(`Case-insensitive ${this.toolTarget} command collision: "${stripControlCharacters(claimed.spelling)}" and "${stripControlCharacters(candidate)}" resolve to the same command file. Keeping "${stripControlCharacters(claimed.spelling)}" from ${claimed.source === secondarySource ? "earlier in the same source" : `the higher-precedence ${claimed.source}`} and ignoring "${stripControlCharacters(key)}" from ${secondarySource}, which is not imported.`);
|
|
18658
18892
|
continue;
|
|
18659
18893
|
}
|
|
18660
|
-
for (const candidate of keysOf(command))
|
|
18894
|
+
for (const candidate of keysOf(command)) claimedKeys.claim({
|
|
18895
|
+
identity: candidate,
|
|
18896
|
+
source: secondarySource
|
|
18897
|
+
});
|
|
18661
18898
|
toolCommands.push(command);
|
|
18662
18899
|
}
|
|
18663
18900
|
}
|
|
@@ -18947,6 +19184,21 @@ var AmpHooks = class AmpHooks extends ToolHooks {
|
|
|
18947
19184
|
}
|
|
18948
19185
|
};
|
|
18949
19186
|
//#endregion
|
|
19187
|
+
//#region src/utils/own-lookup.ts
|
|
19188
|
+
/**
|
|
19189
|
+
* Read a key from a plain string map without walking its prototype chain.
|
|
19190
|
+
*
|
|
19191
|
+
* A bracket read on an object literal resolves inherited members too, so a
|
|
19192
|
+
* user-supplied key such as `toString` or `constructor` "succeeds" with an
|
|
19193
|
+
* `Object.prototype` function instead of falling through to the caller's
|
|
19194
|
+
* `?? fallback`. Hook adapters translate native event names this way from
|
|
19195
|
+
* `Object.entries()` over a config file, so route the read through here to keep
|
|
19196
|
+
* the fallback honest: only a key the map itself defines yields a value.
|
|
19197
|
+
*/
|
|
19198
|
+
function lookupOwn({ record, key }) {
|
|
19199
|
+
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
19200
|
+
}
|
|
19201
|
+
//#endregion
|
|
18950
19202
|
//#region src/utils/object.ts
|
|
18951
19203
|
/**
|
|
18952
19204
|
* Return a shallow copy of `obj` keeping only the entries whose value is
|
|
@@ -19331,7 +19583,10 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
|
|
|
19331
19583
|
const warn = warnOnce(logger);
|
|
19332
19584
|
const result = {};
|
|
19333
19585
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
19334
|
-
const toolEventName =
|
|
19586
|
+
const toolEventName = lookupOwn({
|
|
19587
|
+
record: converterConfig.canonicalToToolEventNames,
|
|
19588
|
+
key: eventName
|
|
19589
|
+
}) ?? eventName;
|
|
19335
19590
|
const byMatcher = groupDefinitionsByMatcher({
|
|
19336
19591
|
definitions,
|
|
19337
19592
|
converterConfig
|
|
@@ -19759,7 +20014,10 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
19759
20014
|
const warn = warnOnce(logger);
|
|
19760
20015
|
const canonical = {};
|
|
19761
20016
|
for (const [toolEventName, matcherEntries] of Object.entries(hooks)) {
|
|
19762
|
-
const eventName =
|
|
20017
|
+
const eventName = lookupOwn({
|
|
20018
|
+
record: converterConfig.toolToCanonicalEventNames,
|
|
20019
|
+
key: toolEventName
|
|
20020
|
+
}) ?? toolEventName;
|
|
19763
20021
|
if (!Array.isArray(matcherEntries)) continue;
|
|
19764
20022
|
const defs = [];
|
|
19765
20023
|
for (const rawEntry of matcherEntries) {
|
|
@@ -19815,7 +20073,10 @@ function flattenAntigravityHooks(parsed) {
|
|
|
19815
20073
|
const flat = {};
|
|
19816
20074
|
const addEvent = (event, entries) => {
|
|
19817
20075
|
if (isPrototypePollutionKey(event) || !Array.isArray(entries)) return;
|
|
19818
|
-
const existing =
|
|
20076
|
+
const existing = lookupOwn({
|
|
20077
|
+
record: flat,
|
|
20078
|
+
key: event
|
|
20079
|
+
});
|
|
19819
20080
|
flat[event] = existing ? [...existing, ...entries] : [...entries];
|
|
19820
20081
|
};
|
|
19821
20082
|
for (const [key, value] of Object.entries(parsed)) if (Array.isArray(value)) addEvent(key, value);
|
|
@@ -20202,6 +20463,16 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20202
20463
|
const paths = AugmentcodeHooks.getSettablePaths({ global });
|
|
20203
20464
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
20204
20465
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
20466
|
+
let existingHooks = {};
|
|
20467
|
+
try {
|
|
20468
|
+
const parsed = JSON.parse(existingContent);
|
|
20469
|
+
const candidate = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed.hooks : void 0;
|
|
20470
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) existingHooks = candidate;
|
|
20471
|
+
} catch {
|
|
20472
|
+
existingHooks = {};
|
|
20473
|
+
}
|
|
20474
|
+
const nativeEventKeys = new Set(Object.values(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES));
|
|
20475
|
+
const preservedHooks = Object.fromEntries(Object.entries(existingHooks).filter(([key]) => !nativeEventKeys.has(key)));
|
|
20205
20476
|
const config = rulesyncHooks.getJson();
|
|
20206
20477
|
const augmentHooks = canonicalToToolHooks({
|
|
20207
20478
|
config,
|
|
@@ -20213,7 +20484,10 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
|
|
|
20213
20484
|
fileKey: sharedConfigFileKey(paths),
|
|
20214
20485
|
feature: "hooks",
|
|
20215
20486
|
existingContent,
|
|
20216
|
-
patch: { hooks:
|
|
20487
|
+
patch: { hooks: {
|
|
20488
|
+
...preservedHooks,
|
|
20489
|
+
...augmentHooks
|
|
20490
|
+
} },
|
|
20217
20491
|
filePath
|
|
20218
20492
|
});
|
|
20219
20493
|
return new AugmentcodeHooks({
|
|
@@ -20971,7 +21245,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
20971
21245
|
};
|
|
20972
21246
|
const copilot = {};
|
|
20973
21247
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
20974
|
-
const copilotEventName =
|
|
21248
|
+
const copilotEventName = lookupOwn({
|
|
21249
|
+
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
21250
|
+
key: eventName
|
|
21251
|
+
}) ?? eventName;
|
|
20975
21252
|
const entries = [];
|
|
20976
21253
|
for (const def of definitions) {
|
|
20977
21254
|
const hookType = def.type ?? "command";
|
|
@@ -21048,7 +21325,10 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
21048
21325
|
if (copilotHooks === null || copilotHooks === void 0 || typeof copilotHooks !== "object") return {};
|
|
21049
21326
|
const canonical = {};
|
|
21050
21327
|
for (const [copilotEventName, hookEntries] of Object.entries(copilotHooks)) {
|
|
21051
|
-
const eventName =
|
|
21328
|
+
const eventName = lookupOwn({
|
|
21329
|
+
record: COPILOT_TO_CANONICAL_EVENT_NAMES,
|
|
21330
|
+
key: copilotEventName
|
|
21331
|
+
}) ?? copilotEventName;
|
|
21052
21332
|
if (!Array.isArray(hookEntries)) continue;
|
|
21053
21333
|
const defs = [];
|
|
21054
21334
|
for (const rawEntry of hookEntries) {
|
|
@@ -21318,7 +21598,10 @@ function canonicalToCopilotCliHooks(config, logger) {
|
|
|
21318
21598
|
};
|
|
21319
21599
|
const out = {};
|
|
21320
21600
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
21321
|
-
const copilotEventName =
|
|
21601
|
+
const copilotEventName = lookupOwn({
|
|
21602
|
+
record: CANONICAL_TO_COPILOTCLI_EVENT_NAMES,
|
|
21603
|
+
key: eventName
|
|
21604
|
+
}) ?? eventName;
|
|
21322
21605
|
const entries = buildCopilotCliEntriesForEvent({
|
|
21323
21606
|
eventName,
|
|
21324
21607
|
definitions,
|
|
@@ -21375,7 +21658,10 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
|
|
|
21375
21658
|
if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
|
|
21376
21659
|
const canonical = {};
|
|
21377
21660
|
for (const [copilotEventName, hookEntries] of Object.entries(rawHooks)) {
|
|
21378
|
-
const eventName =
|
|
21661
|
+
const eventName = lookupOwn({
|
|
21662
|
+
record: COPILOTCLI_TO_CANONICAL_EVENT_NAMES,
|
|
21663
|
+
key: copilotEventName
|
|
21664
|
+
}) ?? copilotEventName;
|
|
21379
21665
|
if (!Array.isArray(hookEntries)) continue;
|
|
21380
21666
|
const defs = [];
|
|
21381
21667
|
for (const rawEntry of hookEntries) {
|
|
@@ -21527,7 +21813,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21527
21813
|
const mappedHooks = {};
|
|
21528
21814
|
const cursorSupportedTypes = /* @__PURE__ */ new Set(["command", "prompt"]);
|
|
21529
21815
|
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
21530
|
-
const cursorEventName =
|
|
21816
|
+
const cursorEventName = lookupOwn({
|
|
21817
|
+
record: CANONICAL_TO_CURSOR_EVENT_NAMES,
|
|
21818
|
+
key: eventName
|
|
21819
|
+
}) ?? eventName;
|
|
21531
21820
|
const mappedDefs = defs.filter((def) => cursorSupportedTypes.has(def.type ?? "command")).map((def) => ({
|
|
21532
21821
|
...def.type !== void 0 && def.type !== null && { type: def.type },
|
|
21533
21822
|
...def.command !== void 0 && def.command !== null && { command: def.command },
|
|
@@ -21560,7 +21849,10 @@ var CursorHooks = class CursorHooks extends ToolHooks {
|
|
|
21560
21849
|
const cursorHooks = parsed.hooks ?? {};
|
|
21561
21850
|
const canonicalHooks = {};
|
|
21562
21851
|
for (const [cursorEventName, defs] of Object.entries(cursorHooks)) {
|
|
21563
|
-
const eventName =
|
|
21852
|
+
const eventName = lookupOwn({
|
|
21853
|
+
record: CURSOR_TO_CANONICAL_EVENT_NAMES,
|
|
21854
|
+
key: cursorEventName
|
|
21855
|
+
}) ?? cursorEventName;
|
|
21564
21856
|
canonicalHooks[eventName] = defs;
|
|
21565
21857
|
}
|
|
21566
21858
|
const version = parsed.version ?? 1;
|
|
@@ -21630,7 +21922,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21630
21922
|
const hooks = {};
|
|
21631
21923
|
for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
|
|
21632
21924
|
if (!supported.has(canonicalEvent)) continue;
|
|
21633
|
-
const deepagentsEvent =
|
|
21925
|
+
const deepagentsEvent = lookupOwn({
|
|
21926
|
+
record: CANONICAL_TO_DEEPAGENTS_EVENT_NAMES,
|
|
21927
|
+
key: canonicalEvent
|
|
21928
|
+
});
|
|
21634
21929
|
if (!deepagentsEvent) continue;
|
|
21635
21930
|
for (const def of definitions) {
|
|
21636
21931
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -21659,7 +21954,10 @@ function canonicalToDeepagentsHooks(config) {
|
|
|
21659
21954
|
function deepagentsToCanonicalHooks(hooks) {
|
|
21660
21955
|
const canonical = {};
|
|
21661
21956
|
for (const [deepagentsEvent, groups] of Object.entries(hooks)) {
|
|
21662
|
-
const canonicalEvent =
|
|
21957
|
+
const canonicalEvent = lookupOwn({
|
|
21958
|
+
record: DEEPAGENTS_TO_CANONICAL_EVENT_NAMES,
|
|
21959
|
+
key: deepagentsEvent
|
|
21960
|
+
});
|
|
21663
21961
|
if (!canonicalEvent || !Array.isArray(groups)) continue;
|
|
21664
21962
|
for (const group of groups) {
|
|
21665
21963
|
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
@@ -21692,7 +21990,10 @@ function deepagentsLegacyToCanonicalHooks(entries) {
|
|
|
21692
21990
|
const command = argv.length === 3 && argv[0] === "bash" && argv[1] === "-c" ? String(argv[2] ?? "") : argv.join(" ");
|
|
21693
21991
|
const events = Array.isArray(entry.events) ? entry.events : [];
|
|
21694
21992
|
for (const legacyEvent of events) {
|
|
21695
|
-
const canonicalEvent = typeof legacyEvent === "string" ?
|
|
21993
|
+
const canonicalEvent = typeof legacyEvent === "string" ? lookupOwn({
|
|
21994
|
+
record: DEEPAGENTS_LEGACY_TO_CANONICAL_EVENT_NAMES,
|
|
21995
|
+
key: legacyEvent
|
|
21996
|
+
}) : void 0;
|
|
21696
21997
|
if (!canonicalEvent) continue;
|
|
21697
21998
|
(canonical[canonicalEvent] ??= []).push({
|
|
21698
21999
|
type: "command",
|
|
@@ -22413,7 +22714,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22413
22714
|
const result = {};
|
|
22414
22715
|
for (const [canonicalEvent, definitions] of Object.entries(config.hooks)) {
|
|
22415
22716
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22416
|
-
const nativeEvent =
|
|
22717
|
+
const nativeEvent = lookupOwn({
|
|
22718
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22719
|
+
key: canonicalEvent
|
|
22720
|
+
});
|
|
22417
22721
|
if (nativeEvent) setHermesHookEntries({
|
|
22418
22722
|
result,
|
|
22419
22723
|
event: nativeEvent,
|
|
@@ -22424,7 +22728,10 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
22424
22728
|
}
|
|
22425
22729
|
for (const [canonicalEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
22426
22730
|
if (!HERMESAGENT_CANONICAL_EVENTS.has(canonicalEvent)) continue;
|
|
22427
|
-
const nativeEvent =
|
|
22731
|
+
const nativeEvent = lookupOwn({
|
|
22732
|
+
record: CANONICAL_TO_HERMESAGENT_EVENT_NAMES,
|
|
22733
|
+
key: canonicalEvent
|
|
22734
|
+
});
|
|
22428
22735
|
if (nativeEvent) setHermesHookEntries({
|
|
22429
22736
|
result,
|
|
22430
22737
|
event: nativeEvent,
|
|
@@ -22490,7 +22797,10 @@ function hermesHooksToCanonical(hooks) {
|
|
|
22490
22797
|
for (const [nativeEvent, entries] of Object.entries(hooks)) {
|
|
22491
22798
|
if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
|
|
22492
22799
|
if (!isHermesHookEventEntry(nativeEvent, entries)) continue;
|
|
22493
|
-
const rulesyncEvent =
|
|
22800
|
+
const rulesyncEvent = lookupOwn({
|
|
22801
|
+
record: HERMESAGENT_TO_CANONICAL_EVENT_NAMES,
|
|
22802
|
+
key: nativeEvent
|
|
22803
|
+
}) ?? nativeEvent;
|
|
22494
22804
|
const defs = entries.map((raw) => hermesEntryToDefinition({
|
|
22495
22805
|
nativeEvent,
|
|
22496
22806
|
raw
|
|
@@ -23127,7 +23437,10 @@ function canonicalToKimiCodeHooks({ config, toolOverrideHooks, trustedDirectory,
|
|
|
23127
23437
|
const result = [];
|
|
23128
23438
|
const nativeEvents = new Set(KIMI_CODE_NATIVE_HOOK_EVENTS);
|
|
23129
23439
|
for (const [event, definitions] of Object.entries(buildEffectiveHooks(config, toolOverrideHooks))) {
|
|
23130
|
-
const nativeEvent =
|
|
23440
|
+
const nativeEvent = lookupOwn({
|
|
23441
|
+
record: CANONICAL_TO_KIMI_CODE_EVENT_NAMES,
|
|
23442
|
+
key: event
|
|
23443
|
+
}) ?? event;
|
|
23131
23444
|
if (!nativeEvents.has(nativeEvent)) {
|
|
23132
23445
|
logger?.warn(`Kimi Code hooks: skipping unsupported event "${event}".`);
|
|
23133
23446
|
continue;
|
|
@@ -23161,14 +23474,22 @@ function kimiCodeHooksToCanonical(hooks) {
|
|
|
23161
23474
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
23162
23475
|
const entry = raw;
|
|
23163
23476
|
if (typeof entry.event !== "string" || typeof entry.command !== "string") continue;
|
|
23164
|
-
const event =
|
|
23477
|
+
const event = lookupOwn({
|
|
23478
|
+
record: KIMI_CODE_TO_CANONICAL_EVENT_NAMES,
|
|
23479
|
+
key: entry.event
|
|
23480
|
+
}) ?? entry.event;
|
|
23165
23481
|
const definition = {
|
|
23166
23482
|
type: "command",
|
|
23167
23483
|
command: stripTrustedDirectoryWrapper(entry.command),
|
|
23168
23484
|
...typeof entry.matcher === "string" && { matcher: entry.matcher },
|
|
23169
23485
|
...typeof entry.timeout === "number" && { timeout: entry.timeout }
|
|
23170
23486
|
};
|
|
23171
|
-
|
|
23487
|
+
const list = lookupOwn({
|
|
23488
|
+
record: result,
|
|
23489
|
+
key: event
|
|
23490
|
+
}) ?? [];
|
|
23491
|
+
list.push(definition);
|
|
23492
|
+
result[event] = list;
|
|
23172
23493
|
}
|
|
23173
23494
|
return result;
|
|
23174
23495
|
}
|
|
@@ -23371,7 +23692,13 @@ function canonicalToKiroIdeHooks(config) {
|
|
|
23371
23692
|
};
|
|
23372
23693
|
const entries = [];
|
|
23373
23694
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23374
|
-
const trigger =
|
|
23695
|
+
const trigger = lookupOwn({
|
|
23696
|
+
record: CANONICAL_TO_KIRO_IDE_EVENT_NAMES,
|
|
23697
|
+
key: eventName
|
|
23698
|
+
}) ?? lookupOwn({
|
|
23699
|
+
record: KIRO_LEGACY_TO_KIRO_IDE_TRIGGER_NAMES,
|
|
23700
|
+
key: eventName
|
|
23701
|
+
}) ?? eventName;
|
|
23375
23702
|
entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
|
|
23376
23703
|
}
|
|
23377
23704
|
return entries;
|
|
@@ -23380,7 +23707,10 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23380
23707
|
const canonical = {};
|
|
23381
23708
|
for (const entry of entries) {
|
|
23382
23709
|
if (entry.trigger === void 0 || entry.action === void 0) continue;
|
|
23383
|
-
const eventName =
|
|
23710
|
+
const eventName = lookupOwn({
|
|
23711
|
+
record: KIRO_IDE_TO_CANONICAL_EVENT_NAMES,
|
|
23712
|
+
key: entry.trigger
|
|
23713
|
+
}) ?? entry.trigger;
|
|
23384
23714
|
if (isPrototypePollutionKey(eventName)) continue;
|
|
23385
23715
|
const def = {};
|
|
23386
23716
|
if (entry.action.type === "command") {
|
|
@@ -23397,7 +23727,12 @@ function kiroIdeHooksToCanonical(entries) {
|
|
|
23397
23727
|
if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
|
|
23398
23728
|
if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
|
|
23399
23729
|
if (entry.enabled === false) def.enabled = false;
|
|
23400
|
-
|
|
23730
|
+
const list = lookupOwn({
|
|
23731
|
+
record: canonical,
|
|
23732
|
+
key: eventName
|
|
23733
|
+
}) ?? [];
|
|
23734
|
+
list.push(def);
|
|
23735
|
+
canonical[eventName] = list;
|
|
23401
23736
|
}
|
|
23402
23737
|
return canonical;
|
|
23403
23738
|
}
|
|
@@ -23578,7 +23913,10 @@ function canonicalToKiroHooks({ config, logger }) {
|
|
|
23578
23913
|
};
|
|
23579
23914
|
const kiro = {};
|
|
23580
23915
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
23581
|
-
const kiroEventName =
|
|
23916
|
+
const kiroEventName = lookupOwn({
|
|
23917
|
+
record: CANONICAL_TO_KIRO_EVENT_NAMES,
|
|
23918
|
+
key: eventName
|
|
23919
|
+
}) ?? eventName;
|
|
23582
23920
|
const entries = buildKiroEntriesForEvent(definitions);
|
|
23583
23921
|
if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
|
|
23584
23922
|
else kiro[kiroEventName] = entries;
|
|
@@ -23609,7 +23947,10 @@ function kiroHooksToCanonical(kiroHooks) {
|
|
|
23609
23947
|
if (kiroHooks === null || kiroHooks === void 0 || typeof kiroHooks !== "object") return {};
|
|
23610
23948
|
const canonical = {};
|
|
23611
23949
|
for (const [kiroEventName, entries] of Object.entries(kiroHooks)) {
|
|
23612
|
-
const eventName =
|
|
23950
|
+
const eventName = lookupOwn({
|
|
23951
|
+
record: KIRO_TO_CANONICAL_EVENT_NAMES,
|
|
23952
|
+
key: kiroEventName
|
|
23953
|
+
}) ?? kiroEventName;
|
|
23613
23954
|
if (!Array.isArray(entries)) continue;
|
|
23614
23955
|
const defs = [];
|
|
23615
23956
|
for (const rawEntry of entries) {
|
|
@@ -24168,7 +24509,10 @@ function canonicalToQwencodeHooks(config, logger) {
|
|
|
24168
24509
|
]);
|
|
24169
24510
|
const qwencode = {};
|
|
24170
24511
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
24171
|
-
const qwencodeEventName =
|
|
24512
|
+
const qwencodeEventName = lookupOwn({
|
|
24513
|
+
record: CANONICAL_TO_QWENCODE_EVENT_NAMES,
|
|
24514
|
+
key: eventName
|
|
24515
|
+
}) ?? eventName;
|
|
24172
24516
|
const byMatcher = /* @__PURE__ */ new Map();
|
|
24173
24517
|
for (const def of definitions) {
|
|
24174
24518
|
if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
|
|
@@ -24273,7 +24617,10 @@ function qwencodeHooksToCanonical(qwencodeHooks) {
|
|
|
24273
24617
|
if (qwencodeHooks === null || qwencodeHooks === void 0 || typeof qwencodeHooks !== "object") return {};
|
|
24274
24618
|
const canonical = {};
|
|
24275
24619
|
for (const [qwencodeEventName, matcherEntries] of Object.entries(qwencodeHooks)) {
|
|
24276
|
-
const eventName =
|
|
24620
|
+
const eventName = lookupOwn({
|
|
24621
|
+
record: QWENCODE_TO_CANONICAL_EVENT_NAMES,
|
|
24622
|
+
key: qwencodeEventName
|
|
24623
|
+
}) ?? qwencodeEventName;
|
|
24277
24624
|
if (!Array.isArray(matcherEntries)) continue;
|
|
24278
24625
|
const defs = [];
|
|
24279
24626
|
for (const rawEntry of matcherEntries) {
|
|
@@ -24388,7 +24735,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24388
24735
|
const result = {};
|
|
24389
24736
|
for (const [event, defs] of Object.entries(effectiveHooks)) {
|
|
24390
24737
|
if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
|
|
24391
|
-
const reasonixEvent =
|
|
24738
|
+
const reasonixEvent = lookupOwn({
|
|
24739
|
+
record: CANONICAL_TO_REASONIX_EVENT_NAMES,
|
|
24740
|
+
key: event
|
|
24741
|
+
}) ?? event;
|
|
24392
24742
|
const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
|
|
24393
24743
|
const entries = [];
|
|
24394
24744
|
for (const def of defs) {
|
|
@@ -24401,7 +24751,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
|
|
|
24401
24751
|
if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
|
|
24402
24752
|
entries.push(entry);
|
|
24403
24753
|
}
|
|
24404
|
-
if (entries.length > 0) result[reasonixEvent] = [...
|
|
24754
|
+
if (entries.length > 0) result[reasonixEvent] = [...lookupOwn({
|
|
24755
|
+
record: result,
|
|
24756
|
+
key: reasonixEvent
|
|
24757
|
+
}) ?? [], ...entries];
|
|
24405
24758
|
}
|
|
24406
24759
|
return result;
|
|
24407
24760
|
}
|
|
@@ -24414,7 +24767,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24414
24767
|
if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
|
|
24415
24768
|
for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
|
|
24416
24769
|
if (!Array.isArray(rawEntries)) continue;
|
|
24417
|
-
const canonicalEvent =
|
|
24770
|
+
const canonicalEvent = lookupOwn({
|
|
24771
|
+
record: REASONIX_TO_CANONICAL_EVENT_NAMES,
|
|
24772
|
+
key: reasonixEvent
|
|
24773
|
+
}) ?? reasonixEvent;
|
|
24418
24774
|
const defs = [];
|
|
24419
24775
|
for (const rawEntry of rawEntries) {
|
|
24420
24776
|
if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
|
|
@@ -24429,7 +24785,10 @@ function reasonixHooksToCanonical(hooks) {
|
|
|
24429
24785
|
if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
|
|
24430
24786
|
defs.push(def);
|
|
24431
24787
|
}
|
|
24432
|
-
if (defs.length > 0) canonical[canonicalEvent] = [...
|
|
24788
|
+
if (defs.length > 0) canonical[canonicalEvent] = [...lookupOwn({
|
|
24789
|
+
record: canonical,
|
|
24790
|
+
key: canonicalEvent
|
|
24791
|
+
}) ?? [], ...defs];
|
|
24433
24792
|
}
|
|
24434
24793
|
return canonical;
|
|
24435
24794
|
}
|
|
@@ -24557,7 +24916,10 @@ function canonicalToVibeHooks(config, toolOverride) {
|
|
|
24557
24916
|
const hooks = [];
|
|
24558
24917
|
for (const [event, defs] of Object.entries(effective)) {
|
|
24559
24918
|
if (!SUPPORTED_VIBE_EVENTS.has(event)) continue;
|
|
24560
|
-
const vibeEvent =
|
|
24919
|
+
const vibeEvent = lookupOwn({
|
|
24920
|
+
record: CANONICAL_TO_VIBE_EVENT_NAMES,
|
|
24921
|
+
key: event
|
|
24922
|
+
}) ?? event;
|
|
24561
24923
|
let index = 0;
|
|
24562
24924
|
for (const def of defs) {
|
|
24563
24925
|
if ((def.type ?? "command") !== "command") continue;
|
|
@@ -24587,7 +24949,10 @@ function vibeEntryToCanonicalDef(raw) {
|
|
|
24587
24949
|
const vibeEvent = typeof entry.type === "string" ? entry.type : void 0;
|
|
24588
24950
|
if (vibeEvent === void 0) return null;
|
|
24589
24951
|
if (isPrototypePollutionKey(vibeEvent)) return null;
|
|
24590
|
-
const canonicalEvent =
|
|
24952
|
+
const canonicalEvent = lookupOwn({
|
|
24953
|
+
record: VIBE_TO_CANONICAL_EVENT_NAMES,
|
|
24954
|
+
key: vibeEvent
|
|
24955
|
+
}) ?? vibeEvent;
|
|
24591
24956
|
const def = { type: "command" };
|
|
24592
24957
|
if (typeof entry.command === "string") def.command = entry.command;
|
|
24593
24958
|
if (typeof entry.match === "string" && entry.match !== "" && entry.match !== "*") def.matcher = entry.match;
|
|
@@ -24612,7 +24977,10 @@ function vibeHooksToCanonical(parsed) {
|
|
|
24612
24977
|
for (const raw of rawHooks) {
|
|
24613
24978
|
const result = vibeEntryToCanonicalDef(raw);
|
|
24614
24979
|
if (result === null) continue;
|
|
24615
|
-
const list =
|
|
24980
|
+
const list = lookupOwn({
|
|
24981
|
+
record: canonical,
|
|
24982
|
+
key: result.canonicalEvent
|
|
24983
|
+
}) ?? [];
|
|
24616
24984
|
list.push(result.def);
|
|
24617
24985
|
canonical[result.canonicalEvent] = list;
|
|
24618
24986
|
}
|
|
@@ -25691,6 +26059,68 @@ var ClineIgnore = class ClineIgnore extends ToolIgnore {
|
|
|
25691
26059
|
}
|
|
25692
26060
|
};
|
|
25693
26061
|
//#endregion
|
|
26062
|
+
//#region src/constants/crush-paths.ts
|
|
26063
|
+
const CRUSH_RULE_FILE_NAME = "CRUSH.md";
|
|
26064
|
+
const CRUSH_GLOBAL_DIR = join(".config", "crush");
|
|
26065
|
+
const CRUSH_IGNORE_FILE_NAME = ".crushignore";
|
|
26066
|
+
const CRUSH_SKILLS_PROJECT_DIR = join(".crush", "skills");
|
|
26067
|
+
const CRUSH_SKILLS_GLOBAL_DIR = join(CRUSH_GLOBAL_DIR, "skills");
|
|
26068
|
+
//#endregion
|
|
26069
|
+
//#region src/features/ignore/crush-ignore.ts
|
|
26070
|
+
/**
|
|
26071
|
+
* Ignore generator for Crush.
|
|
26072
|
+
*
|
|
26073
|
+
* Crush excludes files from tool access via a `.crushignore` file, read
|
|
26074
|
+
* hierarchically (root and any subdirectory, the same way it walks
|
|
26075
|
+
* `.gitignore`) using gitignore syntax. Crush documents no global/user-scope
|
|
26076
|
+
* ignore file, so this is project-only.
|
|
26077
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/fsext/fileutil.go
|
|
26078
|
+
*/
|
|
26079
|
+
var CrushIgnore = class CrushIgnore extends ToolIgnore {
|
|
26080
|
+
static getSettablePaths() {
|
|
26081
|
+
return {
|
|
26082
|
+
relativeDirPath: ".",
|
|
26083
|
+
relativeFilePath: CRUSH_IGNORE_FILE_NAME
|
|
26084
|
+
};
|
|
26085
|
+
}
|
|
26086
|
+
toRulesyncIgnore() {
|
|
26087
|
+
return new RulesyncIgnore({
|
|
26088
|
+
outputRoot: ".",
|
|
26089
|
+
relativeDirPath: ".",
|
|
26090
|
+
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
26091
|
+
fileContent: this.fileContent
|
|
26092
|
+
});
|
|
26093
|
+
}
|
|
26094
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
26095
|
+
const body = rulesyncIgnore.getFileContent();
|
|
26096
|
+
return new CrushIgnore({
|
|
26097
|
+
outputRoot,
|
|
26098
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26099
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26100
|
+
fileContent: body
|
|
26101
|
+
});
|
|
26102
|
+
}
|
|
26103
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
26104
|
+
const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
26105
|
+
return new CrushIgnore({
|
|
26106
|
+
outputRoot,
|
|
26107
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
26108
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
26109
|
+
fileContent,
|
|
26110
|
+
validate
|
|
26111
|
+
});
|
|
26112
|
+
}
|
|
26113
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
26114
|
+
return new CrushIgnore({
|
|
26115
|
+
outputRoot,
|
|
26116
|
+
relativeDirPath,
|
|
26117
|
+
relativeFilePath,
|
|
26118
|
+
fileContent: "",
|
|
26119
|
+
validate: false
|
|
26120
|
+
});
|
|
26121
|
+
}
|
|
26122
|
+
};
|
|
26123
|
+
//#endregion
|
|
25694
26124
|
//#region src/features/ignore/cursor-ignore.ts
|
|
25695
26125
|
/**
|
|
25696
26126
|
* Cursor ignore adapter.
|
|
@@ -26687,6 +27117,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
26687
27117
|
["claudecode", { class: ClaudecodeIgnore }],
|
|
26688
27118
|
["claudecode-legacy", { class: ClaudecodeIgnore }],
|
|
26689
27119
|
["cline", { class: ClineIgnore }],
|
|
27120
|
+
["crush", { class: CrushIgnore }],
|
|
26690
27121
|
["cursor", { class: CursorIgnore }],
|
|
26691
27122
|
["hermesagent", { class: HermesagentIgnore }],
|
|
26692
27123
|
["junie", { class: JunieIgnore }],
|
|
@@ -27991,9 +28422,8 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
|
|
|
27991
28422
|
throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
|
|
27992
28423
|
}
|
|
27993
28424
|
const strippedMcpServers = rulesyncMcp.getMcpServers();
|
|
27994
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
27995
28425
|
const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
|
|
27996
|
-
const rawServer =
|
|
28426
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
27997
28427
|
return [serverName, {
|
|
27998
28428
|
...serverConfig,
|
|
27999
28429
|
...isRecord$1(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
|
|
@@ -30956,9 +31386,8 @@ var MusecodeMcp = class MusecodeMcp extends ToolMcp {
|
|
|
30956
31386
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
30957
31387
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
30958
31388
|
const existing = parseMusecodeSettings(existingContent, filePath);
|
|
30959
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
30960
31389
|
const converted = convertToMusecodeFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
30961
|
-
const rawServer =
|
|
31390
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
30962
31391
|
const mode = asMusecodeMode(isRecord$1(rawServer) ? rawServer.musecodeMode : void 0);
|
|
30963
31392
|
return [serverName, {
|
|
30964
31393
|
...serverConfig,
|
|
@@ -31907,6 +32336,66 @@ async function readRovodevConfigYaml({ outputRoot }) {
|
|
|
31907
32336
|
filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
|
|
31908
32337
|
});
|
|
31909
32338
|
}
|
|
32339
|
+
/**
|
|
32340
|
+
* Decide the absolute path a configured `mcpConfigPath` would name in the
|
|
32341
|
+
* given scope, without yet checking whether that path stays inside it. Split
|
|
32342
|
+
* out of resolveRovodevMcpImportPath so each function's branching stays
|
|
32343
|
+
* within the project's complexity budget.
|
|
32344
|
+
*/
|
|
32345
|
+
function resolveMcpConfigCandidatePath({ outputRoot, global, normalizedPath, configuredPath }) {
|
|
32346
|
+
if (global && normalizedPath.startsWith("~/")) return { path: resolve(outputRoot, normalizedPath.slice(2)) };
|
|
32347
|
+
if (!global && normalizedPath.startsWith("~/")) return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in project scope. A home-anchored path cannot be imported as part of a project, so importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32348
|
+
if (isAbsolute(normalizedPath)) return { path: resolve(normalizedPath) };
|
|
32349
|
+
if (!global) return { path: resolve(outputRoot, normalizedPath) };
|
|
32350
|
+
return { rejectionMessage: `Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)} in global scope. Only home-anchored or absolute paths can be imported safely, so importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.` };
|
|
32351
|
+
}
|
|
32352
|
+
/**
|
|
32353
|
+
* Resolve the active Rovo Dev MCP config without following a pointer outside
|
|
32354
|
+
* the import scope. The implementation is deliberately separate from
|
|
32355
|
+
* fromFile: it keeps path-policy decisions testable without changing the
|
|
32356
|
+
* public ToolMcp contract.
|
|
32357
|
+
*/
|
|
32358
|
+
async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger }) {
|
|
32359
|
+
const fallback = {
|
|
32360
|
+
filePath: join(outputRoot, ROVODEV_DIR, ROVODEV_MCP_FILE_NAME),
|
|
32361
|
+
relativeDirPath: ROVODEV_DIR,
|
|
32362
|
+
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32363
|
+
};
|
|
32364
|
+
const configuredPath = (config && isRecord$1(config.mcp) ? config.mcp : {}).mcpConfigPath;
|
|
32365
|
+
if (configuredPath === void 0) {
|
|
32366
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is unset in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)}. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)}, which may not be the file Rovo Dev reads.`);
|
|
32367
|
+
return fallback;
|
|
32368
|
+
}
|
|
32369
|
+
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
32370
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)} must be a non-empty string. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32371
|
+
return fallback;
|
|
32372
|
+
}
|
|
32373
|
+
const normalizedPath = normalizeMcpConfigPathValue(configuredPath.trim());
|
|
32374
|
+
const candidateResult = resolveMcpConfigCandidatePath({
|
|
32375
|
+
outputRoot,
|
|
32376
|
+
global,
|
|
32377
|
+
normalizedPath,
|
|
32378
|
+
configuredPath
|
|
32379
|
+
});
|
|
32380
|
+
if ("rejectionMessage" in candidateResult) {
|
|
32381
|
+
logger?.warn(candidateResult.rejectionMessage);
|
|
32382
|
+
return fallback;
|
|
32383
|
+
}
|
|
32384
|
+
const candidatePath = candidateResult.path;
|
|
32385
|
+
const relativePath = relative(resolve(outputRoot), candidatePath);
|
|
32386
|
+
if (relativePath === "" || splitPathSegments(normalizedPath).includes("..") || pathEscapesRoot(relativePath) || await resolvedPathEscapesRoot({
|
|
32387
|
+
rootPath: outputRoot,
|
|
32388
|
+
targetPath: candidatePath
|
|
32389
|
+
})) {
|
|
32390
|
+
logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath is ${quoteValueForWarning(configuredPath)}, which is outside the import scope or traverses a symbolic link. Importing ${join(ROVODEV_DIR, ROVODEV_MCP_FILE_NAME)} instead.`);
|
|
32391
|
+
return fallback;
|
|
32392
|
+
}
|
|
32393
|
+
return {
|
|
32394
|
+
filePath: candidatePath,
|
|
32395
|
+
relativeDirPath: dirname(relativePath),
|
|
32396
|
+
relativeFilePath: basename(relativePath)
|
|
32397
|
+
};
|
|
32398
|
+
}
|
|
31910
32399
|
function disabledNamesOf(config) {
|
|
31911
32400
|
const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
|
|
31912
32401
|
return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
|
|
@@ -32012,6 +32501,25 @@ function envVarMcpFileSpellings({ fileName }) {
|
|
|
32012
32501
|
return [`$HOME/${tail}`, `\${HOME}/${tail}`];
|
|
32013
32502
|
}
|
|
32014
32503
|
/**
|
|
32504
|
+
* Classify the existing `mcpConfigPath` without deciding how to report it.
|
|
32505
|
+
* Keep the known-file checks in this order: the generated file is valid in
|
|
32506
|
+
* either scope, while the documented default and environment-variable
|
|
32507
|
+
* spellings are global-only alternatives that need their own warnings.
|
|
32508
|
+
*/
|
|
32509
|
+
function classifyExistingPointer({ existing, global, outputRoot }) {
|
|
32510
|
+
if (existing === void 0) return { kind: "unset" };
|
|
32511
|
+
const normalized = typeof existing === "string" ? normalizeMcpConfigPathValue(existing) : void 0;
|
|
32512
|
+
const namesFile = (fileName) => normalized !== void 0 && mcpFileSpellings({
|
|
32513
|
+
fileName,
|
|
32514
|
+
global,
|
|
32515
|
+
outputRoot
|
|
32516
|
+
}).includes(normalized);
|
|
32517
|
+
if (namesFile("mcp.json")) return { kind: "already-generated" };
|
|
32518
|
+
if (global && namesFile(ROVODEV_ALTERNATE_MCP_FILE_NAME)) return { kind: "documented-default" };
|
|
32519
|
+
if (global && normalized !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalized)) return { kind: "env-var-spelling" };
|
|
32520
|
+
return { kind: "unrelated" };
|
|
32521
|
+
}
|
|
32522
|
+
/**
|
|
32015
32523
|
* Point `mcp.mcpConfigPath` at the `mcp.json` rulesync writes for this scope,
|
|
32016
32524
|
* and report whether the block gained a value it did not already carry.
|
|
32017
32525
|
*
|
|
@@ -32122,45 +32630,44 @@ function announcePointer({ global, logger }) {
|
|
|
32122
32630
|
async function applyMcpConfigPointer({ existingMcp, global, hasLiveServers, outputRoot, logger }) {
|
|
32123
32631
|
const { pointer, configLabel, mcpLabel } = pointerLabels(global);
|
|
32124
32632
|
const existing = existingMcp.mcpConfigPath;
|
|
32125
|
-
const
|
|
32126
|
-
|
|
32127
|
-
fileName,
|
|
32633
|
+
const classification = classifyExistingPointer({
|
|
32634
|
+
existing,
|
|
32128
32635
|
global,
|
|
32129
32636
|
outputRoot
|
|
32130
|
-
})
|
|
32131
|
-
const pointsAtGeneratedFile = namesFile(ROVODEV_MCP_FILE_NAME);
|
|
32637
|
+
});
|
|
32132
32638
|
if (!hasLiveServers) {
|
|
32133
|
-
if (
|
|
32639
|
+
if (classification.kind === "already-generated") logger?.warn(`Rovo Dev MCP: mcp.mcpConfigPath in ${configLabel} points at ${mcpLabel}, which now has no enabled server. Rovo Dev reads MCP servers from that file and nowhere else, so ${global ? "Rovo Dev has" : "this project has"} no MCP servers at all until one targeting rovodev is added back — remove the mcp.mcpConfigPath line to fall back to ${global ? "Rovo Dev's own default" : "the global config"}.`);
|
|
32134
32640
|
return false;
|
|
32135
32641
|
}
|
|
32136
|
-
|
|
32137
|
-
|
|
32138
|
-
|
|
32139
|
-
|
|
32140
|
-
|
|
32642
|
+
switch (classification.kind) {
|
|
32643
|
+
case "unset": {
|
|
32644
|
+
const displaced = global ? await describeDisplacedGlobalServers({ outputRoot }) : null;
|
|
32645
|
+
if (displaced !== null) {
|
|
32646
|
+
logger?.warn(`Rovo Dev MCP: leaving mcp.mcpConfigPath unset in ${configLabel}, because ${displaced}. Atlassian documents that path and ${mcpLabel} as two different defaults for the setting, and mcpConfigPath names one config rather than merging, so pointing it at ${mcpLabel} would stop those servers being read on every project. Move the ones you want to keep into .rulesync/mcp.jsonc — or none at all, if it turns out to hold nothing you need — then set mcp.mcpConfigPath to "${pointer}" yourself, which rulesync will not overwrite.`);
|
|
32647
|
+
return false;
|
|
32648
|
+
}
|
|
32649
|
+
existingMcp.mcpConfigPath = pointer;
|
|
32650
|
+
announcePointer({
|
|
32651
|
+
global,
|
|
32652
|
+
logger
|
|
32653
|
+
});
|
|
32654
|
+
return true;
|
|
32141
32655
|
}
|
|
32142
|
-
|
|
32143
|
-
|
|
32144
|
-
|
|
32145
|
-
|
|
32146
|
-
|
|
32147
|
-
|
|
32148
|
-
|
|
32149
|
-
|
|
32150
|
-
|
|
32151
|
-
|
|
32152
|
-
|
|
32153
|
-
|
|
32154
|
-
logger
|
|
32155
|
-
|
|
32156
|
-
return false;
|
|
32157
|
-
}
|
|
32158
|
-
if (global && normalizedExisting !== void 0 && envVarMcpFileSpellings({ fileName: "mcp.json" }).includes(normalizedExisting)) {
|
|
32159
|
-
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.`);
|
|
32160
|
-
return false;
|
|
32656
|
+
case "already-generated": return false;
|
|
32657
|
+
case "documented-default":
|
|
32658
|
+
await warnAtDocumentedDefault({
|
|
32659
|
+
existing,
|
|
32660
|
+
outputRoot,
|
|
32661
|
+
logger
|
|
32662
|
+
});
|
|
32663
|
+
return false;
|
|
32664
|
+
case "env-var-spelling":
|
|
32665
|
+
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.`);
|
|
32666
|
+
return false;
|
|
32667
|
+
case "unrelated":
|
|
32668
|
+
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}".`);
|
|
32669
|
+
return false;
|
|
32161
32670
|
}
|
|
32162
|
-
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}".`);
|
|
32163
|
-
return false;
|
|
32164
32671
|
}
|
|
32165
32672
|
/**
|
|
32166
32673
|
* Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
|
|
@@ -32202,14 +32709,20 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32202
32709
|
relativeFilePath: ROVODEV_MCP_FILE_NAME
|
|
32203
32710
|
};
|
|
32204
32711
|
}
|
|
32205
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
32206
|
-
const
|
|
32207
|
-
const
|
|
32712
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
32713
|
+
const rovodevConfig = await readRovodevConfigYaml({ outputRoot });
|
|
32714
|
+
const paths = await resolveRovodevMcpImportPath({
|
|
32715
|
+
outputRoot,
|
|
32716
|
+
global,
|
|
32717
|
+
config: rovodevConfig,
|
|
32718
|
+
logger
|
|
32719
|
+
});
|
|
32720
|
+
const json = parseRovodevMcpJson(await readFileContentOrNull(paths.filePath) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
|
|
32208
32721
|
const newJson = {
|
|
32209
32722
|
...json,
|
|
32210
32723
|
mcpServers: json.mcpServers ?? {}
|
|
32211
32724
|
};
|
|
32212
|
-
const disabledNames = disabledNamesOf(
|
|
32725
|
+
const disabledNames = disabledNamesOf(rovodevConfig);
|
|
32213
32726
|
if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
|
|
32214
32727
|
const servers = newJson.mcpServers;
|
|
32215
32728
|
for (const name of disabledNames) {
|
|
@@ -32239,9 +32752,8 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
|
|
|
32239
32752
|
} catch {
|
|
32240
32753
|
canWriteDisableToggle = false;
|
|
32241
32754
|
}
|
|
32242
|
-
const rawMcpServers = rulesyncMcp.getJson().mcpServers;
|
|
32243
32755
|
const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
|
|
32244
|
-
const rawServer =
|
|
32756
|
+
const rawServer = rulesyncMcp.getRawMcpServer(name);
|
|
32245
32757
|
const record = {
|
|
32246
32758
|
...server,
|
|
32247
32759
|
...readEnableInstructions(rawServer) && { rovodevEnableInstructions: true }
|
|
@@ -34229,6 +34741,518 @@ function convertAmpToRulesync({ disable, permissions }) {
|
|
|
34229
34741
|
return { permission };
|
|
34230
34742
|
}
|
|
34231
34743
|
//#endregion
|
|
34744
|
+
//#region src/utils/glob.ts
|
|
34745
|
+
/**
|
|
34746
|
+
* Convert a glob-like pattern into an anchored regex source string.
|
|
34747
|
+
*
|
|
34748
|
+
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34749
|
+
* every other regex metacharacter is escaped so it matches literally. The
|
|
34750
|
+
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34751
|
+
* name?" rather than "does this appear somewhere in it?".
|
|
34752
|
+
*
|
|
34753
|
+
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34754
|
+
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34755
|
+
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34756
|
+
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34757
|
+
* has to say what the tool would read rather than what a glob means. Use
|
|
34758
|
+
* `matchesGlob` for an actual comparison.
|
|
34759
|
+
*/
|
|
34760
|
+
function globToAnchoredRegexSource(glob) {
|
|
34761
|
+
let source = "";
|
|
34762
|
+
for (const char of glob) if (char === "*") source += ".*";
|
|
34763
|
+
else if (char === "?") source += ".";
|
|
34764
|
+
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34765
|
+
else source += char;
|
|
34766
|
+
return `^${source}$`;
|
|
34767
|
+
}
|
|
34768
|
+
/**
|
|
34769
|
+
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34770
|
+
* bracket is never closed — in which case it is an ordinary character.
|
|
34771
|
+
*/
|
|
34772
|
+
function parseGlobClass(characters, start) {
|
|
34773
|
+
let index = start;
|
|
34774
|
+
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34775
|
+
if (negated) index += 1;
|
|
34776
|
+
const members = /* @__PURE__ */ new Set();
|
|
34777
|
+
const ranges = [];
|
|
34778
|
+
let first = true;
|
|
34779
|
+
while (index < characters.length) {
|
|
34780
|
+
const character = characters[index] ?? "";
|
|
34781
|
+
if (character === "]" && !first) return {
|
|
34782
|
+
step: {
|
|
34783
|
+
kind: "class",
|
|
34784
|
+
negated,
|
|
34785
|
+
members,
|
|
34786
|
+
ranges
|
|
34787
|
+
},
|
|
34788
|
+
next: index + 1
|
|
34789
|
+
};
|
|
34790
|
+
first = false;
|
|
34791
|
+
const high = characters[index + 2];
|
|
34792
|
+
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34793
|
+
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34794
|
+
index += 3;
|
|
34795
|
+
continue;
|
|
34796
|
+
}
|
|
34797
|
+
members.add(character);
|
|
34798
|
+
index += 1;
|
|
34799
|
+
}
|
|
34800
|
+
}
|
|
34801
|
+
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34802
|
+
function parseGlob(glob) {
|
|
34803
|
+
const characters = [...glob];
|
|
34804
|
+
const steps = [];
|
|
34805
|
+
let index = 0;
|
|
34806
|
+
let bracketsAreClosed = true;
|
|
34807
|
+
while (index < characters.length) {
|
|
34808
|
+
const character = characters[index] ?? "";
|
|
34809
|
+
index += 1;
|
|
34810
|
+
if (character === "*") {
|
|
34811
|
+
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34812
|
+
continue;
|
|
34813
|
+
}
|
|
34814
|
+
if (character === "?") {
|
|
34815
|
+
steps.push({ kind: "any" });
|
|
34816
|
+
continue;
|
|
34817
|
+
}
|
|
34818
|
+
if (character === "[" && bracketsAreClosed) {
|
|
34819
|
+
const parsed = parseGlobClass(characters, index);
|
|
34820
|
+
if (parsed === void 0) bracketsAreClosed = false;
|
|
34821
|
+
else {
|
|
34822
|
+
steps.push(parsed.step);
|
|
34823
|
+
index = parsed.next;
|
|
34824
|
+
continue;
|
|
34825
|
+
}
|
|
34826
|
+
}
|
|
34827
|
+
steps.push({
|
|
34828
|
+
kind: "literal",
|
|
34829
|
+
character
|
|
34830
|
+
});
|
|
34831
|
+
}
|
|
34832
|
+
return steps;
|
|
34833
|
+
}
|
|
34834
|
+
function matchesGlobStep(step, character) {
|
|
34835
|
+
if (step.kind === "star") return false;
|
|
34836
|
+
if (step.kind === "any") return true;
|
|
34837
|
+
if (step.kind === "literal") return step.character === character;
|
|
34838
|
+
const code = character.codePointAt(0) ?? 0;
|
|
34839
|
+
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34840
|
+
return step.negated ? !admitted : admitted;
|
|
34841
|
+
}
|
|
34842
|
+
/** Whether two single-character steps can both match one same character. */
|
|
34843
|
+
function stepsShareACharacter(left, right) {
|
|
34844
|
+
if (left.kind === "any" || right.kind === "any") return true;
|
|
34845
|
+
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34846
|
+
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34847
|
+
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34848
|
+
return true;
|
|
34849
|
+
}
|
|
34850
|
+
/** Whether every step from `index` on can match the empty string. */
|
|
34851
|
+
function isAllStars(steps, index) {
|
|
34852
|
+
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34853
|
+
return true;
|
|
34854
|
+
}
|
|
34855
|
+
/**
|
|
34856
|
+
* The most work one intersection walk will do, counted in cells times the cost
|
|
34857
|
+
* of one. Past it the two patterns are reported as intersecting without being
|
|
34858
|
+
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34859
|
+
* enough to reach this is pathological rather than a command anybody typed.
|
|
34860
|
+
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34861
|
+
* closed.
|
|
34862
|
+
*/
|
|
34863
|
+
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34864
|
+
/**
|
|
34865
|
+
* The most work a whole run of comparisons will do. A caller holding R
|
|
34866
|
+
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34867
|
+
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34868
|
+
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34869
|
+
* that together take minutes. The shared budget is spent down across the run
|
|
34870
|
+
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34871
|
+
* again the direction that withholds an `allow` rather than writing one.
|
|
34872
|
+
*/
|
|
34873
|
+
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34874
|
+
/**
|
|
34875
|
+
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34876
|
+
* filling the two rows the table is held in, and collecting the answer.
|
|
34877
|
+
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34878
|
+
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34879
|
+
* short allow rules is n squared comparisons that never spend the budget down
|
|
34880
|
+
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34881
|
+
* walk length on the same exhaustible resource.
|
|
34882
|
+
*
|
|
34883
|
+
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34884
|
+
* which lowers how many pairs a run compares from around a million to about
|
|
34885
|
+
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34886
|
+
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34887
|
+
* answer exhaustion gives everywhere else.
|
|
34888
|
+
*/
|
|
34889
|
+
const INTERSECTION_PAIR_COST = 64;
|
|
34890
|
+
/**
|
|
34891
|
+
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34892
|
+
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34893
|
+
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34894
|
+
* it.
|
|
34895
|
+
*/
|
|
34896
|
+
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34897
|
+
return { remaining };
|
|
34898
|
+
}
|
|
34899
|
+
/**
|
|
34900
|
+
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34901
|
+
* the same pattern against a whole list parses it once and reuses the result.
|
|
34902
|
+
*/
|
|
34903
|
+
function parseGlobPattern(glob) {
|
|
34904
|
+
const steps = parseGlob(glob);
|
|
34905
|
+
return {
|
|
34906
|
+
steps,
|
|
34907
|
+
maxRanges: maxRangeCount(steps)
|
|
34908
|
+
};
|
|
34909
|
+
}
|
|
34910
|
+
/**
|
|
34911
|
+
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34912
|
+
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34913
|
+
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34914
|
+
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34915
|
+
* than on cells.
|
|
34916
|
+
*/
|
|
34917
|
+
function maxRangeCount(steps) {
|
|
34918
|
+
let most = 0;
|
|
34919
|
+
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34920
|
+
return most;
|
|
34921
|
+
}
|
|
34922
|
+
/**
|
|
34923
|
+
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34924
|
+
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34925
|
+
* Once that budget is exhausted every further pair answers `true` without being
|
|
34926
|
+
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34927
|
+
* safe side.
|
|
34928
|
+
*/
|
|
34929
|
+
function parsedGlobsIntersect(left, right, budget) {
|
|
34930
|
+
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34931
|
+
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34932
|
+
const cost = rows.length * columns.length * cellCost;
|
|
34933
|
+
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34934
|
+
if (budget !== void 0) {
|
|
34935
|
+
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34936
|
+
if (charge > budget.remaining) {
|
|
34937
|
+
budget.remaining = 0;
|
|
34938
|
+
return true;
|
|
34939
|
+
}
|
|
34940
|
+
budget.remaining -= charge;
|
|
34941
|
+
}
|
|
34942
|
+
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34943
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34944
|
+
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34945
|
+
row[columns.length] = isAllStars(rows, i);
|
|
34946
|
+
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34947
|
+
const rowStep = rows[i];
|
|
34948
|
+
const columnStep = columns[j];
|
|
34949
|
+
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34950
|
+
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34951
|
+
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34952
|
+
continue;
|
|
34953
|
+
}
|
|
34954
|
+
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34955
|
+
}
|
|
34956
|
+
next = row;
|
|
34957
|
+
}
|
|
34958
|
+
return next[0] ?? false;
|
|
34959
|
+
}
|
|
34960
|
+
//#endregion
|
|
34961
|
+
//#region src/features/permissions/shell-command-categories.ts
|
|
34962
|
+
/** The canonical category that names a shell command's permissions. */
|
|
34963
|
+
const SHELL_PERMISSION_CATEGORY = "bash";
|
|
34964
|
+
/**
|
|
34965
|
+
* Collect the canonical rules that govern shell commands, for the adapters
|
|
34966
|
+
* whose tool models commands and nothing else.
|
|
34967
|
+
*
|
|
34968
|
+
* The `bash` category contributes every rule. The all-tools `*` category
|
|
34969
|
+
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
34970
|
+
* written there covers shell commands too, and dropping it inverts the
|
|
34971
|
+
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
34972
|
+
* an adapter that reads only `bash` auto-approves the very command the file
|
|
34973
|
+
* denies.
|
|
34974
|
+
*
|
|
34975
|
+
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
34976
|
+
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
34977
|
+
* carrying it in the restricting direction only over-restricts, while carrying
|
|
34978
|
+
* it in the permissive direction would grant something the author never said
|
|
34979
|
+
* about commands. Both directions therefore fail closed.
|
|
34980
|
+
*/
|
|
34981
|
+
function collectShellCommandRules(permission) {
|
|
34982
|
+
const rules = [];
|
|
34983
|
+
const foreignRestrictingCategories = [];
|
|
34984
|
+
const ignoredAllToolsAllowPatterns = [];
|
|
34985
|
+
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
34986
|
+
if (category === "bash") {
|
|
34987
|
+
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
34988
|
+
pattern,
|
|
34989
|
+
action,
|
|
34990
|
+
fromAllToolsCategory: false
|
|
34991
|
+
});
|
|
34992
|
+
continue;
|
|
34993
|
+
}
|
|
34994
|
+
if (category === "*") {
|
|
34995
|
+
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
34996
|
+
if (action === "allow") {
|
|
34997
|
+
ignoredAllToolsAllowPatterns.push(pattern);
|
|
34998
|
+
continue;
|
|
34999
|
+
}
|
|
35000
|
+
rules.push({
|
|
35001
|
+
pattern,
|
|
35002
|
+
action,
|
|
35003
|
+
fromAllToolsCategory: true
|
|
35004
|
+
});
|
|
35005
|
+
}
|
|
35006
|
+
continue;
|
|
35007
|
+
}
|
|
35008
|
+
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35009
|
+
}
|
|
35010
|
+
return {
|
|
35011
|
+
rules,
|
|
35012
|
+
foreignRestrictingCategories,
|
|
35013
|
+
ignoredAllToolsAllowPatterns
|
|
35014
|
+
};
|
|
35015
|
+
}
|
|
35016
|
+
/**
|
|
35017
|
+
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35018
|
+
* which restrictions it cannot write name some of the same commands? The
|
|
35019
|
+
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35020
|
+
* written — so a caller can report both the allow rules it withheld and the
|
|
35021
|
+
* restrictions that withheld nothing.
|
|
35022
|
+
*
|
|
35023
|
+
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35024
|
+
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35025
|
+
* asking whether any one command matches both. Width does not enter into it: an
|
|
35026
|
+
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35027
|
+
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35028
|
+
* on every `git ... --force` command even though neither pattern covers the
|
|
35029
|
+
* other's spelling. Comparing only identical spellings would let the most
|
|
35030
|
+
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35031
|
+
*
|
|
35032
|
+
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35033
|
+
* the walk for the commonest case.
|
|
35034
|
+
*
|
|
35035
|
+
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35036
|
+
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35037
|
+
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35038
|
+
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35039
|
+
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35040
|
+
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35041
|
+
* withholds an allow rather than writing one the config restricts — see
|
|
35042
|
+
* `warpCommandPatternToGlob`.
|
|
35043
|
+
*/
|
|
35044
|
+
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35045
|
+
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35046
|
+
pattern,
|
|
35047
|
+
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35048
|
+
}));
|
|
35049
|
+
return (allowPattern) => {
|
|
35050
|
+
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35051
|
+
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35052
|
+
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35053
|
+
};
|
|
35054
|
+
}
|
|
35055
|
+
/**
|
|
35056
|
+
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35057
|
+
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35058
|
+
*
|
|
35059
|
+
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35060
|
+
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35061
|
+
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35062
|
+
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35063
|
+
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35064
|
+
*
|
|
35065
|
+
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35066
|
+
* so overlapping no allow rule says nothing is wrong with it.
|
|
35067
|
+
*/
|
|
35068
|
+
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35069
|
+
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35070
|
+
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35071
|
+
return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35072
|
+
}
|
|
35073
|
+
/**
|
|
35074
|
+
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35075
|
+
* commands with those two tiers and nothing else.
|
|
35076
|
+
*
|
|
35077
|
+
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35078
|
+
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35079
|
+
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35080
|
+
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35081
|
+
* about would answer the prompt the author wanted.
|
|
35082
|
+
*
|
|
35083
|
+
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35084
|
+
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35085
|
+
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35086
|
+
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35087
|
+
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35088
|
+
* cannot be written it withholds the allow rules it covers instead, which
|
|
35089
|
+
* restricts in the same direction without touching the denylist.
|
|
35090
|
+
*
|
|
35091
|
+
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35092
|
+
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35093
|
+
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35094
|
+
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35095
|
+
* even where it is written: a pattern under `*` need not name a command —
|
|
35096
|
+
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35097
|
+
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35098
|
+
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35099
|
+
* command pattern is reported; failing open would not be.
|
|
35100
|
+
*
|
|
35101
|
+
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35102
|
+
* patterns are not globs.
|
|
35103
|
+
*/
|
|
35104
|
+
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35105
|
+
const deny = [];
|
|
35106
|
+
const unwrittenDenyPatterns = [];
|
|
35107
|
+
const restrictions = [];
|
|
35108
|
+
const writtenAllToolsDenyPatterns = [];
|
|
35109
|
+
const allToolsAskPatterns = [];
|
|
35110
|
+
for (const rule of rules) {
|
|
35111
|
+
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35112
|
+
if (action === "allow") continue;
|
|
35113
|
+
if (action !== "deny") {
|
|
35114
|
+
restrictions.push(rule);
|
|
35115
|
+
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35116
|
+
continue;
|
|
35117
|
+
}
|
|
35118
|
+
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35119
|
+
deny.push(pattern);
|
|
35120
|
+
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35121
|
+
} else unwrittenDenyPatterns.push(pattern);
|
|
35122
|
+
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35123
|
+
}
|
|
35124
|
+
const budget = createIntersectionBudget();
|
|
35125
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35126
|
+
normalizePattern,
|
|
35127
|
+
budget
|
|
35128
|
+
});
|
|
35129
|
+
const allow = [];
|
|
35130
|
+
const shadowedAllowPatterns = [];
|
|
35131
|
+
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35132
|
+
for (const { pattern, action } of rules) {
|
|
35133
|
+
if (action !== "allow") continue;
|
|
35134
|
+
const shadowing = shadowingRestrictions(pattern);
|
|
35135
|
+
if (shadowing.length > 0) {
|
|
35136
|
+
shadowedAllowPatterns.push(pattern);
|
|
35137
|
+
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35138
|
+
continue;
|
|
35139
|
+
}
|
|
35140
|
+
allow.push(pattern);
|
|
35141
|
+
}
|
|
35142
|
+
return {
|
|
35143
|
+
allow,
|
|
35144
|
+
deny,
|
|
35145
|
+
shadowedAllowPatterns,
|
|
35146
|
+
unwrittenDenyPatterns,
|
|
35147
|
+
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35148
|
+
rules,
|
|
35149
|
+
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35150
|
+
withholdingPatterns
|
|
35151
|
+
}),
|
|
35152
|
+
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35153
|
+
rules,
|
|
35154
|
+
allToolsPatterns: allToolsAskPatterns,
|
|
35155
|
+
withholdingPatterns
|
|
35156
|
+
}),
|
|
35157
|
+
intersectionBudgetExhausted: budget.remaining === 0
|
|
35158
|
+
};
|
|
35159
|
+
}
|
|
35160
|
+
/**
|
|
35161
|
+
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35162
|
+
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35163
|
+
* dropped in one is worded the same way in all.
|
|
35164
|
+
*/
|
|
35165
|
+
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35166
|
+
if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
|
|
35167
|
+
for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
|
|
35168
|
+
if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
|
|
35169
|
+
if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
|
|
35170
|
+
if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
|
|
35171
|
+
if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
|
|
35172
|
+
if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
|
|
35173
|
+
}
|
|
35174
|
+
function resolveShellCommandState(permission, writesAllToolsDeny) {
|
|
35175
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permission);
|
|
35176
|
+
const partitioned = partitionCommandRules({
|
|
35177
|
+
rules,
|
|
35178
|
+
writesAllToolsDeny
|
|
35179
|
+
});
|
|
35180
|
+
return {
|
|
35181
|
+
allow: partitioned.allow,
|
|
35182
|
+
deny: partitioned.deny,
|
|
35183
|
+
bash: bashRulesHonoringAllTools(permission),
|
|
35184
|
+
foreignRestrictingCategories,
|
|
35185
|
+
ignoredAllToolsAllowPatterns,
|
|
35186
|
+
shadowedAllowPatterns: partitioned.shadowedAllowPatterns,
|
|
35187
|
+
unwrittenDenyPatterns: partitioned.unwrittenDenyPatterns,
|
|
35188
|
+
unenforcedAllToolsDenyPatterns: partitioned.unenforcedAllToolsDenyPatterns,
|
|
35189
|
+
unenforcedAllToolsAskPatterns: partitioned.unenforcedAllToolsAskPatterns,
|
|
35190
|
+
intersectionBudgetExhausted: partitioned.intersectionBudgetExhausted
|
|
35191
|
+
};
|
|
35192
|
+
}
|
|
35193
|
+
/**
|
|
35194
|
+
* Collect shell-command allow/deny lists the way the command-only adapters do,
|
|
35195
|
+
* and report every restriction the surface cannot carry.
|
|
35196
|
+
*/
|
|
35197
|
+
function resolveShellCommandLists({ permission, writesAllToolsDeny, toolLabel, surfaceLabel, logger }) {
|
|
35198
|
+
const resolved = resolveShellCommandState(permission, writesAllToolsDeny);
|
|
35199
|
+
warnAboutUnwrittenCommandRules({
|
|
35200
|
+
toolLabel,
|
|
35201
|
+
surfaceLabel,
|
|
35202
|
+
foreignRestrictingCategories: resolved.foreignRestrictingCategories,
|
|
35203
|
+
shadowedAllowPatterns: resolved.shadowedAllowPatterns,
|
|
35204
|
+
unwrittenDenyPatterns: resolved.unwrittenDenyPatterns,
|
|
35205
|
+
unenforcedAllToolsDenyPatterns: resolved.unenforcedAllToolsDenyPatterns,
|
|
35206
|
+
unenforcedAllToolsAskPatterns: resolved.unenforcedAllToolsAskPatterns,
|
|
35207
|
+
ignoredAllToolsAllowPatterns: resolved.ignoredAllToolsAllowPatterns,
|
|
35208
|
+
intersectionBudgetExhausted: resolved.intersectionBudgetExhausted,
|
|
35209
|
+
logger
|
|
35210
|
+
});
|
|
35211
|
+
return {
|
|
35212
|
+
allow: resolved.allow,
|
|
35213
|
+
deny: resolved.deny,
|
|
35214
|
+
bash: resolved.bash
|
|
35215
|
+
};
|
|
35216
|
+
}
|
|
35217
|
+
/**
|
|
35218
|
+
* The `bash` category after all-tools `*` restrictions have been applied. A
|
|
35219
|
+
* `deny`/`ask` written under `*` covers shell commands too, so a bash `allow`
|
|
35220
|
+
* it overlaps is withheld, a `*` deny is copied in, and a `*` ask is copied in
|
|
35221
|
+
* wherever `bash` says nothing about that exact pattern yet — otherwise it
|
|
35222
|
+
* would vanish from the resolved category entirely rather than falling back to
|
|
35223
|
+
* a tier that still prompts. An existing `bash` entry for the same pattern is
|
|
35224
|
+
* never downgraded by a `*` ask (a bash `allow` was already dropped above, and
|
|
35225
|
+
* a bash `deny`/`ask` there is at least as strict already).
|
|
35226
|
+
*/
|
|
35227
|
+
function bashRulesHonoringAllTools(permission) {
|
|
35228
|
+
const { rules } = collectShellCommandRules(permission);
|
|
35229
|
+
const allToolsRestrictions = rules.filter(({ fromAllToolsCategory }) => fromAllToolsCategory);
|
|
35230
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(allToolsRestrictions);
|
|
35231
|
+
const bash = { ...permission.bash };
|
|
35232
|
+
for (const [pattern, action] of Object.entries(bash)) if (action === "allow" && shadowingRestrictions(pattern).length > 0) delete bash[pattern];
|
|
35233
|
+
for (const { pattern, action } of allToolsRestrictions) {
|
|
35234
|
+
if (isPrototypePollutionKey(pattern)) continue;
|
|
35235
|
+
if (action === "deny") {
|
|
35236
|
+
if (bash[pattern] !== "ask") bash[pattern] = "deny";
|
|
35237
|
+
continue;
|
|
35238
|
+
}
|
|
35239
|
+
if (bash[pattern] === void 0) bash[pattern] = "ask";
|
|
35240
|
+
}
|
|
35241
|
+
return bash;
|
|
35242
|
+
}
|
|
35243
|
+
/**
|
|
35244
|
+
* Return a permission block whose `bash` category honors all-tools `*`
|
|
35245
|
+
* restrictions. Other categories are unchanged, so adapters that already model
|
|
35246
|
+
* `*` keep doing so.
|
|
35247
|
+
*/
|
|
35248
|
+
function honorAllToolsOnBash(permission) {
|
|
35249
|
+
if (permission.bash === void 0) return permission;
|
|
35250
|
+
return {
|
|
35251
|
+
...permission,
|
|
35252
|
+
bash: bashRulesHonoringAllTools(permission)
|
|
35253
|
+
};
|
|
35254
|
+
}
|
|
35255
|
+
//#endregion
|
|
34232
35256
|
//#region src/features/permissions/antigravity-cli-permissions.ts
|
|
34233
35257
|
/**
|
|
34234
35258
|
* Top-level `~/.gemini/antigravity-cli/settings.json` keys the `antigravity-cli`
|
|
@@ -34476,7 +35500,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
|
|
|
34476
35500
|
const allow = [];
|
|
34477
35501
|
const ask = [];
|
|
34478
35502
|
const deny = [];
|
|
34479
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35503
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34480
35504
|
const cliToolName = toAntigravityCliToolName(category);
|
|
34481
35505
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
34482
35506
|
const entry = buildPermissionEntry$1(cliToolName, pattern);
|
|
@@ -34687,7 +35711,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
|
|
|
34687
35711
|
const allow = [];
|
|
34688
35712
|
const ask = [];
|
|
34689
35713
|
const deny = [];
|
|
34690
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
35714
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
34691
35715
|
const action = toIdeAction(category);
|
|
34692
35716
|
for (const [pattern, permissionAction] of Object.entries(rules)) {
|
|
34693
35717
|
const entry = buildPermissionEntry(action, pattern);
|
|
@@ -34727,223 +35751,6 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
|
|
|
34727
35751
|
return { permission };
|
|
34728
35752
|
}
|
|
34729
35753
|
//#endregion
|
|
34730
|
-
//#region src/utils/glob.ts
|
|
34731
|
-
/**
|
|
34732
|
-
* Convert a glob-like pattern into an anchored regex source string.
|
|
34733
|
-
*
|
|
34734
|
-
* Only `*` (any run of characters) and `?` (one character) carry meaning;
|
|
34735
|
-
* every other regex metacharacter is escaped so it matches literally. The
|
|
34736
|
-
* result is anchored at both ends, because the callers ask "is this the whole
|
|
34737
|
-
* name?" rather than "does this appear somewhere in it?".
|
|
34738
|
-
*
|
|
34739
|
-
* Note that `[` and `]` are escaped along with everything else, so a bracket
|
|
34740
|
-
* class is a literal here while `matchesGlob` below reads it as a class. The
|
|
34741
|
-
* one caller wants exactly that: AugmentCode writes this source into its own
|
|
34742
|
-
* config as the tool's own shell-command regex, and never executes it, so it
|
|
34743
|
-
* has to say what the tool would read rather than what a glob means. Use
|
|
34744
|
-
* `matchesGlob` for an actual comparison.
|
|
34745
|
-
*/
|
|
34746
|
-
function globToAnchoredRegexSource(glob) {
|
|
34747
|
-
let source = "";
|
|
34748
|
-
for (const char of glob) if (char === "*") source += ".*";
|
|
34749
|
-
else if (char === "?") source += ".";
|
|
34750
|
-
else if (/[\\^$.|+(){}[\]]/.test(char)) source += `\\${char}`;
|
|
34751
|
-
else source += char;
|
|
34752
|
-
return `^${source}$`;
|
|
34753
|
-
}
|
|
34754
|
-
/**
|
|
34755
|
-
* Read a `[...]` class body starting just past the `[`, or `undefined` when the
|
|
34756
|
-
* bracket is never closed — in which case it is an ordinary character.
|
|
34757
|
-
*/
|
|
34758
|
-
function parseGlobClass(characters, start) {
|
|
34759
|
-
let index = start;
|
|
34760
|
-
const negated = characters[index] === "!" || characters[index] === "^";
|
|
34761
|
-
if (negated) index += 1;
|
|
34762
|
-
const members = /* @__PURE__ */ new Set();
|
|
34763
|
-
const ranges = [];
|
|
34764
|
-
let first = true;
|
|
34765
|
-
while (index < characters.length) {
|
|
34766
|
-
const character = characters[index] ?? "";
|
|
34767
|
-
if (character === "]" && !first) return {
|
|
34768
|
-
step: {
|
|
34769
|
-
kind: "class",
|
|
34770
|
-
negated,
|
|
34771
|
-
members,
|
|
34772
|
-
ranges
|
|
34773
|
-
},
|
|
34774
|
-
next: index + 1
|
|
34775
|
-
};
|
|
34776
|
-
first = false;
|
|
34777
|
-
const high = characters[index + 2];
|
|
34778
|
-
if (characters[index + 1] === "-" && high !== void 0 && high !== "]") {
|
|
34779
|
-
ranges.push([character.codePointAt(0) ?? 0, high.codePointAt(0) ?? 0]);
|
|
34780
|
-
index += 3;
|
|
34781
|
-
continue;
|
|
34782
|
-
}
|
|
34783
|
-
members.add(character);
|
|
34784
|
-
index += 1;
|
|
34785
|
-
}
|
|
34786
|
-
}
|
|
34787
|
-
/** Split a glob into the steps `matchesGlob` walks. */
|
|
34788
|
-
function parseGlob(glob) {
|
|
34789
|
-
const characters = [...glob];
|
|
34790
|
-
const steps = [];
|
|
34791
|
-
let index = 0;
|
|
34792
|
-
let bracketsAreClosed = true;
|
|
34793
|
-
while (index < characters.length) {
|
|
34794
|
-
const character = characters[index] ?? "";
|
|
34795
|
-
index += 1;
|
|
34796
|
-
if (character === "*") {
|
|
34797
|
-
if (steps.at(-1)?.kind !== "star") steps.push({ kind: "star" });
|
|
34798
|
-
continue;
|
|
34799
|
-
}
|
|
34800
|
-
if (character === "?") {
|
|
34801
|
-
steps.push({ kind: "any" });
|
|
34802
|
-
continue;
|
|
34803
|
-
}
|
|
34804
|
-
if (character === "[" && bracketsAreClosed) {
|
|
34805
|
-
const parsed = parseGlobClass(characters, index);
|
|
34806
|
-
if (parsed === void 0) bracketsAreClosed = false;
|
|
34807
|
-
else {
|
|
34808
|
-
steps.push(parsed.step);
|
|
34809
|
-
index = parsed.next;
|
|
34810
|
-
continue;
|
|
34811
|
-
}
|
|
34812
|
-
}
|
|
34813
|
-
steps.push({
|
|
34814
|
-
kind: "literal",
|
|
34815
|
-
character
|
|
34816
|
-
});
|
|
34817
|
-
}
|
|
34818
|
-
return steps;
|
|
34819
|
-
}
|
|
34820
|
-
function matchesGlobStep(step, character) {
|
|
34821
|
-
if (step.kind === "star") return false;
|
|
34822
|
-
if (step.kind === "any") return true;
|
|
34823
|
-
if (step.kind === "literal") return step.character === character;
|
|
34824
|
-
const code = character.codePointAt(0) ?? 0;
|
|
34825
|
-
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
34826
|
-
return step.negated ? !admitted : admitted;
|
|
34827
|
-
}
|
|
34828
|
-
/** Whether two single-character steps can both match one same character. */
|
|
34829
|
-
function stepsShareACharacter(left, right) {
|
|
34830
|
-
if (left.kind === "any" || right.kind === "any") return true;
|
|
34831
|
-
if (left.kind === "literal" && right.kind === "literal") return left.character === right.character;
|
|
34832
|
-
if (left.kind === "literal") return matchesGlobStep(right, left.character);
|
|
34833
|
-
if (right.kind === "literal") return matchesGlobStep(left, right.character);
|
|
34834
|
-
return true;
|
|
34835
|
-
}
|
|
34836
|
-
/** Whether every step from `index` on can match the empty string. */
|
|
34837
|
-
function isAllStars(steps, index) {
|
|
34838
|
-
for (let step = index; step < steps.length; step++) if (steps[step]?.kind !== "star") return false;
|
|
34839
|
-
return true;
|
|
34840
|
-
}
|
|
34841
|
-
/**
|
|
34842
|
-
* The most work one intersection walk will do, counted in cells times the cost
|
|
34843
|
-
* of one. Past it the two patterns are reported as intersecting without being
|
|
34844
|
-
* walked: the product of two lengths grows quadratically, and a pattern long
|
|
34845
|
-
* enough to reach this is pathological rather than a command anybody typed.
|
|
34846
|
-
* Answering `true` withholds an `allow`, which is the direction that fails
|
|
34847
|
-
* closed.
|
|
34848
|
-
*/
|
|
34849
|
-
const MAX_INTERSECTION_CELLS = 1e6;
|
|
34850
|
-
/**
|
|
34851
|
-
* The most work a whole run of comparisons will do. A caller holding R
|
|
34852
|
-
* restrictions and A allow rules asks R x A times, and a per-pair cap alone
|
|
34853
|
-
* bounds none of that: a hundred restrictions against a hundred allow rules,
|
|
34854
|
-
* each pattern just under the per-pair cap, is ten thousand affordable walks
|
|
34855
|
-
* that together take minutes. The shared budget is spent down across the run
|
|
34856
|
-
* and, once it is gone, every remaining pair is reported as intersecting —
|
|
34857
|
-
* again the direction that withholds an `allow` rather than writing one.
|
|
34858
|
-
*/
|
|
34859
|
-
const MAX_TOTAL_INTERSECTION_CELLS = 1e7;
|
|
34860
|
-
/**
|
|
34861
|
-
* What a pair costs on top of the cells it walks: the call itself, sizing and
|
|
34862
|
-
* filling the two rows the table is held in, and collecting the answer.
|
|
34863
|
-
* Charging only cells would leave the *number* of pairs unbounded — a pair of
|
|
34864
|
-
* one-step patterns walks a single cell, so n short restrictions against n
|
|
34865
|
-
* short allow rules is n squared comparisons that never spend the budget down
|
|
34866
|
-
* however many of them there are. Charging a floor per pair puts pair count and
|
|
34867
|
-
* walk length on the same exhaustible resource.
|
|
34868
|
-
*
|
|
34869
|
-
* For the short patterns of an ordinary config the floor is the whole charge,
|
|
34870
|
-
* which lowers how many pairs a run compares from around a million to about
|
|
34871
|
-
* 150,000 — roughly 400 restrictions against 400 allow rules. A config past
|
|
34872
|
-
* that line withholds every allow it has not yet compared, the same fail-closed
|
|
34873
|
-
* answer exhaustion gives everywhere else.
|
|
34874
|
-
*/
|
|
34875
|
-
const INTERSECTION_PAIR_COST = 64;
|
|
34876
|
-
/**
|
|
34877
|
-
* A budget for one caller's run of comparisons. Hand the same one to every
|
|
34878
|
-
* `parsedGlobsIntersect` call that belongs together — one adapter reading one
|
|
34879
|
-
* config — so the run as a whole stays bounded rather than only each pair in
|
|
34880
|
-
* it.
|
|
34881
|
-
*/
|
|
34882
|
-
function createIntersectionBudget(remaining = MAX_TOTAL_INTERSECTION_CELLS) {
|
|
34883
|
-
return { remaining };
|
|
34884
|
-
}
|
|
34885
|
-
/**
|
|
34886
|
-
* Parse `glob` into the form `parsedGlobsIntersect` walks. A caller comparing
|
|
34887
|
-
* the same pattern against a whole list parses it once and reuses the result.
|
|
34888
|
-
*/
|
|
34889
|
-
function parseGlobPattern(glob) {
|
|
34890
|
-
const steps = parseGlob(glob);
|
|
34891
|
-
return {
|
|
34892
|
-
steps,
|
|
34893
|
-
maxRanges: maxRangeCount(steps)
|
|
34894
|
-
};
|
|
34895
|
-
}
|
|
34896
|
-
/**
|
|
34897
|
-
* What one cell can cost, as a multiplier on the cell count. A literal met by a
|
|
34898
|
-
* `[a-z...]` class walks that class's ranges, so a single class carrying
|
|
34899
|
-
* thousands of them turns a walk that looks affordable by cell count alone into
|
|
34900
|
-
* a quadratic one — which is why the budget is spent on cells times this rather
|
|
34901
|
-
* than on cells.
|
|
34902
|
-
*/
|
|
34903
|
-
function maxRangeCount(steps) {
|
|
34904
|
-
let most = 0;
|
|
34905
|
-
for (const step of steps) if (step.kind === "class" && step.ranges.length > most) most = step.ranges.length;
|
|
34906
|
-
return most;
|
|
34907
|
-
}
|
|
34908
|
-
/**
|
|
34909
|
-
* `globsIntersect` for two globs already parsed, optionally spending a budget
|
|
34910
|
-
* shared with the rest of the caller's run — see `createIntersectionBudget`.
|
|
34911
|
-
* Once that budget is exhausted every further pair answers `true` without being
|
|
34912
|
-
* walked, so a caller reading the answer as a reason to restrict stays on the
|
|
34913
|
-
* safe side.
|
|
34914
|
-
*/
|
|
34915
|
-
function parsedGlobsIntersect(left, right, budget) {
|
|
34916
|
-
const [rows, columns] = left.steps.length >= right.steps.length ? [left.steps, right.steps] : [right.steps, left.steps];
|
|
34917
|
-
const cellCost = 1 + left.maxRanges + right.maxRanges;
|
|
34918
|
-
const cost = rows.length * columns.length * cellCost;
|
|
34919
|
-
if (cost > MAX_INTERSECTION_CELLS) return true;
|
|
34920
|
-
if (budget !== void 0) {
|
|
34921
|
-
const charge = cost + INTERSECTION_PAIR_COST;
|
|
34922
|
-
if (charge > budget.remaining) {
|
|
34923
|
-
budget.remaining = 0;
|
|
34924
|
-
return true;
|
|
34925
|
-
}
|
|
34926
|
-
budget.remaining -= charge;
|
|
34927
|
-
}
|
|
34928
|
-
let next = Array.from({ length: columns.length + 1 }, (_, j) => isAllStars(columns, j));
|
|
34929
|
-
for (let i = rows.length - 1; i >= 0; i--) {
|
|
34930
|
-
const row = Array.from({ length: columns.length + 1 }, () => false);
|
|
34931
|
-
row[columns.length] = isAllStars(rows, i);
|
|
34932
|
-
for (let j = columns.length - 1; j >= 0; j--) {
|
|
34933
|
-
const rowStep = rows[i];
|
|
34934
|
-
const columnStep = columns[j];
|
|
34935
|
-
if (rowStep === void 0 || columnStep === void 0) continue;
|
|
34936
|
-
if (rowStep.kind === "star" || columnStep.kind === "star") {
|
|
34937
|
-
row[j] = (next[j] ?? false) || (row[j + 1] ?? false);
|
|
34938
|
-
continue;
|
|
34939
|
-
}
|
|
34940
|
-
row[j] = stepsShareACharacter(rowStep, columnStep) && (next[j + 1] ?? false);
|
|
34941
|
-
}
|
|
34942
|
-
next = row;
|
|
34943
|
-
}
|
|
34944
|
-
return next[0] ?? false;
|
|
34945
|
-
}
|
|
34946
|
-
//#endregion
|
|
34947
35754
|
//#region src/features/permissions/augmentcode-permissions.ts
|
|
34948
35755
|
const moduleLogger$2 = fallbackLogger;
|
|
34949
35756
|
z.enum([
|
|
@@ -35195,6 +36002,7 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35195
36002
|
const basicExistingEntries = existingEntries.filter((entry) => !isSpecialEntry(entry));
|
|
35196
36003
|
const generatedKeys = new Set(generated.map((e) => `${e.toolName}|${e.shellInputRegex ?? ""}|${e.permission.type}`));
|
|
35197
36004
|
const preservedBasicEntries = basicExistingEntries.filter((entry) => {
|
|
36005
|
+
if (entry.toolName === "*") return false;
|
|
35198
36006
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(entry.toolName)) return true;
|
|
35199
36007
|
if (entry.permission.type === "deny") {
|
|
35200
36008
|
const key = `${entry.toolName}|${entry.shellInputRegex ?? ""}|${entry.permission.type}`;
|
|
@@ -35272,7 +36080,18 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
35272
36080
|
};
|
|
35273
36081
|
function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
35274
36082
|
const entries = [];
|
|
35275
|
-
|
|
36083
|
+
const resolvedBashRules = bashRulesHonoringAllTools(config.permission);
|
|
36084
|
+
const permission = config.permission.bash !== void 0 || Object.keys(resolvedBashRules).length > 0 ? {
|
|
36085
|
+
...config.permission,
|
|
36086
|
+
bash: resolvedBashRules
|
|
36087
|
+
} : config.permission;
|
|
36088
|
+
const allToolsFailClosedType = computeAllToolsFailClosedType(config.permission["*"]);
|
|
36089
|
+
const categoriesWithOwnEntries = /* @__PURE__ */ new Set();
|
|
36090
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
36091
|
+
if (category === "*") {
|
|
36092
|
+
logger?.warn("AugmentCode permissions: the all-tools '*' category cannot be emitted as a single entry because AugmentCode has no wildcard toolName. Deny/ask rules are folded into 'launch-process' and, for any other managed tool that has no explicit rules of its own, applied as a blanket deny/ask; all-tools allow rules and tools that already define their own rules are left untouched.");
|
|
36093
|
+
continue;
|
|
36094
|
+
}
|
|
35276
36095
|
const augmentToolName = toAugmentToolName(category);
|
|
35277
36096
|
if (!MANAGED_AUGMENT_TOOL_NAMES.has(augmentToolName) && augmentToolName === category) logger?.warn(`AugmentCode permissions: passing through unknown tool category '${category}' as toolName.`);
|
|
35278
36097
|
if (augmentToolName === "launch-process") {
|
|
@@ -35297,19 +36116,50 @@ function convertRulesyncToAugmentEntries({ config, logger }) {
|
|
|
35297
36116
|
toolName: augmentToolName,
|
|
35298
36117
|
permission: { type: "deny" }
|
|
35299
36118
|
});
|
|
36119
|
+
categoriesWithOwnEntries.add(category);
|
|
35300
36120
|
continue;
|
|
35301
36121
|
}
|
|
35302
36122
|
const droppedPatterns = [];
|
|
35303
|
-
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*")
|
|
35304
|
-
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
36123
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") {
|
|
36124
|
+
entries.push({
|
|
36125
|
+
toolName: augmentToolName,
|
|
36126
|
+
permission: { type: actionToAugmentType(action) }
|
|
36127
|
+
});
|
|
36128
|
+
categoriesWithOwnEntries.add(category);
|
|
36129
|
+
} else droppedPatterns.push(pattern);
|
|
35308
36130
|
if (droppedPatterns.length > 0) logger?.warn(`AugmentCode permissions: dropping non-wildcard patterns for category '${category}' (${droppedPatterns.join(", ")}); AugmentCode does not document a per-input matcher for this tool. Use a 'deny' rule with pattern '*' if you need to block this tool entirely.`);
|
|
35309
36131
|
}
|
|
36132
|
+
entries.push(...synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType));
|
|
35310
36133
|
return entries;
|
|
35311
36134
|
}
|
|
35312
36135
|
/**
|
|
36136
|
+
* The strictest action the all-tools `*` category imposes, for tools with no per-input matcher to
|
|
36137
|
+
* narrow it onto (see {@link synthesizeManagedToolFallbackEntries}). `deny` wins over `ask`,
|
|
36138
|
+
* and an all-tools `allow` never forces an entry — there is nothing to fail closed on.
|
|
36139
|
+
*/
|
|
36140
|
+
function computeAllToolsFailClosedType(allToolsRules) {
|
|
36141
|
+
if (!allToolsRules) return void 0;
|
|
36142
|
+
const actions = Object.values(allToolsRules);
|
|
36143
|
+
if (actions.some((action) => action === "deny")) return "deny";
|
|
36144
|
+
if (actions.some((action) => action === "ask")) return "ask-user";
|
|
36145
|
+
}
|
|
36146
|
+
/**
|
|
36147
|
+
* Extend the same fail-closed treatment `bash` gets (via {@link bashRulesHonoringAllTools}) to the
|
|
36148
|
+
* other managed tools: one that produced no entries of its own above must not fall back to
|
|
36149
|
+
* AugmentCode's own default just because it has no per-input matcher to narrow the all-tools
|
|
36150
|
+
* restriction onto. A category can be stated yet still emit nothing (e.g. only non-`*` allow/ask
|
|
36151
|
+
* patterns, dropped with a warning), so this checks emitted entries rather than whether the
|
|
36152
|
+
* category was merely present in the source config. A tool whose own rules did emit entries is
|
|
36153
|
+
* left untouched here.
|
|
36154
|
+
*/
|
|
36155
|
+
function synthesizeManagedToolFallbackEntries(categoriesWithOwnEntries, allToolsFailClosedType) {
|
|
36156
|
+
if (allToolsFailClosedType === void 0) return [];
|
|
36157
|
+
return Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).filter(([canonicalName]) => canonicalName !== "bash" && !categoriesWithOwnEntries.has(canonicalName)).map(([, augmentToolName]) => ({
|
|
36158
|
+
toolName: augmentToolName,
|
|
36159
|
+
permission: { type: allToolsFailClosedType }
|
|
36160
|
+
}));
|
|
36161
|
+
}
|
|
36162
|
+
/**
|
|
35313
36163
|
* Sort AugmentCode tool-permission entries to make the `first-match-wins` semantics safe and predictable.
|
|
35314
36164
|
*
|
|
35315
36165
|
* Augment evaluates `toolPermissions` top-to-bottom and stops at the first match. To prevent a
|
|
@@ -35404,216 +36254,6 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
35404
36254
|
}
|
|
35405
36255
|
return { permission };
|
|
35406
36256
|
}
|
|
35407
|
-
/**
|
|
35408
|
-
* Collect the canonical rules that govern shell commands, for the adapters
|
|
35409
|
-
* whose tool models commands and nothing else.
|
|
35410
|
-
*
|
|
35411
|
-
* The `bash` category contributes every rule. The all-tools `*` category
|
|
35412
|
-
* contributes its **restricting** rules — `deny` and `ask` — because a rule
|
|
35413
|
-
* written there covers shell commands too, and dropping it inverts the
|
|
35414
|
-
* author's intent: with `{"*": {"rm *": "deny"}, "bash": {"rm *": "allow"}}`,
|
|
35415
|
-
* an adapter that reads only `bash` auto-approves the very command the file
|
|
35416
|
-
* denies.
|
|
35417
|
-
*
|
|
35418
|
-
* Its `allow` rules are deliberately **not** contributed. A pattern under `*`
|
|
35419
|
-
* need not be a command at all — `secrets/**` under `*` denies a path — and
|
|
35420
|
-
* carrying it in the restricting direction only over-restricts, while carrying
|
|
35421
|
-
* it in the permissive direction would grant something the author never said
|
|
35422
|
-
* about commands. Both directions therefore fail closed.
|
|
35423
|
-
*/
|
|
35424
|
-
function collectShellCommandRules(permission) {
|
|
35425
|
-
const rules = [];
|
|
35426
|
-
const foreignRestrictingCategories = [];
|
|
35427
|
-
const ignoredAllToolsAllowPatterns = [];
|
|
35428
|
-
for (const [category, categoryRules] of Object.entries(permission)) {
|
|
35429
|
-
if (category === "bash") {
|
|
35430
|
-
for (const [pattern, action] of Object.entries(categoryRules)) rules.push({
|
|
35431
|
-
pattern,
|
|
35432
|
-
action,
|
|
35433
|
-
fromAllToolsCategory: false
|
|
35434
|
-
});
|
|
35435
|
-
continue;
|
|
35436
|
-
}
|
|
35437
|
-
if (category === "*") {
|
|
35438
|
-
for (const [pattern, action] of Object.entries(categoryRules)) {
|
|
35439
|
-
if (action === "allow") {
|
|
35440
|
-
ignoredAllToolsAllowPatterns.push(pattern);
|
|
35441
|
-
continue;
|
|
35442
|
-
}
|
|
35443
|
-
rules.push({
|
|
35444
|
-
pattern,
|
|
35445
|
-
action,
|
|
35446
|
-
fromAllToolsCategory: true
|
|
35447
|
-
});
|
|
35448
|
-
}
|
|
35449
|
-
continue;
|
|
35450
|
-
}
|
|
35451
|
-
if (Object.values(categoryRules).some((action) => action === "deny" || action === "ask")) foreignRestrictingCategories.push(category);
|
|
35452
|
-
}
|
|
35453
|
-
return {
|
|
35454
|
-
rules,
|
|
35455
|
-
foreignRestrictingCategories,
|
|
35456
|
-
ignoredAllToolsAllowPatterns
|
|
35457
|
-
};
|
|
35458
|
-
}
|
|
35459
|
-
/**
|
|
35460
|
-
* Build the test an adapter applies to an `allow` pattern before writing it:
|
|
35461
|
-
* which restrictions it cannot write name some of the same commands? The
|
|
35462
|
-
* answer is the list of those restrictions — empty when the `allow` may be
|
|
35463
|
-
* written — so a caller can report both the allow rules it withheld and the
|
|
35464
|
-
* restrictions that withheld nothing.
|
|
35465
|
-
*
|
|
35466
|
-
* Canonically the stricter rule wins **whatever its width** — rulesync collapses
|
|
35467
|
-
* colliding rules as `deny > ask > allow` — so the two patterns are compared by
|
|
35468
|
-
* asking whether any one command matches both. Width does not enter into it: an
|
|
35469
|
-
* `ask` on `*` overlaps an allowed `git *`, an `ask` on `npm publish` overlaps
|
|
35470
|
-
* an allowed `npm *`, and an `ask` on `* --force` overlaps an allowed `git *`
|
|
35471
|
-
* on every `git ... --force` command even though neither pattern covers the
|
|
35472
|
-
* other's spelling. Comparing only identical spellings would let the most
|
|
35473
|
-
* ordinary catch-all (`{"*": {"*": "ask"}}`) disappear without a word.
|
|
35474
|
-
*
|
|
35475
|
-
* Identical spellings are still compared as strings first, as a shortcut past
|
|
35476
|
-
* the walk for the commonest case.
|
|
35477
|
-
*
|
|
35478
|
-
* `normalizePattern` rewrites a pattern written in the tool's own language into
|
|
35479
|
-
* the widest glob it could stand for, for a tool whose patterns are not globs.
|
|
35480
|
-
* It reaches the `bash` rules and the `allow` rules, which is where such a
|
|
35481
|
-
* pattern is written; an all-tools `*` pattern is canonical — it is read by
|
|
35482
|
-
* every tool, so it is a glob already — and is compared as it stands. The
|
|
35483
|
-
* rewrite must only ever widen what a pattern covers, so an inexact reading
|
|
35484
|
-
* withholds an allow rather than writing one the config restricts — see
|
|
35485
|
-
* `warpCommandPatternToGlob`.
|
|
35486
|
-
*/
|
|
35487
|
-
function createShadowingRestrictionsTest(restrictions, { normalizePattern = (pattern) => pattern, budget = createIntersectionBudget() } = {}) {
|
|
35488
|
-
const normalized = restrictions.map(({ pattern, fromAllToolsCategory }) => ({
|
|
35489
|
-
pattern,
|
|
35490
|
-
glob: parseGlobPattern(fromAllToolsCategory ? pattern : normalizePattern(pattern))
|
|
35491
|
-
}));
|
|
35492
|
-
return (allowPattern) => {
|
|
35493
|
-
if (budget.remaining === 0) return normalized.map(({ pattern }) => pattern);
|
|
35494
|
-
const allowGlob = parseGlobPattern(normalizePattern(allowPattern));
|
|
35495
|
-
return normalized.filter(({ pattern, glob }) => pattern === allowPattern || parsedGlobsIntersect(glob, allowGlob, budget)).map(({ pattern }) => pattern);
|
|
35496
|
-
};
|
|
35497
|
-
}
|
|
35498
|
-
/**
|
|
35499
|
-
* Which of the given all-tools `*` restrictions look like they may not name a
|
|
35500
|
-
* command at all — the question a `deny` and an `ask` written there both raise.
|
|
35501
|
-
*
|
|
35502
|
-
* "Withheld no allow rule" alone does not answer it: a config with no `allow`
|
|
35503
|
-
* rules has nothing to withhold, and a pattern the author also wrote under
|
|
35504
|
-
* `bash` is a command on their own word. Both are excluded, so what remains is
|
|
35505
|
-
* a `*` pattern that had allow rules to overlap, overlapped none of them, and
|
|
35506
|
-
* is claimed as a command nowhere else — the shape `secrets/**` has.
|
|
35507
|
-
*
|
|
35508
|
-
* A `bash` restriction never belongs here: it names a command by construction,
|
|
35509
|
-
* so overlapping no allow rule says nothing is wrong with it.
|
|
35510
|
-
*/
|
|
35511
|
-
function collectUnenforcedAllToolsPatterns({ rules, allToolsPatterns, withholdingPatterns }) {
|
|
35512
|
-
if (!rules.some(({ action }) => action === "allow")) return [];
|
|
35513
|
-
const shellPatterns = new Set(rules.filter(({ fromAllToolsCategory }) => !fromAllToolsCategory).map(({ pattern }) => pattern));
|
|
35514
|
-
return uniq(allToolsPatterns).filter((pattern) => !withholdingPatterns.has(pattern) && !shellPatterns.has(pattern));
|
|
35515
|
-
}
|
|
35516
|
-
/**
|
|
35517
|
-
* Split shell-command rules into the allow and deny lists of a tool that models
|
|
35518
|
-
* commands with those two tiers and nothing else.
|
|
35519
|
-
*
|
|
35520
|
-
* `ask` has no list of its own — such a tool already prompts for whatever it
|
|
35521
|
-
* does not auto-approve, so an `ask` rule is satisfied by writing nothing. It
|
|
35522
|
-
* still has to *withhold* the `allow` rules it covers, though: the canonical
|
|
35523
|
-
* order is `deny > ask > allow`, so auto-approving a command the file also asks
|
|
35524
|
-
* about would answer the prompt the author wanted.
|
|
35525
|
-
*
|
|
35526
|
-
* `writesAllToolsDeny` says whether the tool's denylist can carry a pattern
|
|
35527
|
-
* from the all-tools `*` category. Warp's cannot: it matches commands with
|
|
35528
|
-
* regular expressions rather than globs, and writing any denylist **replaces**
|
|
35529
|
-
* Warp's built-in default one, so an inert `secrets/**` entry there would trade
|
|
35530
|
-
* the tool's own protection for a rule that matches no command. Where the deny
|
|
35531
|
-
* cannot be written it withholds the allow rules it covers instead, which
|
|
35532
|
-
* restricts in the same direction without touching the denylist.
|
|
35533
|
-
*
|
|
35534
|
-
* A `bash` deny withholds nothing: it names a command by construction, so the
|
|
35535
|
-
* denylist entry enforces it wherever the tool's deny-beats-allow order applies,
|
|
35536
|
-
* and a narrow deny keeps carving an exception out of a wider allow (`git *`
|
|
35537
|
-
* allowed, `git push *` denied). An all-tools `*` deny withholds all the same,
|
|
35538
|
-
* even where it is written: a pattern under `*` need not name a command —
|
|
35539
|
-
* `secrets/**` there denies a path — so as a denylist entry it may match nothing
|
|
35540
|
-
* at all, and leaving an overlapping allow beside it would auto-approve the very
|
|
35541
|
-
* commands the author meant to stop. Over-restricting a `*` deny that *was* a
|
|
35542
|
-
* command pattern is reported; failing open would not be.
|
|
35543
|
-
*
|
|
35544
|
-
* `normalizePattern` is handed to `createShadowingRestrictionsTest` for a tool whose
|
|
35545
|
-
* patterns are not globs.
|
|
35546
|
-
*/
|
|
35547
|
-
function partitionCommandRules({ rules, writesAllToolsDeny, normalizePattern }) {
|
|
35548
|
-
const deny = [];
|
|
35549
|
-
const unwrittenDenyPatterns = [];
|
|
35550
|
-
const restrictions = [];
|
|
35551
|
-
const writtenAllToolsDenyPatterns = [];
|
|
35552
|
-
const allToolsAskPatterns = [];
|
|
35553
|
-
for (const rule of rules) {
|
|
35554
|
-
const { pattern, action, fromAllToolsCategory } = rule;
|
|
35555
|
-
if (action === "allow") continue;
|
|
35556
|
-
if (action !== "deny") {
|
|
35557
|
-
restrictions.push(rule);
|
|
35558
|
-
if (fromAllToolsCategory) allToolsAskPatterns.push(pattern);
|
|
35559
|
-
continue;
|
|
35560
|
-
}
|
|
35561
|
-
if (writesAllToolsDeny || !fromAllToolsCategory) {
|
|
35562
|
-
deny.push(pattern);
|
|
35563
|
-
if (fromAllToolsCategory) writtenAllToolsDenyPatterns.push(pattern);
|
|
35564
|
-
} else unwrittenDenyPatterns.push(pattern);
|
|
35565
|
-
if (fromAllToolsCategory) restrictions.push(rule);
|
|
35566
|
-
}
|
|
35567
|
-
const budget = createIntersectionBudget();
|
|
35568
|
-
const shadowingRestrictions = createShadowingRestrictionsTest(restrictions, {
|
|
35569
|
-
normalizePattern,
|
|
35570
|
-
budget
|
|
35571
|
-
});
|
|
35572
|
-
const allow = [];
|
|
35573
|
-
const shadowedAllowPatterns = [];
|
|
35574
|
-
const withholdingPatterns = /* @__PURE__ */ new Set();
|
|
35575
|
-
for (const { pattern, action } of rules) {
|
|
35576
|
-
if (action !== "allow") continue;
|
|
35577
|
-
const shadowing = shadowingRestrictions(pattern);
|
|
35578
|
-
if (shadowing.length > 0) {
|
|
35579
|
-
shadowedAllowPatterns.push(pattern);
|
|
35580
|
-
for (const restriction of shadowing) withholdingPatterns.add(restriction);
|
|
35581
|
-
continue;
|
|
35582
|
-
}
|
|
35583
|
-
allow.push(pattern);
|
|
35584
|
-
}
|
|
35585
|
-
return {
|
|
35586
|
-
allow,
|
|
35587
|
-
deny,
|
|
35588
|
-
shadowedAllowPatterns,
|
|
35589
|
-
unwrittenDenyPatterns,
|
|
35590
|
-
unenforcedAllToolsDenyPatterns: collectUnenforcedAllToolsPatterns({
|
|
35591
|
-
rules,
|
|
35592
|
-
allToolsPatterns: writtenAllToolsDenyPatterns,
|
|
35593
|
-
withholdingPatterns
|
|
35594
|
-
}),
|
|
35595
|
-
unenforcedAllToolsAskPatterns: collectUnenforcedAllToolsPatterns({
|
|
35596
|
-
rules,
|
|
35597
|
-
allToolsPatterns: allToolsAskPatterns,
|
|
35598
|
-
withholdingPatterns
|
|
35599
|
-
}),
|
|
35600
|
-
intersectionBudgetExhausted: budget.remaining === 0
|
|
35601
|
-
};
|
|
35602
|
-
}
|
|
35603
|
-
/**
|
|
35604
|
-
* Report, for one command-only tool, every canonical rule its two lists could
|
|
35605
|
-
* not carry. Every command-only adapter shares this reporting, so a rule
|
|
35606
|
-
* dropped in one is worded the same way in all.
|
|
35607
|
-
*/
|
|
35608
|
-
function warnAboutUnwrittenCommandRules({ toolLabel, surfaceLabel, foreignRestrictingCategories, shadowedAllowPatterns, unwrittenDenyPatterns = [], unwrittenDenyReason, unenforcedAllToolsDenyPatterns = [], unenforcedAllToolsAskPatterns = [], ignoredAllToolsAllowPatterns = [], intersectionBudgetExhausted = false, logger }) {
|
|
35609
|
-
if (intersectionBudgetExhausted) warnWithFallback(logger, `${toolLabel} reached the limit on how much work one generation may spend comparing .rulesync/permissions.jsonc's allow rules against its deny and ask rules, so the allow rules left over were withheld rather than compared — the safe answer, but a wider one than the file asks for. Write fewer or shorter command patterns to have them all compared.`);
|
|
35610
|
-
for (const category of foreignRestrictingCategories) warnWithFallback(logger, `${toolLabel} only models shell-command permissions (${surfaceLabel}); '${category}' deny and ask rules cannot be represented and were skipped.`);
|
|
35611
|
-
if (unwrittenDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} did not write the all-tools '*' deny rule(s) for ${unwrittenDenyPatterns.join(", ")} into its denylist.${unwrittenDenyReason === void 0 ? "" : ` ${unwrittenDenyReason}`} They restrict only by withholding the allow rules they cover; write them under 'bash' to have them enforced as commands.`);
|
|
35612
|
-
if (unenforcedAllToolsDenyPatterns.length > 0) warnWithFallback(logger, `${toolLabel} wrote the all-tools '*' deny rule(s) for ${unenforcedAllToolsDenyPatterns.join(", ")} into its denylist as they stand, but they withheld none of the allow rules beside them. A pattern written under '*' need not name a command — 'secrets/**' there denies a path — and a denylist entry that names none blocks nothing; write it under 'bash' too if it is a command pattern.`);
|
|
35613
|
-
if (unenforcedAllToolsAskPatterns.length > 0) warnWithFallback(logger, `${toolLabel} has no ask tier (${surfaceLabel}), so the all-tools '*' ask rule(s) for ${unenforcedAllToolsAskPatterns.join(", ")} restrict only by withholding the allow rules they cover — and they covered none. A pattern written under '*' need not name a command, so nothing observed says these ones do; write them under 'bash' if they are command patterns.`);
|
|
35614
|
-
if (ignoredAllToolsAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} reads the all-tools '*' category for its deny and ask rules only, so the allow rule(s) for ${ignoredAllToolsAllowPatterns.join(", ")} were skipped — a pattern written under '*' need not be a command. Write them under 'bash' to auto-approve them as commands.`);
|
|
35615
|
-
if (shadowedAllowPatterns.length > 0) warnWithFallback(logger, `${toolLabel} was not given the allow rule(s) for ${shadowedAllowPatterns.join(", ")} because .rulesync/permissions.jsonc restricts the same commands elsewhere, and the stricter rule wins whatever its width.`);
|
|
35616
|
-
}
|
|
35617
36257
|
//#endregion
|
|
35618
36258
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
35619
36259
|
/**
|
|
@@ -37375,7 +38015,7 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
|
37375
38015
|
return merged;
|
|
37376
38016
|
}
|
|
37377
38017
|
function buildCodexBashRulesContent(config) {
|
|
37378
|
-
const bashRules = config.permission
|
|
38018
|
+
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
37379
38019
|
const entries = Object.entries(bashRules);
|
|
37380
38020
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
37381
38021
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
@@ -38066,7 +38706,7 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
38066
38706
|
function convertRulesyncToCursorPermissions(config, logger) {
|
|
38067
38707
|
const allow = [];
|
|
38068
38708
|
const deny = [];
|
|
38069
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
38709
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38070
38710
|
const cursorType = toCursorType(category);
|
|
38071
38711
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38072
38712
|
const entry = buildCursorPermissionEntry(cursorType, toCursorPattern(category, pattern));
|
|
@@ -38885,7 +39525,7 @@ function convertRulesyncToDevinPermissions(config) {
|
|
|
38885
39525
|
const allow = [];
|
|
38886
39526
|
const ask = [];
|
|
38887
39527
|
const deny = [];
|
|
38888
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
39528
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
38889
39529
|
const scope = toDevinScope(category);
|
|
38890
39530
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
38891
39531
|
const entry = buildDevinPermissionEntry(scope, pattern);
|
|
@@ -39173,6 +39813,19 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
|
|
|
39173
39813
|
isDeletable() {
|
|
39174
39814
|
return false;
|
|
39175
39815
|
}
|
|
39816
|
+
/**
|
|
39817
|
+
* `permission.yaml` is Goose's file, not one rulesync owns: rulesync merges
|
|
39818
|
+
* into it when it exists but has no business bringing it into existence to
|
|
39819
|
+
* hold nothing. When no rule maps, the `user` block holds three empty lists,
|
|
39820
|
+
* which would otherwise be written as a fresh permission.yaml that says
|
|
39821
|
+
* nothing — an absent file and empty lists both mean "no user override, so
|
|
39822
|
+
* Goose decides on its own". An existing file is still rewritten as before,
|
|
39823
|
+
* so user content is never dropped — the skip only applies when there is no
|
|
39824
|
+
* file yet.
|
|
39825
|
+
*/
|
|
39826
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
39827
|
+
return true;
|
|
39828
|
+
}
|
|
39176
39829
|
static getSettablePaths(_options) {
|
|
39177
39830
|
return {
|
|
39178
39831
|
relativeDirPath: GOOSE_GLOBAL_DIR,
|
|
@@ -39257,7 +39910,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
39257
39910
|
never_allow: []
|
|
39258
39911
|
};
|
|
39259
39912
|
const assigned = /* @__PURE__ */ new Map();
|
|
39260
|
-
const orderedEntries = Object.entries(config.permission).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39913
|
+
const orderedEntries = Object.entries(honorAllToolsOnBash(config.permission)).toSorted(([a], [b]) => (a === "edit" ? 1 : 0) - (b === "edit" ? 1 : 0));
|
|
39261
39914
|
for (const [category, rules] of orderedEntries) {
|
|
39262
39915
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
39263
39916
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -39589,7 +40242,7 @@ function unmanagedEntries(existingPermission, key) {
|
|
|
39589
40242
|
*/
|
|
39590
40243
|
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
39591
40244
|
const ranked = /* @__PURE__ */ new Map();
|
|
39592
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
40245
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
39593
40246
|
const entry = buildGrokEntry(category, pattern);
|
|
39594
40247
|
if (entry === null) {
|
|
39595
40248
|
if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
|
|
@@ -39678,6 +40331,8 @@ function deriveGrokPermissionMode(config) {
|
|
|
39678
40331
|
function patternsByAction(category, action) {
|
|
39679
40332
|
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
39680
40333
|
}
|
|
40334
|
+
/** The canonical category whose `deny` rules feed `security.website_blocklist`. */
|
|
40335
|
+
const WEBFETCH_PERMISSION_CATEGORY = "webfetch";
|
|
39681
40336
|
function clonePermissionBlock(permission) {
|
|
39682
40337
|
return Object.fromEntries(Object.entries(permission).map(([category, rules]) => [category, { ...rules }]));
|
|
39683
40338
|
}
|
|
@@ -39690,18 +40345,39 @@ function ensureCategory(permission, category) {
|
|
|
39690
40345
|
function removeEmptyCategories(permission) {
|
|
39691
40346
|
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
39692
40347
|
}
|
|
40348
|
+
/**
|
|
40349
|
+
* Make the native `command_allowlist` authoritative for the `bash` allow rules
|
|
40350
|
+
* it can speak for, and for those only. The allowlist is a list of shell-command
|
|
40351
|
+
* patterns, so it is generated from `bash` alone (see `fromRulesyncPermissions`);
|
|
40352
|
+
* an allow in any other category names something Hermes's allowlist cannot
|
|
40353
|
+
* carry, so its absence from the list says nothing about it and it is kept as
|
|
40354
|
+
* provenance wrote it. The same holds for a `bash` allow the generator withheld
|
|
40355
|
+
* because a stricter `*` or `bash` rule covers it: the allowlist never carried
|
|
40356
|
+
* it, so its absence is not a retraction and the rule is kept too.
|
|
40357
|
+
*/
|
|
39693
40358
|
function reconcileCommandAllowlist({ permission, commandAllowlist }) {
|
|
39694
|
-
const
|
|
39695
|
-
const
|
|
39696
|
-
|
|
39697
|
-
|
|
39698
|
-
|
|
39699
|
-
|
|
39700
|
-
|
|
39701
|
-
for (const pattern of
|
|
39702
|
-
|
|
39703
|
-
|
|
39704
|
-
|
|
40359
|
+
const { rules: commandRules } = collectShellCommandRules(permission);
|
|
40360
|
+
const { shadowedAllowPatterns } = partitionCommandRules({
|
|
40361
|
+
rules: commandRules,
|
|
40362
|
+
writesAllToolsDeny: false
|
|
40363
|
+
});
|
|
40364
|
+
const withheld = new Set(shadowedAllowPatterns);
|
|
40365
|
+
const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
|
|
40366
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
|
|
40367
|
+
for (const pattern of commandAllowlist) rules[pattern] = "allow";
|
|
40368
|
+
}
|
|
40369
|
+
/**
|
|
40370
|
+
* Report the restricting rules Hermes has no per-pattern primitive for: a
|
|
40371
|
+
* `deny` or `ask` in any category other than `bash`, `*`, and `webfetch`, and
|
|
40372
|
+
* an `ask` under `webfetch` — the blocklist carries a `webfetch` deny but has
|
|
40373
|
+
* no ask tier. (`bash` and `*` are reported by `warnAboutUnwrittenCommandRules`.)
|
|
40374
|
+
* Such rules survive only in the round-trip blob.
|
|
40375
|
+
*/
|
|
40376
|
+
function warnAboutUnexpressedHermesRestrictions({ permissionBlock, foreignRestrictingCategories, logger }) {
|
|
40377
|
+
for (const category of foreignRestrictingCategories) {
|
|
40378
|
+
const isWebfetch = category === WEBFETCH_PERMISSION_CATEGORY;
|
|
40379
|
+
if (isWebfetch && patternsByAction(permissionBlock[category], "ask").length === 0) continue;
|
|
40380
|
+
warnWithFallback(logger, isWebfetch ? "Hermes Agent's security.website_blocklist has no ask tier, so the 'webfetch' ask rule(s) cannot be represented and were skipped; they survive only in the permissions.rulesync round-trip block." : `Hermes Agent has no per-pattern primitive for '${category}' deny and ask rules (it enforces command_allowlist, approvals.deny, and security.website_blocklist), so they were skipped; they survive only in the permissions.rulesync round-trip block.`);
|
|
39705
40381
|
}
|
|
39706
40382
|
}
|
|
39707
40383
|
function reconcileNativeDenies({ permission, category, patterns }) {
|
|
@@ -39813,14 +40489,14 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39813
40489
|
const approvals = isRecord$1(config.approvals) ? config.approvals : {};
|
|
39814
40490
|
reconcileNativeDenies({
|
|
39815
40491
|
permission,
|
|
39816
|
-
category:
|
|
40492
|
+
category: SHELL_PERMISSION_CATEGORY,
|
|
39817
40493
|
patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
|
|
39818
40494
|
});
|
|
39819
40495
|
const security = isRecord$1(config.security) ? config.security : {};
|
|
39820
40496
|
const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
|
|
39821
40497
|
reconcileNativeDenies({
|
|
39822
40498
|
permission,
|
|
39823
|
-
category:
|
|
40499
|
+
category: WEBFETCH_PERMISSION_CATEGORY,
|
|
39824
40500
|
patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
|
|
39825
40501
|
});
|
|
39826
40502
|
removeEmptyCategories(permission);
|
|
@@ -39840,12 +40516,32 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
39840
40516
|
fileContent: JSON.stringify(imported, null, 2)
|
|
39841
40517
|
});
|
|
39842
40518
|
}
|
|
39843
|
-
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false }) {
|
|
40519
|
+
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, global = false, logger }) {
|
|
39844
40520
|
const permissions = rulesyncPermissions.getJson();
|
|
39845
40521
|
const permissionBlock = permissions.permission ?? {};
|
|
39846
|
-
const
|
|
39847
|
-
const bashDeny =
|
|
39848
|
-
|
|
40522
|
+
const { rules, foreignRestrictingCategories, ignoredAllToolsAllowPatterns } = collectShellCommandRules(permissionBlock);
|
|
40523
|
+
const { allow: commandAllowlist, deny: bashDeny, shadowedAllowPatterns, unwrittenDenyPatterns, unenforcedAllToolsAskPatterns, intersectionBudgetExhausted } = partitionCommandRules({
|
|
40524
|
+
rules,
|
|
40525
|
+
writesAllToolsDeny: false
|
|
40526
|
+
});
|
|
40527
|
+
warnAboutUnexpressedHermesRestrictions({
|
|
40528
|
+
permissionBlock,
|
|
40529
|
+
foreignRestrictingCategories,
|
|
40530
|
+
logger
|
|
40531
|
+
});
|
|
40532
|
+
warnAboutUnwrittenCommandRules({
|
|
40533
|
+
toolLabel: "Hermes Agent",
|
|
40534
|
+
surfaceLabel: "command_allowlist/approvals.deny",
|
|
40535
|
+
foreignRestrictingCategories: [],
|
|
40536
|
+
shadowedAllowPatterns,
|
|
40537
|
+
unwrittenDenyPatterns,
|
|
40538
|
+
unwrittenDenyReason: "approvals.deny is a hard denylist of shell commands, and a pattern written under '*' need not be a command at all.",
|
|
40539
|
+
unenforcedAllToolsAskPatterns,
|
|
40540
|
+
ignoredAllToolsAllowPatterns,
|
|
40541
|
+
intersectionBudgetExhausted,
|
|
40542
|
+
logger
|
|
40543
|
+
});
|
|
40544
|
+
const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
|
|
39849
40545
|
let config = {};
|
|
39850
40546
|
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
39851
40547
|
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
@@ -40112,7 +40808,7 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
40112
40808
|
*/
|
|
40113
40809
|
function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
|
|
40114
40810
|
const ruleLists = {};
|
|
40115
|
-
for (const [category, patterns] of Object.entries(config.permission)) {
|
|
40811
|
+
for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
40116
40812
|
const group = CANONICAL_TO_JUNIE_GROUP[category];
|
|
40117
40813
|
if (!group) {
|
|
40118
40814
|
if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
|
|
@@ -40362,7 +41058,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
40362
41058
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
40363
41059
|
const kiloOverride = rulesyncJson.kilo;
|
|
40364
41060
|
const incomingPermission = {
|
|
40365
|
-
...rulesyncJson.permission,
|
|
41061
|
+
...honorAllToolsOnBash(rulesyncJson.permission),
|
|
40366
41062
|
...kiloOverride?.permission
|
|
40367
41063
|
};
|
|
40368
41064
|
const droppedDenyByKey = {};
|
|
@@ -40881,7 +41577,7 @@ function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
|
40881
41577
|
allowedCommands: [],
|
|
40882
41578
|
deniedCommands: []
|
|
40883
41579
|
};
|
|
40884
|
-
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
41580
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) for (const [pattern, action] of Object.entries(rules)) {
|
|
40885
41581
|
if (action === "ask") {
|
|
40886
41582
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
40887
41583
|
continue;
|
|
@@ -41165,7 +41861,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
41165
41861
|
const rulesyncJson = rulesyncPermissions.getJson();
|
|
41166
41862
|
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
41167
41863
|
const sharedPermission = {};
|
|
41168
|
-
for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41864
|
+
for (const [category, value] of Object.entries(honorAllToolsOnBash(rulesyncJson.permission ?? {}))) sharedPermission[toOpencodePermissionKey(category)] = value;
|
|
41169
41865
|
const permission = {};
|
|
41170
41866
|
for (const [category, value] of Object.entries({
|
|
41171
41867
|
...sharedPermission,
|
|
@@ -42294,11 +42990,19 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42294
42990
|
const paths = this.getSettablePaths();
|
|
42295
42991
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
42296
42992
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
42297
|
-
const
|
|
42993
|
+
const permission = rulesyncPermissions.getJson().permission;
|
|
42994
|
+
const bashStated = permission[COMMAND_CATEGORY] !== void 0;
|
|
42298
42995
|
const patch = {};
|
|
42299
|
-
if (
|
|
42996
|
+
if (bashStated) {
|
|
42997
|
+
const { bash } = resolveShellCommandLists({
|
|
42998
|
+
permission,
|
|
42999
|
+
writesAllToolsDeny: true,
|
|
43000
|
+
toolLabel: this.getToolLabel(),
|
|
43001
|
+
surfaceLabel: `${this.getAllowedCommandsKey()}/${this.getDeniedCommandsKey()}`,
|
|
43002
|
+
logger
|
|
43003
|
+
});
|
|
42300
43004
|
const { allowed, denied } = buildVscodeCommandLists({
|
|
42301
|
-
rules,
|
|
43005
|
+
rules: bash,
|
|
42302
43006
|
toolLabel: this.getToolLabel(),
|
|
42303
43007
|
logger
|
|
42304
43008
|
});
|
|
@@ -42309,7 +43013,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
42309
43013
|
outputRoot,
|
|
42310
43014
|
relativeDirPath: paths.relativeDirPath,
|
|
42311
43015
|
relativeFilePath: paths.relativeFilePath,
|
|
42312
|
-
ownsCommandKeys:
|
|
43016
|
+
ownsCommandKeys: bashStated,
|
|
42313
43017
|
fileContent: applySharedConfigPatch({
|
|
42314
43018
|
fileKey: sharedConfigFileKey(paths),
|
|
42315
43019
|
feature: "permissions",
|
|
@@ -42689,7 +43393,7 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
|
|
|
42689
43393
|
config,
|
|
42690
43394
|
logger
|
|
42691
43395
|
});
|
|
42692
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
43396
|
+
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
42693
43397
|
if (category === CATCH_ALL_PATTERN$1) {
|
|
42694
43398
|
const toolWideDefault = convertAllToolsRules({
|
|
42695
43399
|
rules,
|
|
@@ -43699,7 +44403,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
43699
44403
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43700
44404
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
43701
44405
|
const config = parseVibeConfig(existingContent);
|
|
43702
|
-
const permission = rulesyncPermissions.getJson().permission;
|
|
44406
|
+
const permission = honorAllToolsOnBash(rulesyncPermissions.getJson().permission);
|
|
43703
44407
|
const vibeOverride = rulesyncPermissions.getJson().vibe;
|
|
43704
44408
|
const tools = toVibeToolsRecord(config.tools);
|
|
43705
44409
|
const diskShellPatterns = new Map([VIBE_SHELL_CATEGORY, ...VIBE_SHELL_ALIAS_TOOL_NAMES].map((vibeToolName) => [vibeToolName, toStringArray(readVibeToolConfig({
|
|
@@ -45040,6 +45744,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
45040
45744
|
isDeletable() {
|
|
45041
45745
|
return false;
|
|
45042
45746
|
}
|
|
45747
|
+
/**
|
|
45748
|
+
* `settings.toml` is Warp's file, not one rulesync owns: rulesync merges into
|
|
45749
|
+
* it when it exists but has no business bringing it into existence to hold
|
|
45750
|
+
* nothing. When no rule maps, both command lists are dropped and the payload
|
|
45751
|
+
* is a bare `[agents.profiles]` table, which would otherwise be written as a
|
|
45752
|
+
* fresh settings file that says nothing — an absent file and an empty table
|
|
45753
|
+
* both mean "Warp's own defaults". An existing file is still rewritten as
|
|
45754
|
+
* before, so user content is never dropped — the skip only applies when
|
|
45755
|
+
* there is no file yet.
|
|
45756
|
+
*/
|
|
45757
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
45758
|
+
return true;
|
|
45759
|
+
}
|
|
45043
45760
|
static getSettablePaths(_options) {
|
|
45044
45761
|
return {
|
|
45045
45762
|
relativeDirPath: warpSettingsDir(),
|
|
@@ -45651,6 +46368,7 @@ function buildZedToolPermissions({ permission, logger }) {
|
|
|
45651
46368
|
for (const [category, rules] of Object.entries(permission)) {
|
|
45652
46369
|
if (category === "*") {
|
|
45653
46370
|
for (const [pattern, action] of Object.entries(rules)) if (pattern === "*") managedDefault = CANONICAL_TO_ZED_ACTION[action];
|
|
46371
|
+
else if (permission.bash?.[pattern] === "deny" || permission.bash?.[pattern] === "ask") continue;
|
|
45654
46372
|
else logger?.warn(`Zed permissions: dropping the "*" category rule for pattern "${pattern}" — Zed's global tool-permission default takes no patterns; scope the rule to a tool category instead.`);
|
|
45655
46373
|
continue;
|
|
45656
46374
|
}
|
|
@@ -45805,7 +46523,7 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
45805
46523
|
const toolPermissions = asRecord(agent.tool_permissions);
|
|
45806
46524
|
const existingTools = asRecord(toolPermissions.tools);
|
|
45807
46525
|
const { managedDefault, managedTools, excludedCategories, inertMcpCategories } = buildZedToolPermissions({
|
|
45808
|
-
permission: config.permission,
|
|
46526
|
+
permission: honorAllToolsOnBash(config.permission),
|
|
45809
46527
|
logger
|
|
45810
46528
|
});
|
|
45811
46529
|
if (excludedCategories.length > 0) logger?.warn(`Zed permissions: dropping the ${excludedCategories.map((category) => `"${category}"`).join(", ")} ${excludedCategories.length === 1 ? "category" : "categories"} — Zed does not gate its read-only tools, so the entries would never be consulted. Zed's read-denial surface is \`private_files\`, which the ignore feature writes from \`.rulesync/.aiignore\`.`);
|
|
@@ -46314,6 +47032,24 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
46314
47032
|
}
|
|
46315
47033
|
};
|
|
46316
47034
|
//#endregion
|
|
47035
|
+
//#region src/constants/codebuddy-paths.ts
|
|
47036
|
+
/**
|
|
47037
|
+
* CodeBuddy Code configuration-layout conventions.
|
|
47038
|
+
*
|
|
47039
|
+
* CodeBuddy Code (`@tencent-ai/codebuddy-code`) is Tencent Cloud's terminal
|
|
47040
|
+
* coding agent. Its configuration surface mirrors Claude Code closely: a
|
|
47041
|
+
* root memory file plus a `.codebuddy/` tree.
|
|
47042
|
+
*
|
|
47043
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
47044
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
47045
|
+
*/
|
|
47046
|
+
/** Root directory for CodeBuddy Code configuration, relative to the scope root. */
|
|
47047
|
+
const CODEBUDDY_DIR = ".codebuddy";
|
|
47048
|
+
const CODEBUDDY_RULE_FILE_NAME = "CODEBUDDY.md";
|
|
47049
|
+
const CODEBUDDY_LOCAL_RULE_FILE_NAME = "CODEBUDDY.local.md";
|
|
47050
|
+
/** Modular rules directory name under `.codebuddy/`. */
|
|
47051
|
+
const CODEBUDDY_RULES_DIR_NAME = "rules";
|
|
47052
|
+
//#endregion
|
|
46317
47053
|
//#region src/features/skills/simulated-skill.ts
|
|
46318
47054
|
const SimulatedSkillFrontmatterSchema = z.looseObject({
|
|
46319
47055
|
name: z.string(),
|
|
@@ -46852,7 +47588,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46852
47588
|
* This only deletes directories that are no longer in the rulesync source, not directories that will be overwritten.
|
|
46853
47589
|
*/
|
|
46854
47590
|
async removeOrphanAiDirs(existingDirs, generatedDirs) {
|
|
46855
|
-
const generatedPaths = new Set(generatedDirs.map((d) => d.getDirPath()));
|
|
47591
|
+
const generatedPaths = new Set(generatedDirs.map((d) => caseFoldIdentity(d.getDirPath())));
|
|
46856
47592
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
46857
47593
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
46858
47594
|
for (const aiDir of existingDirs) {
|
|
@@ -46877,7 +47613,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46877
47613
|
this.logger.warn(verdict === "equal" ? `Refusing to delete ${quotedDirPath}: it is the root it was found in, not a directory inside that root` : `Refusing to delete ${quotedDirPath}: it is not inside ${quotedRoot}, the root it was found in`);
|
|
46878
47614
|
continue;
|
|
46879
47615
|
}
|
|
46880
|
-
if (!generatedPaths.has(dirPath)) orphanPaths.add(dirPath);
|
|
47616
|
+
if (!generatedPaths.has(caseFoldIdentity(dirPath))) orphanPaths.add(dirPath);
|
|
46881
47617
|
}
|
|
46882
47618
|
return await this.deleteOrphanPaths({
|
|
46883
47619
|
paths: orphanPaths,
|
|
@@ -46976,7 +47712,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46976
47712
|
const mainFile = aiDir.getMainFile();
|
|
46977
47713
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
46978
47714
|
for (const file of aiDir.getOtherFiles()) generatedNames.add(toPosixPath(file.relativeFilePathToDirPath));
|
|
46979
|
-
const generatedNamesFolded = new Set([...generatedNames].map((name) => name
|
|
47715
|
+
const generatedNamesFolded = new Set([...generatedNames].map((name) => caseFoldIdentity(name)));
|
|
46980
47716
|
let existingNames;
|
|
46981
47717
|
try {
|
|
46982
47718
|
existingNames = await listFilePathsRecursively(dirPath, {
|
|
@@ -46991,7 +47727,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
46991
47727
|
const posixName = toPosixPath(existingName);
|
|
46992
47728
|
if (generatedNames.has(posixName)) continue;
|
|
46993
47729
|
const filePath = join(dirPath, existingName);
|
|
46994
|
-
if (generatedNamesFolded.has(posixName
|
|
47730
|
+
if (generatedNamesFolded.has(caseFoldIdentity(posixName))) {
|
|
46995
47731
|
this.logger.warn(`Refusing to delete ${quoteForLog(filePath)}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
46996
47732
|
continue;
|
|
46997
47733
|
}
|
|
@@ -47047,7 +47783,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47047
47783
|
generatedPaths.add(flatFilePath);
|
|
47048
47784
|
for (const file of generatedDir.getOtherFiles()) generatedPaths.add(join(generatedDirPath, file.relativeFilePathToDirPath));
|
|
47049
47785
|
}
|
|
47050
|
-
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => generatedPath
|
|
47786
|
+
const generatedPathsFolded = new Set([...generatedPaths].map((generatedPath) => caseFoldIdentity(generatedPath)));
|
|
47051
47787
|
const orphanPaths = /* @__PURE__ */ new Set();
|
|
47052
47788
|
const quotedOutputRoot = quoteForLog(this.outputRoot);
|
|
47053
47789
|
for (const aiDir of existingFlatFiles) {
|
|
@@ -47077,7 +47813,7 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
47077
47813
|
continue;
|
|
47078
47814
|
}
|
|
47079
47815
|
if (generatedPaths.has(filePath)) continue;
|
|
47080
|
-
if (generatedPathsFolded.has(filePath
|
|
47816
|
+
if (generatedPathsFolded.has(caseFoldIdentity(filePath))) {
|
|
47081
47817
|
this.logger.warn(`Refusing to delete ${quotedFilePath}: this run wrote a file whose path differs from it only in case, which on a case-insensitive filesystem is the very file it wrote`);
|
|
47082
47818
|
continue;
|
|
47083
47819
|
}
|
|
@@ -48909,6 +49645,191 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
|
|
|
48909
49645
|
}
|
|
48910
49646
|
};
|
|
48911
49647
|
//#endregion
|
|
49648
|
+
//#region src/features/skills/crush-skill.ts
|
|
49649
|
+
const CrushSkillFrontmatterSchema = z.looseObject({
|
|
49650
|
+
name: z.string(),
|
|
49651
|
+
description: z.string(),
|
|
49652
|
+
"user-invocable": z.optional(z.boolean()),
|
|
49653
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
49654
|
+
license: z.optional(z.string()),
|
|
49655
|
+
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
49656
|
+
metadata: z.optional(z.looseObject({}))
|
|
49657
|
+
});
|
|
49658
|
+
/**
|
|
49659
|
+
* Represents a Crush Agent Skill directory.
|
|
49660
|
+
*
|
|
49661
|
+
* Crush auto-discovers Agent Skills (`SKILL.md` per directory) from
|
|
49662
|
+
* `.crush/skills/` at project scope and `~/.config/crush/skills/` (or
|
|
49663
|
+
* `$CRUSH_SKILLS_DIR`) at global scope. Unless `$CRUSH_SKILLS_DIR` is set,
|
|
49664
|
+
* Crush also scans several shared directories it does not own (globally
|
|
49665
|
+
* `~/.config/agents/skills/`, `~/.agents/skills/`, `~/.claude/skills/`;
|
|
49666
|
+
* per-project `.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, also
|
|
49667
|
+
* checked at a git worktree's common root); this class writes only to the
|
|
49668
|
+
* Crush-specific path above, leaving those shared roots to their own targets.
|
|
49669
|
+
*
|
|
49670
|
+
* Crush's `UserInvocable` field is a non-pointer Go `bool`, so an omitted
|
|
49671
|
+
* `user-invocable` (at both the root and the `crush:` section) resolves to
|
|
49672
|
+
* `false`: the skill stays reachable by the model but is hidden from Crush's
|
|
49673
|
+
* command palette. See `FromSkillCatalog` in `internal/commands/commands.go`.
|
|
49674
|
+
* @see https://github.com/charmbracelet/crush/blob/main/internal/config/load.go
|
|
49675
|
+
*/
|
|
49676
|
+
var CrushSkill = class CrushSkill extends ToolSkill {
|
|
49677
|
+
constructor({ outputRoot = process.cwd(), relativeDirPath = CRUSH_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
49678
|
+
super({
|
|
49679
|
+
outputRoot,
|
|
49680
|
+
relativeDirPath,
|
|
49681
|
+
dirName,
|
|
49682
|
+
mainFile: {
|
|
49683
|
+
name: SKILL_FILE_NAME,
|
|
49684
|
+
body,
|
|
49685
|
+
frontmatter: { ...frontmatter }
|
|
49686
|
+
},
|
|
49687
|
+
otherFiles,
|
|
49688
|
+
global
|
|
49689
|
+
});
|
|
49690
|
+
if (validate) {
|
|
49691
|
+
const result = this.validate();
|
|
49692
|
+
if (!result.success) throw result.error;
|
|
49693
|
+
}
|
|
49694
|
+
}
|
|
49695
|
+
static getSettablePaths({ global = false } = {}) {
|
|
49696
|
+
return { relativeDirPath: global ? CRUSH_SKILLS_GLOBAL_DIR : CRUSH_SKILLS_PROJECT_DIR };
|
|
49697
|
+
}
|
|
49698
|
+
getFrontmatter() {
|
|
49699
|
+
return CrushSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
49700
|
+
}
|
|
49701
|
+
getBody() {
|
|
49702
|
+
return this.mainFile?.body ?? "";
|
|
49703
|
+
}
|
|
49704
|
+
validate() {
|
|
49705
|
+
if (!this.mainFile) return {
|
|
49706
|
+
success: false,
|
|
49707
|
+
error: /* @__PURE__ */ new Error(`${this.getDirPath()}: ${SKILL_FILE_NAME} file does not exist`)
|
|
49708
|
+
};
|
|
49709
|
+
const result = CrushSkillFrontmatterSchema.safeParse(this.mainFile.frontmatter);
|
|
49710
|
+
if (!result.success) return {
|
|
49711
|
+
success: false,
|
|
49712
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${this.getDirPath()}: ${formatError(result.error)}`)
|
|
49713
|
+
};
|
|
49714
|
+
return {
|
|
49715
|
+
success: true,
|
|
49716
|
+
error: null
|
|
49717
|
+
};
|
|
49718
|
+
}
|
|
49719
|
+
toRulesyncSkill() {
|
|
49720
|
+
const frontmatter = this.getFrontmatter();
|
|
49721
|
+
const crushSection = {
|
|
49722
|
+
...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
|
|
49723
|
+
...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
|
|
49724
|
+
...frontmatter.license !== void 0 && { license: frontmatter.license },
|
|
49725
|
+
...frontmatter.compatibility !== void 0 && { compatibility: frontmatter.compatibility },
|
|
49726
|
+
...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
|
|
49727
|
+
};
|
|
49728
|
+
const rulesyncFrontmatter = {
|
|
49729
|
+
name: frontmatter.name,
|
|
49730
|
+
description: frontmatter.description,
|
|
49731
|
+
targets: ["*"],
|
|
49732
|
+
...Object.keys(crushSection).length > 0 && { crush: crushSection }
|
|
49733
|
+
};
|
|
49734
|
+
return new RulesyncSkill({
|
|
49735
|
+
outputRoot: this.outputRoot,
|
|
49736
|
+
relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
49737
|
+
dirName: this.getDirName(),
|
|
49738
|
+
frontmatter: rulesyncFrontmatter,
|
|
49739
|
+
body: this.getBody(),
|
|
49740
|
+
otherFiles: this.getOtherFiles(),
|
|
49741
|
+
validate: true,
|
|
49742
|
+
global: this.global
|
|
49743
|
+
});
|
|
49744
|
+
}
|
|
49745
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false }) {
|
|
49746
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49747
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
49748
|
+
const crushSection = rulesyncFrontmatter.crush;
|
|
49749
|
+
const resolvedUserInvocable = resolveUserInvocable({
|
|
49750
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49751
|
+
section: crushSection
|
|
49752
|
+
});
|
|
49753
|
+
const resolvedDisableModelInvocation = resolveDisableModelInvocation({
|
|
49754
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49755
|
+
section: crushSection
|
|
49756
|
+
});
|
|
49757
|
+
const license = resolveLicense({
|
|
49758
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49759
|
+
section: crushSection
|
|
49760
|
+
});
|
|
49761
|
+
const compatibility = resolveCompatibility({
|
|
49762
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49763
|
+
section: crushSection
|
|
49764
|
+
});
|
|
49765
|
+
const metadata = resolveMetadata({
|
|
49766
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
49767
|
+
section: crushSection
|
|
49768
|
+
});
|
|
49769
|
+
const compatibilityString = compatibility === void 0 ? void 0 : toCompatibilityString(compatibility);
|
|
49770
|
+
const crushFrontmatter = {
|
|
49771
|
+
name: rulesyncFrontmatter.name,
|
|
49772
|
+
description: rulesyncFrontmatter.description,
|
|
49773
|
+
...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
|
|
49774
|
+
...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
|
|
49775
|
+
...license !== void 0 && { license },
|
|
49776
|
+
...compatibilityString !== void 0 && compatibilityString.length > 0 && { compatibility: compatibilityString },
|
|
49777
|
+
...metadata !== void 0 && { metadata: toStringMetadata(metadata) }
|
|
49778
|
+
};
|
|
49779
|
+
return new CrushSkill({
|
|
49780
|
+
outputRoot,
|
|
49781
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
49782
|
+
dirName: rulesyncSkill.getDirName(),
|
|
49783
|
+
frontmatter: crushFrontmatter,
|
|
49784
|
+
body: rulesyncSkill.getBody(),
|
|
49785
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
49786
|
+
validate,
|
|
49787
|
+
global
|
|
49788
|
+
});
|
|
49789
|
+
}
|
|
49790
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
49791
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
49792
|
+
return targets.includes("*") || targets.includes("crush");
|
|
49793
|
+
}
|
|
49794
|
+
static async fromDir(params) {
|
|
49795
|
+
const loaded = await this.loadSkillDirContent({
|
|
49796
|
+
...params,
|
|
49797
|
+
getSettablePaths: CrushSkill.getSettablePaths
|
|
49798
|
+
});
|
|
49799
|
+
const result = CrushSkillFrontmatterSchema.safeParse(loaded.frontmatter);
|
|
49800
|
+
if (!result.success) {
|
|
49801
|
+
const skillDirPath = join(loaded.outputRoot, loaded.relativeDirPath, loaded.dirName);
|
|
49802
|
+
throw new Error(`Invalid frontmatter in ${join(skillDirPath, SKILL_FILE_NAME)}: ${formatError(result.error)}`);
|
|
49803
|
+
}
|
|
49804
|
+
return new CrushSkill({
|
|
49805
|
+
outputRoot: loaded.outputRoot,
|
|
49806
|
+
relativeDirPath: loaded.relativeDirPath,
|
|
49807
|
+
dirName: loaded.dirName,
|
|
49808
|
+
frontmatter: result.data,
|
|
49809
|
+
body: loaded.body,
|
|
49810
|
+
otherFiles: loaded.otherFiles,
|
|
49811
|
+
validate: true,
|
|
49812
|
+
global: loaded.global
|
|
49813
|
+
});
|
|
49814
|
+
}
|
|
49815
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, dirName, global = false }) {
|
|
49816
|
+
const settablePaths = CrushSkill.getSettablePaths({ global });
|
|
49817
|
+
return new CrushSkill({
|
|
49818
|
+
outputRoot,
|
|
49819
|
+
relativeDirPath: relativeDirPath ?? settablePaths.relativeDirPath,
|
|
49820
|
+
dirName,
|
|
49821
|
+
frontmatter: {
|
|
49822
|
+
name: "",
|
|
49823
|
+
description: ""
|
|
49824
|
+
},
|
|
49825
|
+
body: "",
|
|
49826
|
+
otherFiles: [],
|
|
49827
|
+
validate: false,
|
|
49828
|
+
global
|
|
49829
|
+
});
|
|
49830
|
+
}
|
|
49831
|
+
};
|
|
49832
|
+
//#endregion
|
|
48912
49833
|
//#region src/features/skills/cursor-skill.ts
|
|
48913
49834
|
const CursorSkillFrontmatterSchema = z.looseObject({
|
|
48914
49835
|
name: z.string(),
|
|
@@ -49073,9 +49994,9 @@ const DeepagentsSkillFrontmatterSchema = z.looseObject({
|
|
|
49073
49994
|
name: z.string(),
|
|
49074
49995
|
description: z.string(),
|
|
49075
49996
|
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
|
|
49076
|
-
license: z.optional(z.
|
|
49077
|
-
compatibility: z.optional(z.
|
|
49078
|
-
metadata: z.optional(z.
|
|
49997
|
+
license: z.optional(z.unknown()),
|
|
49998
|
+
compatibility: z.optional(z.unknown()),
|
|
49999
|
+
metadata: z.optional(z.unknown())
|
|
49079
50000
|
});
|
|
49080
50001
|
var DeepagentsSkill = class DeepagentsSkill extends ToolSkill {
|
|
49081
50002
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DEEPAGENTS_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -50133,9 +51054,9 @@ var JunieSkill = class JunieSkill extends ToolSkill {
|
|
|
50133
51054
|
const KiloSkillFrontmatterSchema = z.looseObject({
|
|
50134
51055
|
name: z.string(),
|
|
50135
51056
|
description: z.string(),
|
|
50136
|
-
license: z.optional(z.
|
|
50137
|
-
compatibility: z.optional(z.
|
|
50138
|
-
metadata: z.optional(z.
|
|
51057
|
+
license: z.optional(z.unknown()),
|
|
51058
|
+
compatibility: z.optional(z.unknown()),
|
|
51059
|
+
metadata: z.optional(z.unknown()),
|
|
50139
51060
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
50140
51061
|
});
|
|
50141
51062
|
var KiloSkill = class KiloSkill extends ToolSkill {
|
|
@@ -50572,10 +51493,11 @@ var KiroSkill = class KiroSkill extends ToolSkill {
|
|
|
50572
51493
|
rootFrontmatter: rulesyncFrontmatter,
|
|
50573
51494
|
section: kiroSection
|
|
50574
51495
|
});
|
|
51496
|
+
const { name: _sectionName, description: _sectionDescription, ...section } = kiroSection ?? {};
|
|
50575
51497
|
const kiroFrontmatter = {
|
|
50576
|
-
...kiroSection,
|
|
50577
51498
|
name: rulesyncFrontmatter.name,
|
|
50578
51499
|
description: rulesyncFrontmatter.description,
|
|
51500
|
+
...section,
|
|
50579
51501
|
...license !== void 0 && { license },
|
|
50580
51502
|
...compatibility !== void 0 && { compatibility },
|
|
50581
51503
|
...metadata !== void 0 && { metadata }
|
|
@@ -50807,9 +51729,9 @@ var MusecodeSkill = class MusecodeSkill extends ToolSkill {
|
|
|
50807
51729
|
const OpenCodeSkillFrontmatterSchema = z.looseObject({
|
|
50808
51730
|
name: z.string(),
|
|
50809
51731
|
description: z.string(),
|
|
50810
|
-
license: z.optional(z.
|
|
50811
|
-
compatibility: z.optional(z.
|
|
50812
|
-
metadata: z.optional(z.
|
|
51732
|
+
license: z.optional(z.unknown()),
|
|
51733
|
+
compatibility: z.optional(z.unknown()),
|
|
51734
|
+
metadata: z.optional(z.unknown()),
|
|
50813
51735
|
"allowed-tools": z.optional(z.array(z.string()))
|
|
50814
51736
|
});
|
|
50815
51737
|
var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
|
|
@@ -52723,6 +53645,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
52723
53645
|
supportsGlobal: true
|
|
52724
53646
|
}
|
|
52725
53647
|
}],
|
|
53648
|
+
["crush", {
|
|
53649
|
+
class: CrushSkill,
|
|
53650
|
+
meta: {
|
|
53651
|
+
supportsProject: true,
|
|
53652
|
+
supportsSimulated: false,
|
|
53653
|
+
supportsGlobal: true
|
|
53654
|
+
}
|
|
53655
|
+
}],
|
|
52726
53656
|
["cursor", {
|
|
52727
53657
|
class: CursorSkill,
|
|
52728
53658
|
meta: {
|
|
@@ -60466,6 +61396,238 @@ var ClineRule = class ClineRule extends ToolRule {
|
|
|
60466
61396
|
}
|
|
60467
61397
|
};
|
|
60468
61398
|
//#endregion
|
|
61399
|
+
//#region src/features/rules/codebuddy-rule.ts
|
|
61400
|
+
/**
|
|
61401
|
+
* Frontmatter schema for CodeBuddy Code modular rules.
|
|
61402
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61403
|
+
*/
|
|
61404
|
+
const CodebuddyRuleFrontmatterSchema = z.object({
|
|
61405
|
+
description: z.optional(z.string()),
|
|
61406
|
+
paths: z.optional(z.array(z.string())),
|
|
61407
|
+
alwaysApply: z.optional(z.boolean())
|
|
61408
|
+
});
|
|
61409
|
+
/**
|
|
61410
|
+
* A universal glob (matching everything) is redundant on an Always Apply
|
|
61411
|
+
* rule and, paired with `alwaysApply: true`, is the same semantic conflict
|
|
61412
|
+
* `CursorRule.resolveCursorGlobs` avoids for Cursor: `alwaysApply` already
|
|
61413
|
+
* applies the rule everywhere, so also emitting an explicit
|
|
61414
|
+
* `paths: ["**\/*"]` is at best redundant and, on a subsequent
|
|
61415
|
+
* import/generate round-trip, misleadingly implies the rule is scoped by
|
|
61416
|
+
* path rather than always-on.
|
|
61417
|
+
*/
|
|
61418
|
+
const UNIVERSAL_PATHS = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
61419
|
+
/**
|
|
61420
|
+
* Rule generator for CodeBuddy Code, Tencent Cloud's terminal coding agent
|
|
61421
|
+
* (`@tencent-ai/codebuddy-code`). Its configuration surface mirrors Claude
|
|
61422
|
+
* Code closely.
|
|
61423
|
+
*
|
|
61424
|
+
* Rules format:
|
|
61425
|
+
* - {project}/CODEBUDDY.md (root: true), also read from {project}/.codebuddy/CODEBUDDY.md
|
|
61426
|
+
* - {project}/.codebuddy/rules/*.md (root: false, with optional
|
|
61427
|
+
* `description` / `paths` / `alwaysApply` frontmatter)
|
|
61428
|
+
* - Global: ~/.codebuddy/CODEBUDDY.md and ~/.codebuddy/rules/*.md
|
|
61429
|
+
*
|
|
61430
|
+
* @see https://www.codebuddy.ai/docs/cli/memory
|
|
61431
|
+
* @see https://www.codebuddy.ai/docs/cli/codebuddy-dir
|
|
61432
|
+
*/
|
|
61433
|
+
var CodebuddyRule = class CodebuddyRule extends ToolRule {
|
|
61434
|
+
frontmatter;
|
|
61435
|
+
body;
|
|
61436
|
+
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61437
|
+
if (global) return {
|
|
61438
|
+
root: {
|
|
61439
|
+
relativeDirPath: buildToolPath(CODEBUDDY_DIR, ".", excludeToolDir),
|
|
61440
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61441
|
+
},
|
|
61442
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61443
|
+
};
|
|
61444
|
+
return {
|
|
61445
|
+
root: {
|
|
61446
|
+
relativeDirPath: ".",
|
|
61447
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61448
|
+
},
|
|
61449
|
+
alternativeRoots: [{
|
|
61450
|
+
relativeDirPath: CODEBUDDY_DIR,
|
|
61451
|
+
relativeFilePath: CODEBUDDY_RULE_FILE_NAME
|
|
61452
|
+
}],
|
|
61453
|
+
nonRoot: { relativeDirPath: buildToolPath(CODEBUDDY_DIR, CODEBUDDY_RULES_DIR_NAME, excludeToolDir) }
|
|
61454
|
+
};
|
|
61455
|
+
}
|
|
61456
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
61457
|
+
if (rest.validate) {
|
|
61458
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61459
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
61460
|
+
}
|
|
61461
|
+
super({
|
|
61462
|
+
...rest,
|
|
61463
|
+
fileContent: rest.root ? body : CodebuddyRule.generateFileContent(body, frontmatter)
|
|
61464
|
+
});
|
|
61465
|
+
this.frontmatter = frontmatter;
|
|
61466
|
+
this.body = body;
|
|
61467
|
+
}
|
|
61468
|
+
static generateFileContent(body, frontmatter) {
|
|
61469
|
+
if (frontmatter.description === void 0 && frontmatter.paths === void 0 && frontmatter.alwaysApply === void 0) return body;
|
|
61470
|
+
return stringifyFrontmatter(body, {
|
|
61471
|
+
description: frontmatter.description,
|
|
61472
|
+
alwaysApply: frontmatter.alwaysApply,
|
|
61473
|
+
paths: frontmatter.paths
|
|
61474
|
+
});
|
|
61475
|
+
}
|
|
61476
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, relativeDirPath: overrideDirPath }) {
|
|
61477
|
+
const paths = this.getSettablePaths({ global });
|
|
61478
|
+
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61479
|
+
const rootDirPath = overrideDirPath ?? paths.root.relativeDirPath;
|
|
61480
|
+
const fileContent = await readFileContent(join(outputRoot, rootDirPath, paths.root.relativeFilePath));
|
|
61481
|
+
return new CodebuddyRule({
|
|
61482
|
+
outputRoot,
|
|
61483
|
+
relativeDirPath: rootDirPath,
|
|
61484
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61485
|
+
frontmatter: {},
|
|
61486
|
+
body: fileContent.trim(),
|
|
61487
|
+
validate,
|
|
61488
|
+
root: true
|
|
61489
|
+
});
|
|
61490
|
+
}
|
|
61491
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
|
|
61492
|
+
const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
61493
|
+
const filePath = join(outputRoot, relativePath);
|
|
61494
|
+
const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
61495
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(frontmatter);
|
|
61496
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
61497
|
+
return new CodebuddyRule({
|
|
61498
|
+
outputRoot,
|
|
61499
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61500
|
+
relativeFilePath,
|
|
61501
|
+
frontmatter: result.data,
|
|
61502
|
+
body: content.trim(),
|
|
61503
|
+
validate,
|
|
61504
|
+
root: false
|
|
61505
|
+
});
|
|
61506
|
+
}
|
|
61507
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61508
|
+
const isRoot = relativeFilePath === this.getSettablePaths({ global }).root.relativeFilePath;
|
|
61509
|
+
return new CodebuddyRule({
|
|
61510
|
+
outputRoot,
|
|
61511
|
+
relativeDirPath,
|
|
61512
|
+
relativeFilePath,
|
|
61513
|
+
frontmatter: {},
|
|
61514
|
+
body: "",
|
|
61515
|
+
validate: false,
|
|
61516
|
+
root: isRoot
|
|
61517
|
+
});
|
|
61518
|
+
}
|
|
61519
|
+
static resolveCodebuddyPaths({ paths, alwaysApply }) {
|
|
61520
|
+
if (!paths || paths.length === 0) return;
|
|
61521
|
+
if (alwaysApply && paths.every((path) => UNIVERSAL_PATHS.has(path.trim()))) return;
|
|
61522
|
+
return paths;
|
|
61523
|
+
}
|
|
61524
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61525
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
61526
|
+
const root = rulesyncFrontmatter.root ?? false;
|
|
61527
|
+
const paths = this.getSettablePaths({ global });
|
|
61528
|
+
const body = rulesyncRule.getBody();
|
|
61529
|
+
if (root) return new CodebuddyRule({
|
|
61530
|
+
outputRoot,
|
|
61531
|
+
frontmatter: {},
|
|
61532
|
+
body,
|
|
61533
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
61534
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
61535
|
+
validate,
|
|
61536
|
+
root
|
|
61537
|
+
});
|
|
61538
|
+
if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${rulesyncRule.getRelativeFilePath()}`);
|
|
61539
|
+
const codebuddyPaths = rulesyncFrontmatter.codebuddy?.paths;
|
|
61540
|
+
const globs = rulesyncFrontmatter.globs;
|
|
61541
|
+
const alwaysApply = rulesyncFrontmatter.codebuddy?.alwaysApply;
|
|
61542
|
+
const pathsValue = CodebuddyRule.resolveCodebuddyPaths({
|
|
61543
|
+
paths: codebuddyPaths ?? (globs?.length ? globs : void 0),
|
|
61544
|
+
alwaysApply: alwaysApply === true
|
|
61545
|
+
});
|
|
61546
|
+
const codebuddyFrontmatter = {
|
|
61547
|
+
description: rulesyncFrontmatter.codebuddy?.description ?? rulesyncFrontmatter.description,
|
|
61548
|
+
paths: pathsValue,
|
|
61549
|
+
alwaysApply
|
|
61550
|
+
};
|
|
61551
|
+
return new CodebuddyRule({
|
|
61552
|
+
outputRoot,
|
|
61553
|
+
frontmatter: codebuddyFrontmatter,
|
|
61554
|
+
body,
|
|
61555
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
61556
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath(),
|
|
61557
|
+
validate,
|
|
61558
|
+
root
|
|
61559
|
+
});
|
|
61560
|
+
}
|
|
61561
|
+
toRulesyncRule() {
|
|
61562
|
+
const targets = ["*"];
|
|
61563
|
+
if (this.isRoot()) {
|
|
61564
|
+
const rulesyncFrontmatter = {
|
|
61565
|
+
targets,
|
|
61566
|
+
root: true,
|
|
61567
|
+
description: this.description,
|
|
61568
|
+
globs: ["**/*"]
|
|
61569
|
+
};
|
|
61570
|
+
return new RulesyncRule({
|
|
61571
|
+
outputRoot: this.getOutputRoot(),
|
|
61572
|
+
frontmatter: rulesyncFrontmatter,
|
|
61573
|
+
body: this.body,
|
|
61574
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61575
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61576
|
+
validate: true
|
|
61577
|
+
});
|
|
61578
|
+
}
|
|
61579
|
+
const isAlways = this.frontmatter.alwaysApply === true;
|
|
61580
|
+
const sourcePaths = this.frontmatter.paths ?? [];
|
|
61581
|
+
const globs = sourcePaths.length === 0 && isAlways ? ["**/*"] : sourcePaths;
|
|
61582
|
+
const rulesyncFrontmatter = {
|
|
61583
|
+
targets,
|
|
61584
|
+
root: false,
|
|
61585
|
+
description: this.frontmatter.description,
|
|
61586
|
+
globs,
|
|
61587
|
+
...(this.frontmatter.paths !== void 0 || this.frontmatter.alwaysApply !== void 0 || this.frontmatter.description !== void 0) && { codebuddy: {
|
|
61588
|
+
paths: this.frontmatter.paths,
|
|
61589
|
+
alwaysApply: this.frontmatter.alwaysApply,
|
|
61590
|
+
description: this.frontmatter.description
|
|
61591
|
+
} }
|
|
61592
|
+
};
|
|
61593
|
+
return new RulesyncRule({
|
|
61594
|
+
outputRoot: this.getOutputRoot(),
|
|
61595
|
+
frontmatter: rulesyncFrontmatter,
|
|
61596
|
+
body: this.body,
|
|
61597
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
61598
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
61599
|
+
validate: true
|
|
61600
|
+
});
|
|
61601
|
+
}
|
|
61602
|
+
validate() {
|
|
61603
|
+
if (!this.frontmatter) return {
|
|
61604
|
+
success: true,
|
|
61605
|
+
error: null
|
|
61606
|
+
};
|
|
61607
|
+
const result = CodebuddyRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
61608
|
+
if (result.success) return {
|
|
61609
|
+
success: true,
|
|
61610
|
+
error: null
|
|
61611
|
+
};
|
|
61612
|
+
else return {
|
|
61613
|
+
success: false,
|
|
61614
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
61615
|
+
};
|
|
61616
|
+
}
|
|
61617
|
+
getFrontmatter() {
|
|
61618
|
+
return this.frontmatter;
|
|
61619
|
+
}
|
|
61620
|
+
getBody() {
|
|
61621
|
+
return this.body;
|
|
61622
|
+
}
|
|
61623
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
61624
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
61625
|
+
rulesyncRule,
|
|
61626
|
+
toolTarget: "codebuddy"
|
|
61627
|
+
});
|
|
61628
|
+
}
|
|
61629
|
+
};
|
|
61630
|
+
//#endregion
|
|
60469
61631
|
//#region src/features/rules/codexcli-rule.ts
|
|
60470
61632
|
var CodexcliRule = class CodexcliRule extends ToolRule {
|
|
60471
61633
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -60762,6 +61924,79 @@ var CopilotcliRule = class CopilotcliRule extends CopilotRule {
|
|
|
60762
61924
|
}
|
|
60763
61925
|
};
|
|
60764
61926
|
//#endregion
|
|
61927
|
+
//#region src/features/rules/crush-rule.ts
|
|
61928
|
+
var CrushRule = class CrushRule extends ToolRule {
|
|
61929
|
+
constructor({ fileContent, root, ...rest }) {
|
|
61930
|
+
super({
|
|
61931
|
+
...rest,
|
|
61932
|
+
fileContent,
|
|
61933
|
+
root: root ?? false
|
|
61934
|
+
});
|
|
61935
|
+
}
|
|
61936
|
+
static getSettablePaths({ global = false } = {}) {
|
|
61937
|
+
if (global) return { root: {
|
|
61938
|
+
relativeDirPath: CRUSH_GLOBAL_DIR,
|
|
61939
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
61940
|
+
} };
|
|
61941
|
+
return { root: {
|
|
61942
|
+
relativeDirPath: ".",
|
|
61943
|
+
relativeFilePath: CRUSH_RULE_FILE_NAME
|
|
61944
|
+
} };
|
|
61945
|
+
}
|
|
61946
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
61947
|
+
const { root } = this.getSettablePaths({ global });
|
|
61948
|
+
const relativePath = join(root.relativeDirPath, root.relativeFilePath);
|
|
61949
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
61950
|
+
return new CrushRule({
|
|
61951
|
+
outputRoot,
|
|
61952
|
+
relativeDirPath: root.relativeDirPath,
|
|
61953
|
+
relativeFilePath: root.relativeFilePath,
|
|
61954
|
+
fileContent,
|
|
61955
|
+
validate,
|
|
61956
|
+
root: true
|
|
61957
|
+
});
|
|
61958
|
+
}
|
|
61959
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
61960
|
+
const { root } = this.getSettablePaths({ global });
|
|
61961
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
61962
|
+
return new CrushRule({
|
|
61963
|
+
outputRoot,
|
|
61964
|
+
relativeDirPath: root.relativeDirPath,
|
|
61965
|
+
relativeFilePath: root.relativeFilePath,
|
|
61966
|
+
fileContent: rulesyncRule.getBody(),
|
|
61967
|
+
validate,
|
|
61968
|
+
root: isRoot
|
|
61969
|
+
});
|
|
61970
|
+
}
|
|
61971
|
+
toRulesyncRule() {
|
|
61972
|
+
return this.toRulesyncRuleDefault();
|
|
61973
|
+
}
|
|
61974
|
+
validate() {
|
|
61975
|
+
return {
|
|
61976
|
+
success: true,
|
|
61977
|
+
error: null
|
|
61978
|
+
};
|
|
61979
|
+
}
|
|
61980
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61981
|
+
const { root } = this.getSettablePaths({ global });
|
|
61982
|
+
const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
|
|
61983
|
+
return new CrushRule({
|
|
61984
|
+
outputRoot,
|
|
61985
|
+
relativeDirPath,
|
|
61986
|
+
relativeFilePath,
|
|
61987
|
+
fileContent: "",
|
|
61988
|
+
validate: false,
|
|
61989
|
+
root: isRoot
|
|
61990
|
+
});
|
|
61991
|
+
}
|
|
61992
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
61993
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
61994
|
+
rulesyncRule,
|
|
61995
|
+
toolTarget: "crush"
|
|
61996
|
+
});
|
|
61997
|
+
}
|
|
61998
|
+
};
|
|
61999
|
+
//#endregion
|
|
60765
62000
|
//#region src/features/rules/cursor-rule.ts
|
|
60766
62001
|
const CursorRuleFrontmatterSchema = z.object({
|
|
60767
62002
|
description: z.optional(z.string()),
|
|
@@ -61369,13 +62604,34 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
61369
62604
|
};
|
|
61370
62605
|
//#endregion
|
|
61371
62606
|
//#region src/features/rules/factorydroid-rule.ts
|
|
62607
|
+
/**
|
|
62608
|
+
* Rule generator for Factory Droid.
|
|
62609
|
+
*
|
|
62610
|
+
* Factory Droid loads the root `AGENTS.md` (project) / `~/.factory/AGENTS.md`
|
|
62611
|
+
* (global) as coding guidelines, plus non-root rules referenced from it via
|
|
62612
|
+
* `.factory/rules/*.md`.
|
|
62613
|
+
*
|
|
62614
|
+
* Factory Droid also loads `DESIGN.md` (project only) as a second,
|
|
62615
|
+
* independent instruction surface: "Always-on design-system, UX, visual, and
|
|
62616
|
+
* interaction guidance", loaded separately from `AGENTS.md`'s coding
|
|
62617
|
+
* guidelines. Rulesync emits it from any non-root rule that opts in via a
|
|
62618
|
+
* `factorydroid.channel: design` frontmatter block — those rule bodies are
|
|
62619
|
+
* routed to `DESIGN.md` instead of `AGENTS.md`/`.factory/rules/*.md`, and
|
|
62620
|
+
* multiple opted-in rules concatenate in source order. Factory's docs describe
|
|
62621
|
+
* `DESIGN.md` at the repository root and in nested subdirectories, like
|
|
62622
|
+
* `AGENTS.md`, but document no personal/global home-directory equivalent, so
|
|
62623
|
+
* this channel is project scope only.
|
|
62624
|
+
* @see https://docs.factory.ai/cli/configuration/agents-md
|
|
62625
|
+
*/
|
|
61372
62626
|
var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
61373
|
-
|
|
62627
|
+
design;
|
|
62628
|
+
constructor({ fileContent, root, design = false, ...rest }) {
|
|
61374
62629
|
super({
|
|
61375
62630
|
...rest,
|
|
61376
62631
|
fileContent,
|
|
61377
62632
|
root: root ?? false
|
|
61378
62633
|
});
|
|
62634
|
+
this.design = design;
|
|
61379
62635
|
}
|
|
61380
62636
|
static getSettablePaths({ global, excludeToolDir } = {}) {
|
|
61381
62637
|
if (global) return { root: {
|
|
@@ -61387,11 +62643,47 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61387
62643
|
relativeDirPath: ".",
|
|
61388
62644
|
relativeFilePath: FACTORYDROID_RULE_FILE_NAME
|
|
61389
62645
|
},
|
|
61390
|
-
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) }
|
|
62646
|
+
nonRoot: { relativeDirPath: buildToolPath(FACTORYDROID_DIR, "rules", excludeToolDir) },
|
|
62647
|
+
design: {
|
|
62648
|
+
relativeDirPath: ".",
|
|
62649
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME
|
|
62650
|
+
}
|
|
61391
62651
|
};
|
|
61392
62652
|
}
|
|
61393
|
-
|
|
62653
|
+
/**
|
|
62654
|
+
* Extra fixed files this tool manages beyond the root/non-root rules. The
|
|
62655
|
+
* RulesProcessor enumerates these for import and deletion so a stale
|
|
62656
|
+
* `DESIGN.md` is cleaned up once no rule opts in anymore. Empty in global
|
|
62657
|
+
* mode: `DESIGN.md` has no documented home-directory equivalent.
|
|
62658
|
+
*/
|
|
62659
|
+
static getExtraFixedFiles({ global = false } = {}) {
|
|
62660
|
+
if (global) return [];
|
|
62661
|
+
return [this.getSettablePaths({ global }).design];
|
|
62662
|
+
}
|
|
62663
|
+
/**
|
|
62664
|
+
* Factory Droid loads `DESIGN.md` itself, so listing it in the root rule's
|
|
62665
|
+
* TOON reference section would double-load the content (and misrepresent it
|
|
62666
|
+
* as a rule the model must remember to open).
|
|
62667
|
+
*/
|
|
62668
|
+
isExcludedFromRootReferences() {
|
|
62669
|
+
return this.design;
|
|
62670
|
+
}
|
|
62671
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true, global = false }) {
|
|
61394
62672
|
const paths = this.getSettablePaths({ global });
|
|
62673
|
+
const design = !global ? paths.design : void 0;
|
|
62674
|
+
if (design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath) {
|
|
62675
|
+
const relativePath = join(design.relativeDirPath, design.relativeFilePath);
|
|
62676
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
62677
|
+
return new FactorydroidRule({
|
|
62678
|
+
outputRoot,
|
|
62679
|
+
relativeDirPath: design.relativeDirPath,
|
|
62680
|
+
relativeFilePath: design.relativeFilePath,
|
|
62681
|
+
fileContent,
|
|
62682
|
+
validate,
|
|
62683
|
+
root: false,
|
|
62684
|
+
design: true
|
|
62685
|
+
});
|
|
62686
|
+
}
|
|
61395
62687
|
if (relativeFilePath === paths.root.relativeFilePath) {
|
|
61396
62688
|
const relativePath = join(paths.root.relativeDirPath, paths.root.relativeFilePath);
|
|
61397
62689
|
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
@@ -61418,18 +62710,34 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61418
62710
|
}
|
|
61419
62711
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
61420
62712
|
const paths = this.getSettablePaths({ global });
|
|
61421
|
-
const
|
|
62713
|
+
const design = !global ? paths.design : void 0;
|
|
62714
|
+
const isDesign = design !== void 0 && relativeDirPath === design.relativeDirPath && relativeFilePath === design.relativeFilePath;
|
|
62715
|
+
const isRoot = !isDesign && relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
61422
62716
|
return new FactorydroidRule({
|
|
61423
62717
|
outputRoot,
|
|
61424
62718
|
relativeDirPath,
|
|
61425
62719
|
relativeFilePath,
|
|
61426
62720
|
fileContent: "",
|
|
61427
62721
|
validate: false,
|
|
61428
|
-
root: isRoot
|
|
62722
|
+
root: isRoot,
|
|
62723
|
+
design: isDesign
|
|
61429
62724
|
});
|
|
61430
62725
|
}
|
|
61431
62726
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
62727
|
+
const frontmatter = rulesyncRule.getFrontmatter();
|
|
61432
62728
|
const paths = this.getSettablePaths({ global });
|
|
62729
|
+
if (!global && !frontmatter.root && frontmatter.factorydroid?.channel === "design") {
|
|
62730
|
+
const { design } = paths;
|
|
62731
|
+
return new FactorydroidRule({
|
|
62732
|
+
outputRoot,
|
|
62733
|
+
relativeDirPath: design.relativeDirPath,
|
|
62734
|
+
relativeFilePath: design.relativeFilePath,
|
|
62735
|
+
fileContent: rulesyncRule.getBody(),
|
|
62736
|
+
validate,
|
|
62737
|
+
root: false,
|
|
62738
|
+
design: true
|
|
62739
|
+
});
|
|
62740
|
+
}
|
|
61433
62741
|
return new FactorydroidRule(this.buildToolRuleParamsAgentsmd({
|
|
61434
62742
|
outputRoot,
|
|
61435
62743
|
rulesyncRule,
|
|
@@ -61439,6 +62747,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
61439
62747
|
}));
|
|
61440
62748
|
}
|
|
61441
62749
|
toRulesyncRule() {
|
|
62750
|
+
if (this.design) return new RulesyncRule({
|
|
62751
|
+
outputRoot: process.cwd(),
|
|
62752
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
62753
|
+
relativeFilePath: FACTORYDROID_DESIGN_FILE_NAME,
|
|
62754
|
+
frontmatter: {
|
|
62755
|
+
root: false,
|
|
62756
|
+
targets: ["factorydroid"],
|
|
62757
|
+
factorydroid: { channel: "design" }
|
|
62758
|
+
},
|
|
62759
|
+
body: this.getFileContent()
|
|
62760
|
+
});
|
|
61442
62761
|
return this.toRulesyncRuleDefault();
|
|
61443
62762
|
}
|
|
61444
62763
|
validate() {
|
|
@@ -63956,6 +65275,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63956
65275
|
ruleDiscoveryMode: "auto"
|
|
63957
65276
|
}
|
|
63958
65277
|
}],
|
|
65278
|
+
["codebuddy", {
|
|
65279
|
+
class: CodebuddyRule,
|
|
65280
|
+
meta: {
|
|
65281
|
+
extension: "md",
|
|
65282
|
+
supportsGlobal: true,
|
|
65283
|
+
ruleDiscoveryMode: "auto",
|
|
65284
|
+
localRootMode: "separate-local-file",
|
|
65285
|
+
localRootFileName: CODEBUDDY_LOCAL_RULE_FILE_NAME
|
|
65286
|
+
}
|
|
65287
|
+
}],
|
|
63959
65288
|
["codexcli", {
|
|
63960
65289
|
class: CodexcliRule,
|
|
63961
65290
|
meta: {
|
|
@@ -63981,6 +65310,15 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
63981
65310
|
ruleDiscoveryMode: "auto"
|
|
63982
65311
|
}
|
|
63983
65312
|
}],
|
|
65313
|
+
["crush", {
|
|
65314
|
+
class: CrushRule,
|
|
65315
|
+
meta: {
|
|
65316
|
+
extension: "md",
|
|
65317
|
+
supportsGlobal: true,
|
|
65318
|
+
ruleDiscoveryMode: "auto",
|
|
65319
|
+
collisionPolicy: "fold"
|
|
65320
|
+
}
|
|
65321
|
+
}],
|
|
63984
65322
|
["cursor", {
|
|
63985
65323
|
class: CursorRule,
|
|
63986
65324
|
meta: {
|
|
@@ -64349,6 +65687,10 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64349
65687
|
outputFiles,
|
|
64350
65688
|
convertedRules
|
|
64351
65689
|
});
|
|
65690
|
+
await this.warnForDeactivatedImportOnlyRoots({
|
|
65691
|
+
toolRules,
|
|
65692
|
+
factory
|
|
65693
|
+
});
|
|
64352
65694
|
return outputFiles;
|
|
64353
65695
|
}
|
|
64354
65696
|
/**
|
|
@@ -64570,6 +65912,41 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
64570
65912
|
}
|
|
64571
65913
|
}
|
|
64572
65914
|
/**
|
|
65915
|
+
* Warn when this generate run is about to write a root rule file that will
|
|
65916
|
+
* make the tool stop reading paths it currently reads instead — Junie's
|
|
65917
|
+
* `.junie/rules/*.md` and `.junie/playbook.md` become unreachable the
|
|
65918
|
+
* moment `.junie/AGENTS.md` exists, since Junie reads the root file
|
|
65919
|
+
* exclusively once it is present. `importOnlyRoots` with
|
|
65920
|
+
* `onlyWhenRootAbsent` already models exactly this shape for import; this
|
|
65921
|
+
* reuses the same declaration so the
|
|
65922
|
+
* `generate` path — which never calls `loadToolFiles` and so never reached
|
|
65923
|
+
* the existing import-side warning — surfaces it too. Without this, a repo
|
|
65924
|
+
* that only ever runs `generate` never sees any warning: the deactivated
|
|
65925
|
+
* files stay on disk, untouched and not gitignored, silently unread.
|
|
65926
|
+
*/
|
|
65927
|
+
async warnForDeactivatedImportOnlyRoots({ toolRules, factory }) {
|
|
65928
|
+
const rootRule = toolRules.find((rule) => rule.isRoot());
|
|
65929
|
+
if (!rootRule) return;
|
|
65930
|
+
const settablePaths = factory.class.getSettablePaths({ global: this.global });
|
|
65931
|
+
const importOnlyRoots = "importOnlyRoots" in settablePaths ? settablePaths.importOnlyRoots : void 0;
|
|
65932
|
+
if (!importOnlyRoots || importOnlyRoots.length === 0) return;
|
|
65933
|
+
const existingPaths = [];
|
|
65934
|
+
for (const importOnlyRoot of importOnlyRoots) {
|
|
65935
|
+
if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
|
|
65936
|
+
const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
|
|
65937
|
+
cwd: this.outputRoot,
|
|
65938
|
+
type: "file"
|
|
65939
|
+
});
|
|
65940
|
+
existingPaths.push(...matchedPaths);
|
|
65941
|
+
}
|
|
65942
|
+
if (existingPaths.length === 0) return;
|
|
65943
|
+
const rootFileRelativePath = join(rootRule.getRelativeDirPath(), rootRule.getRelativeFilePath());
|
|
65944
|
+
const names = existingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
|
|
65945
|
+
const listedNames = names.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
|
|
65946
|
+
const remainingCount = names.length - listedNames.length;
|
|
65947
|
+
this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
|
|
65948
|
+
}
|
|
65949
|
+
/**
|
|
64573
65950
|
* Handle localRoot rule generation based on tool target.
|
|
64574
65951
|
* - `separate-local-file`: writes a dedicated `*.local.md` root file
|
|
64575
65952
|
* (claudecode/legacy: `./CLAUDE.local.md`, rovodev: `./AGENTS.local.md`)
|
|
@@ -64821,6 +66198,26 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64821
66198
|
}));
|
|
64822
66199
|
}
|
|
64823
66200
|
/**
|
|
66201
|
+
* Load and merge rulesync rule files from every configured input root's
|
|
66202
|
+
* `.rulesync/rules/` directory, by relative path, so that a rule with the
|
|
66203
|
+
* same target path from a later root replaces the earlier root's copy
|
|
66204
|
+
* (case-insensitive, matching the intra-root collision handling).
|
|
66205
|
+
*
|
|
66206
|
+
* This is the side-effect-free half of `loadRulesyncFiles`: it does not
|
|
66207
|
+
* warn about a missing root rule or validate `localRoot` placement, so it
|
|
66208
|
+
* is also safe to call from code paths — like
|
|
66209
|
+
* `warnForFoldImportDuplicationRisk` — that only need the merged rule set
|
|
66210
|
+
* and must not trigger `loadRulesyncFiles`'s generate-time checks.
|
|
66211
|
+
*/
|
|
66212
|
+
async loadMergedRulesyncRules() {
|
|
66213
|
+
return mergeByCaseInsensitiveIdentity({
|
|
66214
|
+
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
66215
|
+
identity: (rule) => rule.getRelativeFilePath(),
|
|
66216
|
+
artifactName: "rule",
|
|
66217
|
+
logger: this.logger
|
|
66218
|
+
});
|
|
66219
|
+
}
|
|
66220
|
+
/**
|
|
64824
66221
|
* Implementation of abstract method from FeatureProcessor
|
|
64825
66222
|
* Load and parse rulesync rule files from every configured input root's
|
|
64826
66223
|
* `.rulesync/rules/` directory, merging by relative path so that a rule
|
|
@@ -64828,12 +66225,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64828
66225
|
* copy (case-insensitive, matching the intra-root collision handling).
|
|
64829
66226
|
*/
|
|
64830
66227
|
async loadRulesyncFiles() {
|
|
64831
|
-
const rulesyncRules =
|
|
64832
|
-
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
64833
|
-
identity: (rule) => rule.getRelativeFilePath(),
|
|
64834
|
-
artifactName: "rule",
|
|
64835
|
-
logger: this.logger
|
|
64836
|
-
});
|
|
66228
|
+
const rulesyncRules = await this.loadMergedRulesyncRules();
|
|
64837
66229
|
const factory = this.getFactory(this.toolTarget);
|
|
64838
66230
|
const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
|
|
64839
66231
|
if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
|
|
@@ -64890,6 +66282,46 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
64890
66282
|
});
|
|
64891
66283
|
}
|
|
64892
66284
|
/**
|
|
66285
|
+
* Warn when importing a `collisionPolicy: "fold"` target's root file while
|
|
66286
|
+
* `.rulesync/rules/` still holds non-root rules targeting it. A fold target
|
|
66287
|
+
* (codexcli, junie, and others) concatenates every targeted non-root rule
|
|
66288
|
+
* into its one root output file on `generate`. Importing that root file
|
|
66289
|
+
* back therefore re-reads the already-folded content as a single new
|
|
66290
|
+
* rulesync rule, while the original non-root rules stay in place
|
|
66291
|
+
* untouched — the next `generate` folds both together, duplicating the
|
|
66292
|
+
* content once per generate/import cycle with nothing to indicate why.
|
|
66293
|
+
*
|
|
66294
|
+
* This does not attempt to detect or drop the specific duplicated content
|
|
66295
|
+
* (the root file has no marker recording which rule contributed what); it
|
|
66296
|
+
* only surfaces that the cycle produces one, per the "at minimum, warn"
|
|
66297
|
+
* option recorded on issue #2743.
|
|
66298
|
+
*
|
|
66299
|
+
* Only the actual `rulesync import` call site invokes this (and only once
|
|
66300
|
+
* it has confirmed there is something to import) — `loadToolFiles` is also
|
|
66301
|
+
* the entry point for `rulesync convert` and `rulesync fetch`, neither of
|
|
66302
|
+
* which writes to `.rulesync/rules/` or carries this duplication risk.
|
|
66303
|
+
*
|
|
66304
|
+
* Reads via `loadMergedRulesyncRules` rather than `loadRulesyncFiles`
|
|
66305
|
+
* deliberately: this runs before the imported root file is written, so
|
|
66306
|
+
* `.rulesync/rules/` never yet has a root rule targeting this tool, and
|
|
66307
|
+
* `loadRulesyncFiles`'s "no root rule found" warning and `localRoot`
|
|
66308
|
+
* validation (which can throw) would fire spuriously on every fold-tool
|
|
66309
|
+
* import — including ones where nothing is actually misconfigured.
|
|
66310
|
+
*
|
|
66311
|
+
* In global mode, a `localRoot: true` rule is excluded from the
|
|
66312
|
+
* duplication check the same way `loadRulesyncFiles`'s global-mode branch
|
|
66313
|
+
* excludes it from `nonRootRules`: `generate` ignores `localRoot` entirely
|
|
66314
|
+
* in global mode, so such a rule is never actually folded into the global
|
|
66315
|
+
* root output and warning about it here would be inaccurate.
|
|
66316
|
+
*/
|
|
66317
|
+
async warnForFoldImportDuplicationRisk() {
|
|
66318
|
+
const factory = this.getFactory(this.toolTarget);
|
|
66319
|
+
if (factory.meta.collisionPolicy !== "fold") return;
|
|
66320
|
+
const nonRootRules = (await this.loadMergedRulesyncRules()).filter((rule) => !rule.getFrontmatter().root && (!this.global || !rule.getFrontmatter().localRoot) && factory.class.isTargetedByRulesyncRule(rule));
|
|
66321
|
+
if (nonRootRules.length === 0) return;
|
|
66322
|
+
this.logger.warn(`Importing ${this.toolTarget}'s root file will re-add content already folded from ${formatRulePaths(nonRootRules)}: ${this.toolTarget} concatenates every non-root rule into its single root output file, so the imported copy duplicates them the next time you run \`rulesync generate --targets ${this.toolTarget}\`. Review the imported rule and remove the duplicated content, or remove the original non-root rule files, before generating again.`);
|
|
66323
|
+
}
|
|
66324
|
+
/**
|
|
64893
66325
|
* Implementation of abstract method from FeatureProcessor
|
|
64894
66326
|
* Load tool-specific rule configurations and parse them into ToolRule instances
|
|
64895
66327
|
*/
|
|
@@ -66953,6 +68385,7 @@ async function importRulesCore(params) {
|
|
|
66953
68385
|
logger.warn(`No rule files found for ${tool}. Skipping import.`);
|
|
66954
68386
|
return 0;
|
|
66955
68387
|
}
|
|
68388
|
+
await rulesProcessor.warnForFoldImportDuplicationRisk();
|
|
66956
68389
|
const rulesyncFiles = await rulesProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
66957
68390
|
const { count: writtenCount } = await rulesProcessor.writeAiFiles(rulesyncFiles);
|
|
66958
68391
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} rule files`);
|
|
@@ -67200,6 +68633,6 @@ async function importChecksCore(params) {
|
|
|
67200
68633
|
return writtenCount;
|
|
67201
68634
|
}
|
|
67202
68635
|
//#endregion
|
|
67203
|
-
export {
|
|
68636
|
+
export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
|
|
67204
68637
|
|
|
67205
|
-
//# sourceMappingURL=import-
|
|
68638
|
+
//# sourceMappingURL=import-BKqbq4Ut.js.map
|