@csark0812/skeleton 1.5.6 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1901 -1284
- package/dist/hooks/customize-on-skill-read.js +144 -23
- package/dist/plugin-types.js +4 -3
- package/package.json +6 -2
package/dist/cli.js
CHANGED
|
@@ -3037,6 +3037,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3037
3037
|
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3038
3038
|
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3039
3039
|
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3040
|
+
var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/?]$/iu);
|
|
3040
3041
|
function stringArrayToHexStripped(input) {
|
|
3041
3042
|
let acc = "";
|
|
3042
3043
|
let code = 0;
|
|
@@ -3180,7 +3181,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3180
3181
|
continue;
|
|
3181
3182
|
}
|
|
3182
3183
|
} else if (input[0] === "/") {
|
|
3183
|
-
if (input[1] === "."
|
|
3184
|
+
if (input[1] === ".") {
|
|
3184
3185
|
output.push("/");
|
|
3185
3186
|
break;
|
|
3186
3187
|
}
|
|
@@ -3262,10 +3263,30 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3262
3263
|
}
|
|
3263
3264
|
return output;
|
|
3264
3265
|
}
|
|
3266
|
+
var BYTE_HEX = new Array(256);
|
|
3267
|
+
{
|
|
3268
|
+
const HEX_DIGITS = "0123456789ABCDEF";
|
|
3269
|
+
for (let i = 0;i < 256; i++) {
|
|
3270
|
+
BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
function isEscapeSafe(cp) {
|
|
3274
|
+
return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95;
|
|
3275
|
+
}
|
|
3276
|
+
function percentEncodeNonAscii(cp) {
|
|
3277
|
+
if (cp < 2048) {
|
|
3278
|
+
return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63];
|
|
3279
|
+
}
|
|
3280
|
+
if (cp < 65536) {
|
|
3281
|
+
return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
|
|
3282
|
+
}
|
|
3283
|
+
return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
|
|
3284
|
+
}
|
|
3265
3285
|
function normalizePathEncoding(input) {
|
|
3266
3286
|
let output = "";
|
|
3267
3287
|
for (let i = 0;i < input.length; i++) {
|
|
3268
|
-
|
|
3288
|
+
const ch = input[i];
|
|
3289
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3269
3290
|
const hex = input.slice(i + 1, i + 3);
|
|
3270
3291
|
if (isHexPair(hex)) {
|
|
3271
3292
|
const normalizedHex = hex.toUpperCase();
|
|
@@ -3279,10 +3300,66 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3279
3300
|
continue;
|
|
3280
3301
|
}
|
|
3281
3302
|
}
|
|
3282
|
-
if (isPathCharacter(
|
|
3283
|
-
output +=
|
|
3303
|
+
if (isPathCharacter(ch)) {
|
|
3304
|
+
output += ch;
|
|
3305
|
+
} else {
|
|
3306
|
+
const code = input.charCodeAt(i);
|
|
3307
|
+
if (code < 128) {
|
|
3308
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
|
|
3309
|
+
} else if (code < 55296 || code > 57343) {
|
|
3310
|
+
output += percentEncodeNonAscii(code);
|
|
3311
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3312
|
+
const low = input.charCodeAt(i + 1);
|
|
3313
|
+
if (low >= 56320 && low <= 57343) {
|
|
3314
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3315
|
+
i++;
|
|
3316
|
+
} else {
|
|
3317
|
+
output += percentEncodeNonAscii(65533);
|
|
3318
|
+
}
|
|
3319
|
+
} else {
|
|
3320
|
+
output += percentEncodeNonAscii(65533);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return output;
|
|
3325
|
+
}
|
|
3326
|
+
function normalizeQueryFragmentEncoding(input) {
|
|
3327
|
+
let output = "";
|
|
3328
|
+
for (let i = 0;i < input.length; i++) {
|
|
3329
|
+
const ch = input[i];
|
|
3330
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3331
|
+
const hex = input.slice(i + 1, i + 3);
|
|
3332
|
+
if (isHexPair(hex)) {
|
|
3333
|
+
const normalizedHex = hex.toUpperCase();
|
|
3334
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3335
|
+
if (isUnreserved(decoded)) {
|
|
3336
|
+
output += decoded;
|
|
3337
|
+
} else {
|
|
3338
|
+
output += "%" + normalizedHex;
|
|
3339
|
+
}
|
|
3340
|
+
i += 2;
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (isQueryFragmentCharacter(ch)) {
|
|
3345
|
+
output += ch;
|
|
3284
3346
|
} else {
|
|
3285
|
-
|
|
3347
|
+
const code = input.charCodeAt(i);
|
|
3348
|
+
if (code < 128) {
|
|
3349
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
|
|
3350
|
+
} else if (code < 55296 || code > 57343) {
|
|
3351
|
+
output += percentEncodeNonAscii(code);
|
|
3352
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3353
|
+
const low = input.charCodeAt(i + 1);
|
|
3354
|
+
if (low >= 56320 && low <= 57343) {
|
|
3355
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3356
|
+
i++;
|
|
3357
|
+
} else {
|
|
3358
|
+
output += percentEncodeNonAscii(65533);
|
|
3359
|
+
}
|
|
3360
|
+
} else {
|
|
3361
|
+
output += percentEncodeNonAscii(65533);
|
|
3362
|
+
}
|
|
3286
3363
|
}
|
|
3287
3364
|
}
|
|
3288
3365
|
return output;
|
|
@@ -3290,7 +3367,8 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3290
3367
|
function escapePreservingEscapes(input) {
|
|
3291
3368
|
let output = "";
|
|
3292
3369
|
for (let i = 0;i < input.length; i++) {
|
|
3293
|
-
|
|
3370
|
+
const ch = input[i];
|
|
3371
|
+
if (ch === "%" && i + 2 < input.length) {
|
|
3294
3372
|
const hex = input.slice(i + 1, i + 3);
|
|
3295
3373
|
if (isHexPair(hex)) {
|
|
3296
3374
|
output += "%" + hex.toUpperCase();
|
|
@@ -3298,7 +3376,22 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3298
3376
|
continue;
|
|
3299
3377
|
}
|
|
3300
3378
|
}
|
|
3301
|
-
|
|
3379
|
+
const code = input.charCodeAt(i);
|
|
3380
|
+
if (code < 128) {
|
|
3381
|
+
output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
|
|
3382
|
+
} else if (code < 55296 || code > 57343) {
|
|
3383
|
+
output += percentEncodeNonAscii(code);
|
|
3384
|
+
} else if (code <= 56319 && i + 1 < input.length) {
|
|
3385
|
+
const low = input.charCodeAt(i + 1);
|
|
3386
|
+
if (low >= 56320 && low <= 57343) {
|
|
3387
|
+
output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
|
|
3388
|
+
i++;
|
|
3389
|
+
} else {
|
|
3390
|
+
output += percentEncodeNonAscii(65533);
|
|
3391
|
+
}
|
|
3392
|
+
} else {
|
|
3393
|
+
output += percentEncodeNonAscii(65533);
|
|
3394
|
+
}
|
|
3302
3395
|
}
|
|
3303
3396
|
return output;
|
|
3304
3397
|
}
|
|
@@ -3332,6 +3425,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
3332
3425
|
reescapeHostDelimiters,
|
|
3333
3426
|
normalizePercentEncoding,
|
|
3334
3427
|
normalizePathEncoding,
|
|
3428
|
+
normalizeQueryFragmentEncoding,
|
|
3335
3429
|
escapePreservingEscapes,
|
|
3336
3430
|
removeDotSegments,
|
|
3337
3431
|
isIPv4,
|
|
@@ -3517,7 +3611,7 @@ var require_schemes = __commonJS((exports, module) => {
|
|
|
3517
3611
|
|
|
3518
3612
|
// node_modules/fast-uri/index.js
|
|
3519
3613
|
var require_fast_uri = __commonJS((exports, module) => {
|
|
3520
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3614
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3521
3615
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3522
3616
|
function normalize(uri, options) {
|
|
3523
3617
|
if (typeof uri === "string") {
|
|
@@ -3529,7 +3623,12 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3529
3623
|
}
|
|
3530
3624
|
function resolve(baseURI, relativeURI, options) {
|
|
3531
3625
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3532
|
-
const
|
|
3626
|
+
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
|
|
3627
|
+
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
|
|
3628
|
+
if (baseMalformed || relativeMalformed) {
|
|
3629
|
+
throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
|
|
3630
|
+
}
|
|
3631
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
|
|
3533
3632
|
schemelessOptions.skipEscape = true;
|
|
3534
3633
|
return serialize(resolved, schemelessOptions);
|
|
3535
3634
|
}
|
|
@@ -3655,6 +3754,8 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3655
3754
|
return uriTokens.join("");
|
|
3656
3755
|
}
|
|
3657
3756
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3757
|
+
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3758
|
+
var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
|
|
3658
3759
|
function getParseError(parsed, matches) {
|
|
3659
3760
|
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
|
|
3660
3761
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3684,9 +3785,28 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3684
3785
|
uri = "//" + uri;
|
|
3685
3786
|
}
|
|
3686
3787
|
}
|
|
3788
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX);
|
|
3789
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
|
|
3790
|
+
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3791
|
+
malformedAuthorityOrPort = true;
|
|
3792
|
+
}
|
|
3793
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
|
|
3794
|
+
if (introducerMatch !== null) {
|
|
3795
|
+
const region = introducerMatch[1];
|
|
3796
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, "");
|
|
3797
|
+
if (normalizedRegion.length >= 2) {
|
|
3798
|
+
if (normalizedRegion.slice(0, 2) !== "//") {
|
|
3799
|
+
parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
|
|
3800
|
+
malformedAuthorityOrPort = true;
|
|
3801
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
3802
|
+
parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
|
|
3803
|
+
malformedAuthorityOrPort = true;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3687
3807
|
const matches = uri.match(URI_PARSE);
|
|
3688
3808
|
if (matches) {
|
|
3689
|
-
parsed.scheme = matches[1];
|
|
3809
|
+
parsed.scheme = matches[1] === undefined ? undefined : matches[1].toLowerCase();
|
|
3690
3810
|
parsed.userinfo = matches[3];
|
|
3691
3811
|
parsed.host = matches[4];
|
|
3692
3812
|
parsed.port = parseInt(matches[5], 10);
|
|
@@ -3745,12 +3865,11 @@ var require_fast_uri = __commonJS((exports, module) => {
|
|
|
3745
3865
|
if (parsed.path) {
|
|
3746
3866
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
3747
3867
|
}
|
|
3868
|
+
if (parsed.query) {
|
|
3869
|
+
parsed.query = normalizeQueryFragmentEncoding(parsed.query);
|
|
3870
|
+
}
|
|
3748
3871
|
if (parsed.fragment) {
|
|
3749
|
-
|
|
3750
|
-
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3751
|
-
} catch {
|
|
3752
|
-
parsed.error = parsed.error || "URI malformed";
|
|
3753
|
-
}
|
|
3872
|
+
parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
|
|
3754
3873
|
}
|
|
3755
3874
|
}
|
|
3756
3875
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -15653,11 +15772,13 @@ var require_extend = __commonJS((exports, module) => {
|
|
|
15653
15772
|
|
|
15654
15773
|
// src/cli.ts
|
|
15655
15774
|
import { readFileSync as readFileSync22 } from "node:fs";
|
|
15775
|
+
import process7 from "node:process";
|
|
15656
15776
|
|
|
15657
15777
|
// src/audit/config/load.ts
|
|
15658
15778
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
15659
15779
|
import { existsSync, readFileSync } from "node:fs";
|
|
15660
15780
|
import { dirname, join } from "node:path";
|
|
15781
|
+
import process2 from "node:process";
|
|
15661
15782
|
import { fileURLToPath } from "node:url";
|
|
15662
15783
|
|
|
15663
15784
|
// node_modules/yaml/dist/index.js
|
|
@@ -15745,7 +15866,7 @@ function isExternalLink(target) {
|
|
|
15745
15866
|
return EXTERNAL_LINK_RE.test(target);
|
|
15746
15867
|
}
|
|
15747
15868
|
function isPlaceholderLink(target) {
|
|
15748
|
-
return !target.includes("/")
|
|
15869
|
+
return !(target.includes("/") || target.includes(".") || target.startsWith("#"));
|
|
15749
15870
|
}
|
|
15750
15871
|
function escapeRegexLiteral(s) {
|
|
15751
15872
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -15826,7 +15947,7 @@ var COVERAGE_BUILTIN_EXCLUDES = [
|
|
|
15826
15947
|
"**/fixtures/**",
|
|
15827
15948
|
"templates/**"
|
|
15828
15949
|
];
|
|
15829
|
-
function findRepoRoot(startDir =
|
|
15950
|
+
function findRepoRoot(startDir = process2.cwd()) {
|
|
15830
15951
|
let dir = startDir;
|
|
15831
15952
|
while (true) {
|
|
15832
15953
|
if (existsSync(join(dir, ".skeleton", "config.yaml")))
|
|
@@ -16861,6 +16982,44 @@ function assertUnderSkeleton(root, absPath) {
|
|
|
16861
16982
|
}
|
|
16862
16983
|
|
|
16863
16984
|
// src/plugins/load.ts
|
|
16985
|
+
function rulesAgree(a, b) {
|
|
16986
|
+
return a.length === b.length && !a.some((rule, i) => rule.id !== b[i]?.id || rule.run !== b[i]?.run);
|
|
16987
|
+
}
|
|
16988
|
+
function policiesAgree(a, b) {
|
|
16989
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && !a.some((p, i) => p !== b[i]);
|
|
16990
|
+
}
|
|
16991
|
+
function resolveRules(defaultRules, namedRules) {
|
|
16992
|
+
if (defaultRules !== undefined && namedRules !== undefined && !rulesAgree(defaultRules, namedRules)) {
|
|
16993
|
+
throw new Error("Plugin exports disagree on rules: default and named `rules` must match when both are set");
|
|
16994
|
+
}
|
|
16995
|
+
return defaultRules ?? namedRules;
|
|
16996
|
+
}
|
|
16997
|
+
function resolvePolicies(defaultPolicies, namedPolicies) {
|
|
16998
|
+
if (defaultPolicies !== undefined && namedPolicies !== undefined && !policiesAgree(defaultPolicies, namedPolicies)) {
|
|
16999
|
+
throw new Error("Plugin exports disagree on policies: default and named `policies` must match when both are set");
|
|
17000
|
+
}
|
|
17001
|
+
return defaultPolicies ?? namedPolicies;
|
|
17002
|
+
}
|
|
17003
|
+
function validateRules(rules) {
|
|
17004
|
+
for (const rule of rules) {
|
|
17005
|
+
if (!rule || typeof rule.id !== "string" || typeof rule.run !== "function") {
|
|
17006
|
+
throw new Error("Plugin rules must each have string id and run()");
|
|
17007
|
+
}
|
|
17008
|
+
}
|
|
17009
|
+
}
|
|
17010
|
+
function validatePolicies(policies) {
|
|
17011
|
+
if (policies === undefined)
|
|
17012
|
+
return;
|
|
17013
|
+
if (!Array.isArray(policies) || policies.some((p) => typeof p !== "string")) {
|
|
17014
|
+
throw new Error("Plugin policies must be string[] (globs relative to .skeleton/) — got non-array");
|
|
17015
|
+
}
|
|
17016
|
+
}
|
|
17017
|
+
function readDefaultExport(record) {
|
|
17018
|
+
const value = record.default;
|
|
17019
|
+
if (!("default" in record) || typeof value !== "object" || value === null)
|
|
17020
|
+
return null;
|
|
17021
|
+
return value;
|
|
17022
|
+
}
|
|
16864
17023
|
function normalizeExport(mod) {
|
|
16865
17024
|
if (!mod || typeof mod !== "object") {
|
|
16866
17025
|
throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
|
|
@@ -16868,44 +17027,17 @@ function normalizeExport(mod) {
|
|
|
16868
17027
|
const record = mod;
|
|
16869
17028
|
const namedRules = Array.isArray(record.rules) ? record.rules : undefined;
|
|
16870
17029
|
const namedPoliciesRaw = "policies" in record ? record.policies : undefined;
|
|
16871
|
-
const def =
|
|
17030
|
+
const def = readDefaultExport(record);
|
|
16872
17031
|
const defaultRules = def && Array.isArray(def.rules) ? def.rules : undefined;
|
|
16873
17032
|
const defaultPoliciesRaw = def && "policies" in def ? def.policies : undefined;
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
if (defaultRules.length !== namedRules.length || defaultRules.some((rule, i) => rule.id !== namedRules[i]?.id || rule.run !== namedRules[i]?.run)) {
|
|
16877
|
-
throw new Error("Plugin exports disagree on rules: default and named `rules` must match when both are set");
|
|
16878
|
-
}
|
|
16879
|
-
rules = defaultRules;
|
|
16880
|
-
} else {
|
|
16881
|
-
rules = defaultRules ?? namedRules;
|
|
16882
|
-
}
|
|
16883
|
-
let policies;
|
|
16884
|
-
if (defaultPoliciesRaw !== undefined && namedPoliciesRaw !== undefined) {
|
|
16885
|
-
if (!Array.isArray(defaultPoliciesRaw) || !Array.isArray(namedPoliciesRaw) || defaultPoliciesRaw.length !== namedPoliciesRaw.length || defaultPoliciesRaw.some((p, i) => p !== namedPoliciesRaw[i])) {
|
|
16886
|
-
throw new Error("Plugin exports disagree on policies: default and named `policies` must match when both are set");
|
|
16887
|
-
}
|
|
16888
|
-
policies = defaultPoliciesRaw;
|
|
16889
|
-
} else {
|
|
16890
|
-
policies = defaultPoliciesRaw ?? namedPoliciesRaw;
|
|
16891
|
-
}
|
|
17033
|
+
const rules = resolveRules(defaultRules, namedRules);
|
|
17034
|
+
const policies = resolvePolicies(defaultPoliciesRaw, namedPoliciesRaw);
|
|
16892
17035
|
if (!Array.isArray(rules)) {
|
|
16893
17036
|
throw new Error("Plugin module must export { rules: AuditRule[]; policies?: string[] }");
|
|
16894
17037
|
}
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
}
|
|
16899
|
-
}
|
|
16900
|
-
if (policies !== undefined) {
|
|
16901
|
-
if (!Array.isArray(policies) || policies.some((p) => typeof p !== "string")) {
|
|
16902
|
-
throw new Error("Plugin policies must be string[] (globs relative to .skeleton/) — got non-array");
|
|
16903
|
-
}
|
|
16904
|
-
}
|
|
16905
|
-
return {
|
|
16906
|
-
rules,
|
|
16907
|
-
policies
|
|
16908
|
-
};
|
|
17038
|
+
validateRules(rules);
|
|
17039
|
+
validatePolicies(policies);
|
|
17040
|
+
return { rules, policies };
|
|
16909
17041
|
}
|
|
16910
17042
|
function expandPolicyGlobs(root, globs) {
|
|
16911
17043
|
const base = skeletonDir(root);
|
|
@@ -16940,20 +17072,23 @@ function loadPoliciesFromGlobs(root, globs) {
|
|
|
16940
17072
|
}
|
|
16941
17073
|
return policies;
|
|
16942
17074
|
}
|
|
17075
|
+
async function importBuiltPlugin(root, entry) {
|
|
17076
|
+
const tsAbs = resolvePluginTsPath(root, entry);
|
|
17077
|
+
const mjsAbs = mjsPathForTs(tsAbs);
|
|
17078
|
+
if (!existsSync4(mjsAbs)) {
|
|
17079
|
+
const rel = relative2(skeletonDir(root), tsAbs) || entry;
|
|
17080
|
+
throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
|
|
17081
|
+
}
|
|
17082
|
+
const mod = await import(pathToFileURL(mjsAbs).href);
|
|
17083
|
+
return normalizeExport(mod);
|
|
17084
|
+
}
|
|
16943
17085
|
async function collectWiredPolicyRelPaths(root, config) {
|
|
16944
17086
|
const entries = config.plugins ?? [];
|
|
16945
17087
|
const wired = new Set;
|
|
16946
17088
|
if (entries.length === 0)
|
|
16947
17089
|
return wired;
|
|
16948
17090
|
for (const entry of entries) {
|
|
16949
|
-
const
|
|
16950
|
-
const mjsAbs = mjsPathForTs(tsAbs);
|
|
16951
|
-
if (!existsSync4(mjsAbs)) {
|
|
16952
|
-
const rel = relative2(skeletonDir(root), tsAbs) || entry;
|
|
16953
|
-
throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
|
|
16954
|
-
}
|
|
16955
|
-
const mod = await import(pathToFileURL(mjsAbs).href);
|
|
16956
|
-
const normalized = normalizeExport(mod);
|
|
17091
|
+
const normalized = await importBuiltPlugin(root, entry);
|
|
16957
17092
|
if (!normalized.policies?.length)
|
|
16958
17093
|
continue;
|
|
16959
17094
|
for (const abs of expandPolicyGlobs(root, normalized.policies)) {
|
|
@@ -16970,14 +17105,7 @@ async function loadPlugins(root, config) {
|
|
|
16970
17105
|
const rules = [];
|
|
16971
17106
|
const policies = [];
|
|
16972
17107
|
for (const entry of entries) {
|
|
16973
|
-
const
|
|
16974
|
-
const mjsAbs = mjsPathForTs(tsAbs);
|
|
16975
|
-
if (!existsSync4(mjsAbs)) {
|
|
16976
|
-
const rel = relative2(skeletonDir(root), tsAbs) || entry;
|
|
16977
|
-
throw new Error(`Plugin not built: ${rel} (missing ${relative2(root, mjsAbs) || mjsAbs}). Run: skeleton build-plugin`);
|
|
16978
|
-
}
|
|
16979
|
-
const mod = await import(pathToFileURL(mjsAbs).href);
|
|
16980
|
-
const normalized = normalizeExport(mod);
|
|
17108
|
+
const normalized = await importBuiltPlugin(root, entry);
|
|
16981
17109
|
rules.push(...normalized.rules);
|
|
16982
17110
|
if (normalized.policies?.length) {
|
|
16983
17111
|
policies.push(...loadPoliciesFromGlobs(root, normalized.policies));
|
|
@@ -17001,6 +17129,39 @@ var DEFAULT_SKILLS_LOCKFILE = "skills-lock.json";
|
|
|
17001
17129
|
function isForeignLockSourceType(sourceType) {
|
|
17002
17130
|
return sourceType !== "local";
|
|
17003
17131
|
}
|
|
17132
|
+
function parseLockSkillEntry(input) {
|
|
17133
|
+
const { lockfileRel, slug, value, warnings } = input;
|
|
17134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
17135
|
+
warnings.push(`${lockfileRel}: skill "${slug}" has invalid entry`);
|
|
17136
|
+
return null;
|
|
17137
|
+
}
|
|
17138
|
+
const entry = value;
|
|
17139
|
+
const source = entry.source;
|
|
17140
|
+
const sourceType = entry.sourceType;
|
|
17141
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
17142
|
+
warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
|
|
17143
|
+
return null;
|
|
17144
|
+
}
|
|
17145
|
+
if (typeof sourceType !== "string" || sourceType.length === 0) {
|
|
17146
|
+
warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
|
|
17147
|
+
return null;
|
|
17148
|
+
}
|
|
17149
|
+
const parsed = { source, sourceType };
|
|
17150
|
+
if (typeof entry.skillPath === "string")
|
|
17151
|
+
parsed.skillPath = entry.skillPath;
|
|
17152
|
+
if (typeof entry.computedHash === "string")
|
|
17153
|
+
parsed.computedHash = entry.computedHash;
|
|
17154
|
+
return parsed;
|
|
17155
|
+
}
|
|
17156
|
+
function parseLockSkills(lockfileRel, skillsRaw, warnings) {
|
|
17157
|
+
const entries = {};
|
|
17158
|
+
for (const [slug, value] of Object.entries(skillsRaw)) {
|
|
17159
|
+
const parsed = parseLockSkillEntry({ lockfileRel, slug, value, warnings });
|
|
17160
|
+
if (parsed)
|
|
17161
|
+
entries[slug] = parsed;
|
|
17162
|
+
}
|
|
17163
|
+
return entries;
|
|
17164
|
+
}
|
|
17004
17165
|
function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
|
|
17005
17166
|
const warnings = [];
|
|
17006
17167
|
const abs = join4(root, lockfileRel);
|
|
@@ -17028,31 +17189,11 @@ function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
|
|
|
17028
17189
|
warnings.push(`${lockfileRel}: missing or invalid "skills" object`);
|
|
17029
17190
|
return { lockfile: lockfileRel, entries: {}, warnings };
|
|
17030
17191
|
}
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
}
|
|
17037
|
-
const entry = value;
|
|
17038
|
-
const source = entry.source;
|
|
17039
|
-
const sourceType = entry.sourceType;
|
|
17040
|
-
if (typeof source !== "string" || source.length === 0) {
|
|
17041
|
-
warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
|
|
17042
|
-
continue;
|
|
17043
|
-
}
|
|
17044
|
-
if (typeof sourceType !== "string" || sourceType.length === 0) {
|
|
17045
|
-
warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
|
|
17046
|
-
continue;
|
|
17047
|
-
}
|
|
17048
|
-
const parsed = { source, sourceType };
|
|
17049
|
-
if (typeof entry.skillPath === "string")
|
|
17050
|
-
parsed.skillPath = entry.skillPath;
|
|
17051
|
-
if (typeof entry.computedHash === "string")
|
|
17052
|
-
parsed.computedHash = entry.computedHash;
|
|
17053
|
-
entries[slug] = parsed;
|
|
17054
|
-
}
|
|
17055
|
-
return { lockfile: lockfileRel, entries, warnings };
|
|
17192
|
+
return {
|
|
17193
|
+
lockfile: lockfileRel,
|
|
17194
|
+
entries: parseLockSkills(lockfileRel, skillsRaw, warnings),
|
|
17195
|
+
warnings
|
|
17196
|
+
};
|
|
17056
17197
|
}
|
|
17057
17198
|
function classifySkillOwnership(slug, provenance, ownership) {
|
|
17058
17199
|
const ownedOverrides = new Set(ownership?.ownedSlugs ?? []);
|
|
@@ -17111,7 +17252,7 @@ function listNestedSlugs(root, relRoot) {
|
|
|
17111
17252
|
const absRoot = join5(root, relRoot);
|
|
17112
17253
|
if (!existsSync6(absRoot))
|
|
17113
17254
|
return [];
|
|
17114
|
-
return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
|
|
17255
|
+
return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
17115
17256
|
}
|
|
17116
17257
|
function listFlatSlugs(root) {
|
|
17117
17258
|
const slugs = [];
|
|
@@ -17124,34 +17265,46 @@ function listFlatSlugs(root) {
|
|
|
17124
17265
|
slugs.push(entry.name);
|
|
17125
17266
|
}
|
|
17126
17267
|
}
|
|
17127
|
-
return slugs.sort();
|
|
17268
|
+
return slugs.sort((a, b) => a.localeCompare(b));
|
|
17269
|
+
}
|
|
17270
|
+
function shouldSkipAgentsRoot(root, relRoot, claudeReal) {
|
|
17271
|
+
if (relRoot !== ".agents/skills" || !claudeReal)
|
|
17272
|
+
return false;
|
|
17273
|
+
const abs = join5(root, relRoot);
|
|
17274
|
+
const agentsReal = safeRealpath(abs);
|
|
17275
|
+
if (agentsReal && agentsReal === claudeReal)
|
|
17276
|
+
return true;
|
|
17277
|
+
const linkTarget = safeRealpath(abs);
|
|
17278
|
+
if (linkTarget === claudeReal)
|
|
17279
|
+
return true;
|
|
17280
|
+
try {
|
|
17281
|
+
const link = readlinkSync(abs);
|
|
17282
|
+
if (link && safeRealpath(join5(root, link)) === claudeReal)
|
|
17283
|
+
return true;
|
|
17284
|
+
} catch {}
|
|
17285
|
+
return false;
|
|
17286
|
+
}
|
|
17287
|
+
function addNestedSkillRoot(input) {
|
|
17288
|
+
const { roots, root, relRoot } = input;
|
|
17289
|
+
let { claudeReal } = input;
|
|
17290
|
+
const abs = join5(root, relRoot);
|
|
17291
|
+
if (!existsSync6(abs))
|
|
17292
|
+
return claudeReal;
|
|
17293
|
+
if (relRoot === ".claude/skills") {
|
|
17294
|
+
claudeReal = safeRealpath(abs);
|
|
17295
|
+
}
|
|
17296
|
+
if (shouldSkipAgentsRoot(root, relRoot, claudeReal))
|
|
17297
|
+
return claudeReal;
|
|
17298
|
+
if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
|
|
17299
|
+
roots.push({ kind: "nested", relPath: relRoot });
|
|
17300
|
+
}
|
|
17301
|
+
return claudeReal;
|
|
17128
17302
|
}
|
|
17129
17303
|
function detectSkillRoots(root) {
|
|
17130
17304
|
const roots = [];
|
|
17131
17305
|
let claudeReal = null;
|
|
17132
17306
|
for (const relRoot of NESTED_SKILL_ROOTS) {
|
|
17133
|
-
|
|
17134
|
-
if (!existsSync6(abs))
|
|
17135
|
-
continue;
|
|
17136
|
-
if (relRoot === ".claude/skills") {
|
|
17137
|
-
claudeReal = safeRealpath(abs);
|
|
17138
|
-
}
|
|
17139
|
-
if (relRoot === ".agents/skills" && claudeReal) {
|
|
17140
|
-
const agentsReal = safeRealpath(abs);
|
|
17141
|
-
if (agentsReal && agentsReal === claudeReal)
|
|
17142
|
-
continue;
|
|
17143
|
-
const linkTarget = safeRealpath(abs);
|
|
17144
|
-
if (linkTarget === claudeReal)
|
|
17145
|
-
continue;
|
|
17146
|
-
try {
|
|
17147
|
-
const link = readlinkSync(abs);
|
|
17148
|
-
if (link && claudeReal && safeRealpath(join5(root, link)) === claudeReal)
|
|
17149
|
-
continue;
|
|
17150
|
-
} catch {}
|
|
17151
|
-
}
|
|
17152
|
-
if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
|
|
17153
|
-
roots.push({ kind: "nested", relPath: relRoot });
|
|
17154
|
-
}
|
|
17307
|
+
claudeReal = addNestedSkillRoot({ roots, root, relRoot, claudeReal });
|
|
17155
17308
|
}
|
|
17156
17309
|
const flatSlugs = listFlatSlugs(root);
|
|
17157
17310
|
if (flatSlugs.length > 0) {
|
|
@@ -17193,22 +17346,30 @@ function resolveSkillPath(index, root, slug) {
|
|
|
17193
17346
|
function isSkillPath(relPath, index) {
|
|
17194
17347
|
return skillSlugForPath(relPath, index) !== null;
|
|
17195
17348
|
}
|
|
17349
|
+
function nestedSlugForPath(normalized, skillRoot, index) {
|
|
17350
|
+
const prefix = `${skillRoot.relPath}/`;
|
|
17351
|
+
if (!normalized.startsWith(prefix))
|
|
17352
|
+
return null;
|
|
17353
|
+
const slug = normalized.slice(prefix.length).split("/")[0];
|
|
17354
|
+
return slug && index.slugs.includes(slug) ? slug : null;
|
|
17355
|
+
}
|
|
17356
|
+
function flatSlugForPath(normalized, index) {
|
|
17357
|
+
const flat = new Set(index.flatSlugs);
|
|
17358
|
+
const first = normalized.split("/")[0];
|
|
17359
|
+
return first && flat.has(first) ? first : null;
|
|
17360
|
+
}
|
|
17196
17361
|
function skillSlugForPath(relPath, index) {
|
|
17197
17362
|
const normalized = normalizeRelPath(relPath);
|
|
17198
|
-
const flat = new Set(index.flatSlugs);
|
|
17199
17363
|
for (const skillRoot of index.roots) {
|
|
17200
17364
|
if (skillRoot.kind === "nested") {
|
|
17201
|
-
const
|
|
17202
|
-
if (
|
|
17203
|
-
|
|
17204
|
-
const slug = normalized.slice(prefix.length).split("/")[0];
|
|
17205
|
-
if (slug && index.slugs.includes(slug))
|
|
17206
|
-
return slug;
|
|
17365
|
+
const slug2 = nestedSlugForPath(normalized, skillRoot, index);
|
|
17366
|
+
if (slug2)
|
|
17367
|
+
return slug2;
|
|
17207
17368
|
continue;
|
|
17208
17369
|
}
|
|
17209
|
-
const
|
|
17210
|
-
if (
|
|
17211
|
-
return
|
|
17370
|
+
const slug = flatSlugForPath(normalized, index);
|
|
17371
|
+
if (slug)
|
|
17372
|
+
return slug;
|
|
17212
17373
|
}
|
|
17213
17374
|
return null;
|
|
17214
17375
|
}
|
|
@@ -17249,46 +17410,54 @@ function slugFromPath(filePath, workspaceRoot) {
|
|
|
17249
17410
|
}
|
|
17250
17411
|
return flatSlug;
|
|
17251
17412
|
}
|
|
17413
|
+
function augmentPatternsForRoot(skillRoot, index, owned) {
|
|
17414
|
+
if (skillRoot.kind === "nested") {
|
|
17415
|
+
return index.ownedSlugs.map((slug) => `${skillRoot.relPath}/${slug}/**`);
|
|
17416
|
+
}
|
|
17417
|
+
return index.flatSlugs.filter((slug) => owned.has(slug)).map((slug) => `${slug}/**`);
|
|
17418
|
+
}
|
|
17252
17419
|
function skillCollectAugments(index) {
|
|
17253
17420
|
const owned = new Set(index.ownedSlugs);
|
|
17254
17421
|
const patterns = [];
|
|
17255
17422
|
for (const skillRoot of index.roots) {
|
|
17256
|
-
|
|
17257
|
-
for (const slug of index.ownedSlugs) {
|
|
17258
|
-
patterns.push(`${skillRoot.relPath}/${slug}/**`);
|
|
17259
|
-
}
|
|
17260
|
-
} else {
|
|
17261
|
-
for (const slug of index.flatSlugs) {
|
|
17262
|
-
if (!owned.has(slug))
|
|
17263
|
-
continue;
|
|
17264
|
-
patterns.push(`${slug}/**`);
|
|
17265
|
-
}
|
|
17266
|
-
}
|
|
17423
|
+
patterns.push(...augmentPatternsForRoot(skillRoot, index, owned));
|
|
17267
17424
|
}
|
|
17268
17425
|
return patterns;
|
|
17269
17426
|
}
|
|
17427
|
+
function markdownPathsForSkillDir(root, absDir) {
|
|
17428
|
+
const paths = [];
|
|
17429
|
+
for (const abs of globSync("**/*.{md,mdc}", {
|
|
17430
|
+
cwd: absDir,
|
|
17431
|
+
absolute: true,
|
|
17432
|
+
onlyFiles: true,
|
|
17433
|
+
dot: true
|
|
17434
|
+
})) {
|
|
17435
|
+
paths.push(normalizeRelPath(relative3(root, abs)));
|
|
17436
|
+
}
|
|
17437
|
+
return paths;
|
|
17438
|
+
}
|
|
17439
|
+
function markdownPathsForRoot(root, skillRoot, owned) {
|
|
17440
|
+
const paths = [];
|
|
17441
|
+
const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
|
|
17442
|
+
for (const slug of slugs) {
|
|
17443
|
+
if (!owned.has(slug))
|
|
17444
|
+
continue;
|
|
17445
|
+
const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
|
|
17446
|
+
if (!existsSync6(absDir))
|
|
17447
|
+
continue;
|
|
17448
|
+
paths.push(...markdownPathsForSkillDir(root, absDir));
|
|
17449
|
+
}
|
|
17450
|
+
return paths;
|
|
17451
|
+
}
|
|
17270
17452
|
function listSkillMarkdownPaths(root, index) {
|
|
17271
17453
|
const owned = new Set(index.ownedSlugs);
|
|
17272
17454
|
const paths = new Set;
|
|
17273
17455
|
for (const skillRoot of index.roots) {
|
|
17274
|
-
const
|
|
17275
|
-
|
|
17276
|
-
if (!owned.has(slug))
|
|
17277
|
-
continue;
|
|
17278
|
-
const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
|
|
17279
|
-
if (!existsSync6(absDir))
|
|
17280
|
-
continue;
|
|
17281
|
-
for (const abs of globSync("**/*.{md,mdc}", {
|
|
17282
|
-
cwd: absDir,
|
|
17283
|
-
absolute: true,
|
|
17284
|
-
onlyFiles: true,
|
|
17285
|
-
dot: true
|
|
17286
|
-
})) {
|
|
17287
|
-
paths.add(normalizeRelPath(relative3(root, abs)));
|
|
17288
|
-
}
|
|
17456
|
+
for (const rel of markdownPathsForRoot(root, skillRoot, owned)) {
|
|
17457
|
+
paths.add(rel);
|
|
17289
17458
|
}
|
|
17290
17459
|
}
|
|
17291
|
-
return [...paths].sort();
|
|
17460
|
+
return [...paths].sort((a, b) => a.localeCompare(b));
|
|
17292
17461
|
}
|
|
17293
17462
|
function listSkillSlugs(index) {
|
|
17294
17463
|
return index.slugs;
|
|
@@ -17306,6 +17475,24 @@ function isMarkdownFile(absPath) {
|
|
|
17306
17475
|
function shouldExclude(relPath, exclude) {
|
|
17307
17476
|
return exclude.some((pattern) => matchesGlobScope(relPath, pattern));
|
|
17308
17477
|
}
|
|
17478
|
+
function rememberMarkdownFile(input) {
|
|
17479
|
+
const { byReal, root, abs, exclude } = input;
|
|
17480
|
+
if (!isMarkdownFile(abs))
|
|
17481
|
+
return;
|
|
17482
|
+
const rel = normalizeRelPath(relative4(root, abs));
|
|
17483
|
+
if (shouldExclude(rel, exclude))
|
|
17484
|
+
return;
|
|
17485
|
+
let real;
|
|
17486
|
+
try {
|
|
17487
|
+
real = realpathSync4(abs);
|
|
17488
|
+
} catch {
|
|
17489
|
+
real = abs;
|
|
17490
|
+
}
|
|
17491
|
+
const existing = byReal.get(real);
|
|
17492
|
+
if (existing === undefined || abs === real && existing !== real) {
|
|
17493
|
+
byReal.set(real, abs);
|
|
17494
|
+
}
|
|
17495
|
+
}
|
|
17309
17496
|
function expandPatterns(root, patterns, exclude) {
|
|
17310
17497
|
const byReal = new Map;
|
|
17311
17498
|
for (const pattern of patterns) {
|
|
@@ -17316,21 +17503,7 @@ function expandPatterns(root, patterns, exclude) {
|
|
|
17316
17503
|
dot: true,
|
|
17317
17504
|
ignore: exclude
|
|
17318
17505
|
})) {
|
|
17319
|
-
|
|
17320
|
-
continue;
|
|
17321
|
-
const rel = normalizeRelPath(relative4(root, abs));
|
|
17322
|
-
if (shouldExclude(rel, exclude))
|
|
17323
|
-
continue;
|
|
17324
|
-
let real;
|
|
17325
|
-
try {
|
|
17326
|
-
real = realpathSync4(abs);
|
|
17327
|
-
} catch {
|
|
17328
|
-
real = abs;
|
|
17329
|
-
}
|
|
17330
|
-
const existing = byReal.get(real);
|
|
17331
|
-
if (existing === undefined || abs === real && existing !== real) {
|
|
17332
|
-
byReal.set(real, abs);
|
|
17333
|
-
}
|
|
17506
|
+
rememberMarkdownFile({ byReal, root, abs, exclude });
|
|
17334
17507
|
}
|
|
17335
17508
|
}
|
|
17336
17509
|
return [...byReal.values()];
|
|
@@ -17393,31 +17566,41 @@ function excludeForeignSkillDocMetaPaths(docMetaPaths, skillIndex) {
|
|
|
17393
17566
|
return docMetaPaths;
|
|
17394
17567
|
return docMetaPaths.filter((rel) => !isForeignSkillPath(rel, skillIndex));
|
|
17395
17568
|
}
|
|
17396
|
-
function
|
|
17569
|
+
function collectRegistryDocMeta(ctx) {
|
|
17397
17570
|
const paths = [];
|
|
17398
|
-
for (const
|
|
17399
|
-
|
|
17400
|
-
}
|
|
17401
|
-
const extras = ["docs/README.md", ".skeleton/registry.md"];
|
|
17402
|
-
for (const file of extras) {
|
|
17403
|
-
const abs = join6(root, file);
|
|
17404
|
-
if (existsSync7(abs))
|
|
17405
|
-
paths.push(normalizeRelPath(file));
|
|
17406
|
-
}
|
|
17407
|
-
for (const rel of registryPaths) {
|
|
17408
|
-
if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
|
|
17571
|
+
for (const rel of ctx.registryPaths) {
|
|
17572
|
+
if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
|
|
17409
17573
|
continue;
|
|
17410
|
-
const abs = join6(root, rel);
|
|
17574
|
+
const abs = join6(ctx.root, rel);
|
|
17411
17575
|
if (existsSync7(abs))
|
|
17412
17576
|
paths.push(normalizeRelPath(rel));
|
|
17413
17577
|
}
|
|
17414
|
-
|
|
17578
|
+
return paths;
|
|
17579
|
+
}
|
|
17580
|
+
function collectScannedDocMeta(ctx) {
|
|
17581
|
+
const paths = [];
|
|
17582
|
+
for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
|
|
17415
17583
|
const content = readFileSync5(abs, "utf8");
|
|
17416
17584
|
if (/<!--\s*doc-meta:/.test(content)) {
|
|
17417
|
-
paths.push(normalizeRelPath(relative4(root, abs)));
|
|
17585
|
+
paths.push(normalizeRelPath(relative4(ctx.root, abs)));
|
|
17418
17586
|
}
|
|
17419
17587
|
}
|
|
17420
|
-
return
|
|
17588
|
+
return paths;
|
|
17589
|
+
}
|
|
17590
|
+
function collectDocMetaPaths(ctx) {
|
|
17591
|
+
const paths = [];
|
|
17592
|
+
for (const abs of expandPatterns(ctx.root, ["docs/*/README.md"], mergedExcludes(ctx.config))) {
|
|
17593
|
+
paths.push(normalizeRelPath(relative4(ctx.root, abs)));
|
|
17594
|
+
}
|
|
17595
|
+
const extras = ["docs/README.md", ".skeleton/registry.md"];
|
|
17596
|
+
for (const file of extras) {
|
|
17597
|
+
const abs = join6(ctx.root, file);
|
|
17598
|
+
if (existsSync7(abs))
|
|
17599
|
+
paths.push(normalizeRelPath(file));
|
|
17600
|
+
}
|
|
17601
|
+
paths.push(...collectRegistryDocMeta(ctx));
|
|
17602
|
+
paths.push(...collectScannedDocMeta(ctx));
|
|
17603
|
+
return excludeForeignSkillDocMetaPaths([...new Set(paths)], ctx.skillIndex);
|
|
17421
17604
|
}
|
|
17422
17605
|
function validateScanRoots(config, root) {
|
|
17423
17606
|
const missing = [];
|
|
@@ -17440,31 +17623,34 @@ function filterToPaths(files, paths, root) {
|
|
|
17440
17623
|
return normalizedPaths.some((path) => rel === path || rel.startsWith(`${path}/`));
|
|
17441
17624
|
});
|
|
17442
17625
|
}
|
|
17626
|
+
function addExplicitPath(out, root, raw) {
|
|
17627
|
+
const rel = normalizeRelPath(raw);
|
|
17628
|
+
const abs = join6(root, rel);
|
|
17629
|
+
if (!existsSync7(abs))
|
|
17630
|
+
return;
|
|
17631
|
+
if (isMarkdownFile(rel)) {
|
|
17632
|
+
out.add(abs);
|
|
17633
|
+
return;
|
|
17634
|
+
}
|
|
17635
|
+
try {
|
|
17636
|
+
if (!statSync2(abs).isDirectory())
|
|
17637
|
+
return;
|
|
17638
|
+
} catch {
|
|
17639
|
+
return;
|
|
17640
|
+
}
|
|
17641
|
+
for (const md of globSync("**/*.{md,mdc}", {
|
|
17642
|
+
cwd: abs,
|
|
17643
|
+
absolute: true,
|
|
17644
|
+
onlyFiles: true,
|
|
17645
|
+
dot: true
|
|
17646
|
+
})) {
|
|
17647
|
+
out.add(md);
|
|
17648
|
+
}
|
|
17649
|
+
}
|
|
17443
17650
|
function includeExplicitMarkdownPaths(files, paths, root) {
|
|
17444
17651
|
const out = new Set(files);
|
|
17445
17652
|
for (const raw of paths) {
|
|
17446
|
-
|
|
17447
|
-
const abs = join6(root, rel);
|
|
17448
|
-
if (!existsSync7(abs))
|
|
17449
|
-
continue;
|
|
17450
|
-
if (isMarkdownFile(rel)) {
|
|
17451
|
-
out.add(abs);
|
|
17452
|
-
continue;
|
|
17453
|
-
}
|
|
17454
|
-
try {
|
|
17455
|
-
if (!statSync2(abs).isDirectory())
|
|
17456
|
-
continue;
|
|
17457
|
-
} catch {
|
|
17458
|
-
continue;
|
|
17459
|
-
}
|
|
17460
|
-
for (const md of globSync("**/*.{md,mdc}", {
|
|
17461
|
-
cwd: abs,
|
|
17462
|
-
absolute: true,
|
|
17463
|
-
onlyFiles: true,
|
|
17464
|
-
dot: true
|
|
17465
|
-
})) {
|
|
17466
|
-
out.add(md);
|
|
17467
|
-
}
|
|
17653
|
+
addExplicitPath(out, root, raw);
|
|
17468
17654
|
}
|
|
17469
17655
|
return [...out];
|
|
17470
17656
|
}
|
|
@@ -17501,9 +17687,6 @@ function parseRegistry(root) {
|
|
|
17501
17687
|
}
|
|
17502
17688
|
return { paths: [...new Set(paths)], hasTableHeader };
|
|
17503
17689
|
}
|
|
17504
|
-
function parseRegistryPaths(root) {
|
|
17505
|
-
return parseRegistry(root).paths;
|
|
17506
|
-
}
|
|
17507
17690
|
|
|
17508
17691
|
// src/audit/core/context.ts
|
|
17509
17692
|
function createContext(options = {}) {
|
|
@@ -17519,7 +17702,12 @@ function createContext(options = {}) {
|
|
|
17519
17702
|
files = filterToPaths(files, options.paths, root);
|
|
17520
17703
|
}
|
|
17521
17704
|
const registry = parseRegistry(root);
|
|
17522
|
-
const allDocMetaPaths = collectDocMetaPaths(
|
|
17705
|
+
const allDocMetaPaths = collectDocMetaPaths({
|
|
17706
|
+
config,
|
|
17707
|
+
root,
|
|
17708
|
+
registryPaths: registry.paths,
|
|
17709
|
+
skillIndex
|
|
17710
|
+
});
|
|
17523
17711
|
return {
|
|
17524
17712
|
root,
|
|
17525
17713
|
config,
|
|
@@ -21791,9 +21979,9 @@ function factoryTitle(effects, ok2, nok, type, markerType, stringType) {
|
|
|
21791
21979
|
return atBreak(code);
|
|
21792
21980
|
}
|
|
21793
21981
|
effects.consume(code);
|
|
21794
|
-
return code === codes.backslash ?
|
|
21982
|
+
return code === codes.backslash ? escape : inside;
|
|
21795
21983
|
}
|
|
21796
|
-
function
|
|
21984
|
+
function escape(code) {
|
|
21797
21985
|
if (code === marker || code === codes.backslash) {
|
|
21798
21986
|
effects.consume(code);
|
|
21799
21987
|
return inside;
|
|
@@ -28820,183 +29008,213 @@ function stripYamlFrontmatter(content3) {
|
|
|
28820
29008
|
return "";
|
|
28821
29009
|
return content3;
|
|
28822
29010
|
}
|
|
28823
|
-
function
|
|
28824
|
-
let i =
|
|
28825
|
-
if (i >= slice.length)
|
|
28826
|
-
return false;
|
|
28827
|
-
if (slice[i] === ")") {
|
|
28828
|
-
return i === slice.length - 1;
|
|
28829
|
-
}
|
|
28830
|
-
const first = slice[i];
|
|
28831
|
-
if (first === undefined || !/\s/.test(first))
|
|
28832
|
-
return false;
|
|
29011
|
+
function skipWhitespace(slice, start) {
|
|
29012
|
+
let i = start;
|
|
28833
29013
|
while (i < slice.length) {
|
|
28834
29014
|
const ch = slice[i];
|
|
28835
29015
|
if (ch === undefined || !/\s/.test(ch))
|
|
28836
29016
|
break;
|
|
28837
29017
|
i++;
|
|
28838
29018
|
}
|
|
28839
|
-
|
|
28840
|
-
|
|
28841
|
-
|
|
28842
|
-
|
|
28843
|
-
|
|
28844
|
-
const open = slice[
|
|
29019
|
+
return i;
|
|
29020
|
+
}
|
|
29021
|
+
function skipTitle(slice, start) {
|
|
29022
|
+
if (start >= slice.length)
|
|
29023
|
+
return start;
|
|
29024
|
+
const open = slice[start];
|
|
28845
29025
|
if (open !== '"' && open !== "'" && open !== "(")
|
|
28846
|
-
return
|
|
29026
|
+
return start;
|
|
28847
29027
|
const close = open === "(" ? ")" : open;
|
|
28848
|
-
i
|
|
29028
|
+
let i = start + 1;
|
|
28849
29029
|
while (i < slice.length && slice[i] !== close)
|
|
28850
29030
|
i++;
|
|
29031
|
+
return i < slice.length ? i + 1 : i;
|
|
29032
|
+
}
|
|
29033
|
+
function destinationConsumesToSliceEnd(slice, afterDest) {
|
|
29034
|
+
if (afterDest >= slice.length)
|
|
29035
|
+
return false;
|
|
29036
|
+
if (slice[afterDest] === ")")
|
|
29037
|
+
return afterDest === slice.length - 1;
|
|
29038
|
+
const first = slice[afterDest];
|
|
29039
|
+
if (first === undefined || !/\s/.test(first))
|
|
29040
|
+
return false;
|
|
29041
|
+
let i = skipWhitespace(slice, afterDest);
|
|
28851
29042
|
if (i >= slice.length)
|
|
28852
29043
|
return false;
|
|
28853
|
-
i
|
|
28854
|
-
|
|
28855
|
-
|
|
28856
|
-
|
|
28857
|
-
break;
|
|
28858
|
-
i++;
|
|
28859
|
-
}
|
|
29044
|
+
if (slice[i] === ")")
|
|
29045
|
+
return i === slice.length - 1;
|
|
29046
|
+
i = skipTitle(slice, i);
|
|
29047
|
+
i = skipWhitespace(slice, i);
|
|
28860
29048
|
return i === slice.length - 1 && slice[i] === ")";
|
|
28861
29049
|
}
|
|
28862
|
-
function
|
|
28863
|
-
|
|
29050
|
+
function spanForAngleUrl(slice, after) {
|
|
29051
|
+
if (!slice.content.startsWith(`<${slice.url}>`, after))
|
|
29052
|
+
return null;
|
|
29053
|
+
const afterDest = after + 2 + slice.url.length;
|
|
29054
|
+
if (!destinationConsumesToSliceEnd(slice.content, afterDest))
|
|
29055
|
+
return null;
|
|
29056
|
+
const urlStart = slice.nodeStart + after + 1;
|
|
29057
|
+
return { urlStart, urlEnd: urlStart + slice.url.length };
|
|
29058
|
+
}
|
|
29059
|
+
function spanForBareUrl(slice, after) {
|
|
29060
|
+
if (!slice.content.startsWith(slice.url, after))
|
|
29061
|
+
return null;
|
|
29062
|
+
const next = slice.content[after + slice.url.length];
|
|
29063
|
+
if (!(next === ")" || next !== undefined && /\s/.test(next)))
|
|
29064
|
+
return null;
|
|
29065
|
+
if (!destinationConsumesToSliceEnd(slice.content, after + slice.url.length))
|
|
29066
|
+
return null;
|
|
29067
|
+
const urlStart = slice.nodeStart + after;
|
|
29068
|
+
return { urlStart, urlEnd: urlStart + slice.url.length };
|
|
29069
|
+
}
|
|
29070
|
+
function tryLinkDestination(slice, openParen) {
|
|
29071
|
+
const after = openParen + 2;
|
|
29072
|
+
return spanForAngleUrl(slice, after) ?? spanForBareUrl(slice, after);
|
|
29073
|
+
}
|
|
29074
|
+
function findUrlSpanInSlice(slice) {
|
|
29075
|
+
const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
|
|
28864
29076
|
let searchFrom = 0;
|
|
28865
|
-
while (searchFrom <
|
|
28866
|
-
const openParen =
|
|
29077
|
+
while (searchFrom < nodeSlice.length) {
|
|
29078
|
+
const openParen = nodeSlice.indexOf("](", searchFrom);
|
|
28867
29079
|
if (openParen === -1)
|
|
28868
29080
|
break;
|
|
28869
|
-
const
|
|
28870
|
-
if (
|
|
28871
|
-
|
|
28872
|
-
if (destinationConsumesToSliceEnd(slice, afterDest)) {
|
|
28873
|
-
const urlStart = nodeStart + after + 1;
|
|
28874
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
28875
|
-
}
|
|
28876
|
-
} else if (slice.startsWith(url, after)) {
|
|
28877
|
-
const next = slice[after + url.length];
|
|
28878
|
-
if (next === ")" || next !== undefined && /\s/.test(next)) {
|
|
28879
|
-
if (destinationConsumesToSliceEnd(slice, after + url.length)) {
|
|
28880
|
-
const urlStart = nodeStart + after;
|
|
28881
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
28882
|
-
}
|
|
28883
|
-
}
|
|
28884
|
-
}
|
|
29081
|
+
const span = tryLinkDestination({ content: nodeSlice, nodeStart: slice.nodeStart, nodeEnd: slice.nodeEnd, url: slice.url }, openParen);
|
|
29082
|
+
if (span)
|
|
29083
|
+
return span;
|
|
28885
29084
|
searchFrom = openParen + 1;
|
|
28886
29085
|
}
|
|
28887
|
-
|
|
28888
|
-
|
|
28889
|
-
|
|
29086
|
+
return matchAutolinkOrBare(nodeSlice, slice);
|
|
29087
|
+
}
|
|
29088
|
+
function matchAutolinkOrBare(nodeSlice, slice) {
|
|
29089
|
+
const auto = `<${slice.url}>`;
|
|
29090
|
+
if (nodeSlice === auto) {
|
|
29091
|
+
return { urlStart: slice.nodeStart + 1, urlEnd: slice.nodeStart + 1 + slice.url.length };
|
|
28890
29092
|
}
|
|
28891
|
-
if (
|
|
28892
|
-
return { urlStart: nodeStart, urlEnd: nodeEnd };
|
|
29093
|
+
if (nodeSlice === slice.url) {
|
|
29094
|
+
return { urlStart: slice.nodeStart, urlEnd: slice.nodeEnd };
|
|
28893
29095
|
}
|
|
28894
|
-
const trimmed =
|
|
29096
|
+
const trimmed = nodeSlice.trim();
|
|
28895
29097
|
if (trimmed === auto) {
|
|
28896
|
-
const lead =
|
|
29098
|
+
const lead = nodeSlice.indexOf(auto);
|
|
28897
29099
|
if (lead !== -1) {
|
|
28898
|
-
return {
|
|
29100
|
+
return {
|
|
29101
|
+
urlStart: slice.nodeStart + lead + 1,
|
|
29102
|
+
urlEnd: slice.nodeStart + lead + 1 + slice.url.length
|
|
29103
|
+
};
|
|
28899
29104
|
}
|
|
28900
29105
|
}
|
|
28901
|
-
if (trimmed === url) {
|
|
28902
|
-
const lead =
|
|
29106
|
+
if (trimmed === slice.url) {
|
|
29107
|
+
const lead = nodeSlice.indexOf(slice.url);
|
|
28903
29108
|
if (lead !== -1) {
|
|
28904
|
-
return {
|
|
29109
|
+
return {
|
|
29110
|
+
urlStart: slice.nodeStart + lead,
|
|
29111
|
+
urlEnd: slice.nodeStart + lead + slice.url.length
|
|
29112
|
+
};
|
|
28905
29113
|
}
|
|
28906
29114
|
}
|
|
28907
|
-
return;
|
|
28908
29115
|
}
|
|
28909
|
-
function findUrlInDefinitionSlice(
|
|
28910
|
-
const
|
|
28911
|
-
const labelEnd2 =
|
|
29116
|
+
function findUrlInDefinitionSlice(slice) {
|
|
29117
|
+
const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
|
|
29118
|
+
const labelEnd2 = nodeSlice.indexOf("]:");
|
|
28912
29119
|
if (labelEnd2 === -1)
|
|
28913
29120
|
return;
|
|
28914
29121
|
let i = labelEnd2 + 2;
|
|
28915
|
-
while (i <
|
|
29122
|
+
while (i < nodeSlice.length && /\s/.test(nodeSlice[i] ?? ""))
|
|
28916
29123
|
i++;
|
|
28917
|
-
if (
|
|
28918
|
-
const urlStart = nodeStart + i + 1;
|
|
28919
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
29124
|
+
if (nodeSlice.startsWith(`<${slice.url}>`, i)) {
|
|
29125
|
+
const urlStart = slice.nodeStart + i + 1;
|
|
29126
|
+
return { urlStart, urlEnd: urlStart + slice.url.length };
|
|
28920
29127
|
}
|
|
28921
|
-
if (
|
|
28922
|
-
const urlStart = nodeStart + i;
|
|
28923
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
29128
|
+
if (nodeSlice.startsWith(slice.url, i)) {
|
|
29129
|
+
const urlStart = slice.nodeStart + i;
|
|
29130
|
+
return { urlStart, urlEnd: urlStart + slice.url.length };
|
|
28924
29131
|
}
|
|
28925
|
-
|
|
29132
|
+
}
|
|
29133
|
+
function definitionFromNode(content3, node2, defs) {
|
|
29134
|
+
if (!(("identifier" in node2) && ("url" in node2)))
|
|
29135
|
+
return;
|
|
29136
|
+
const id = String(node2.identifier).toLowerCase();
|
|
29137
|
+
if (defs.has(id))
|
|
29138
|
+
return;
|
|
29139
|
+
const url = typeof node2.url === "string" ? node2.url : "";
|
|
29140
|
+
if (!url)
|
|
29141
|
+
return;
|
|
29142
|
+
const start = node2.position?.start?.offset;
|
|
29143
|
+
const end = node2.position?.end?.offset;
|
|
29144
|
+
if (start === undefined || end === undefined)
|
|
29145
|
+
return;
|
|
29146
|
+
const span = findUrlInDefinitionSlice({ content: content3, nodeStart: start, nodeEnd: end, url });
|
|
29147
|
+
if (!span)
|
|
29148
|
+
return;
|
|
29149
|
+
defs.set(id, {
|
|
29150
|
+
url,
|
|
29151
|
+
urlStart: span.urlStart,
|
|
29152
|
+
urlEnd: span.urlEnd,
|
|
29153
|
+
line: lineFromOffset(content3, start) ?? 1
|
|
29154
|
+
});
|
|
28926
29155
|
}
|
|
28927
29156
|
function collectReferenceDefinitions(content3, tree) {
|
|
28928
29157
|
const defs = new Map;
|
|
28929
29158
|
visit2(tree, (node2) => {
|
|
28930
29159
|
if (node2.type !== "definition")
|
|
28931
29160
|
return;
|
|
28932
|
-
|
|
28933
|
-
return;
|
|
28934
|
-
const id = String(node2.identifier).toLowerCase();
|
|
28935
|
-
if (defs.has(id))
|
|
28936
|
-
return;
|
|
28937
|
-
const url = typeof node2.url === "string" ? node2.url : "";
|
|
28938
|
-
if (!url)
|
|
28939
|
-
return;
|
|
28940
|
-
const start = node2.position?.start.offset;
|
|
28941
|
-
const end = node2.position?.end.offset;
|
|
28942
|
-
if (start === undefined || end === undefined)
|
|
28943
|
-
return;
|
|
28944
|
-
const span = findUrlInDefinitionSlice(content3, start, end, url);
|
|
28945
|
-
if (!span)
|
|
28946
|
-
return;
|
|
28947
|
-
defs.set(id, {
|
|
28948
|
-
url,
|
|
28949
|
-
urlStart: span.urlStart,
|
|
28950
|
-
urlEnd: span.urlEnd,
|
|
28951
|
-
line: lineFromOffset(content3, start) ?? 1
|
|
28952
|
-
});
|
|
29161
|
+
definitionFromNode(content3, node2, defs);
|
|
28953
29162
|
});
|
|
28954
29163
|
return defs;
|
|
28955
29164
|
}
|
|
29165
|
+
function linkFromDirectNode(content3, node2) {
|
|
29166
|
+
const target = node2.url.trim();
|
|
29167
|
+
const start = node2.position?.start?.offset;
|
|
29168
|
+
const end = node2.position?.end?.offset;
|
|
29169
|
+
const span = start !== undefined && end !== undefined ? findUrlSpanInSlice({ content: content3, nodeStart: start, nodeEnd: end, url: target }) : undefined;
|
|
29170
|
+
return {
|
|
29171
|
+
target,
|
|
29172
|
+
line: lineFromOffset(content3, node2.position?.start?.offset),
|
|
29173
|
+
urlStart: span?.urlStart,
|
|
29174
|
+
urlEnd: span?.urlEnd
|
|
29175
|
+
};
|
|
29176
|
+
}
|
|
29177
|
+
function linkFromReference(id, refDefs) {
|
|
29178
|
+
const def = refDefs.get(id);
|
|
29179
|
+
if (!def)
|
|
29180
|
+
return null;
|
|
29181
|
+
return {
|
|
29182
|
+
target: def.url.trim(),
|
|
29183
|
+
line: def.line,
|
|
29184
|
+
urlStart: def.urlStart,
|
|
29185
|
+
urlEnd: def.urlEnd
|
|
29186
|
+
};
|
|
29187
|
+
}
|
|
28956
29188
|
function extractLinksFromMarkdown(content3, _filePath) {
|
|
28957
29189
|
const tree = processor.parse(content3);
|
|
28958
29190
|
const refDefs = collectReferenceDefinitions(content3, tree);
|
|
28959
29191
|
const links = [];
|
|
28960
29192
|
visit2(tree, (node2) => {
|
|
28961
29193
|
if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
|
|
28962
|
-
|
|
28963
|
-
const start = node2.position?.start.offset;
|
|
28964
|
-
const end = node2.position?.end.offset;
|
|
28965
|
-
const span = start !== undefined && end !== undefined ? findUrlSpanInSlice(content3, start, end, target) : undefined;
|
|
28966
|
-
links.push({
|
|
28967
|
-
target,
|
|
28968
|
-
line: lineFromOffset(content3, node2.position?.start.offset),
|
|
28969
|
-
urlStart: span?.urlStart,
|
|
28970
|
-
urlEnd: span?.urlEnd
|
|
28971
|
-
});
|
|
29194
|
+
links.push(linkFromDirectNode(content3, node2));
|
|
28972
29195
|
}
|
|
28973
29196
|
if (node2.type === "linkReference" && "identifier" in node2) {
|
|
28974
|
-
const
|
|
28975
|
-
|
|
28976
|
-
|
|
28977
|
-
links.push({
|
|
28978
|
-
target: def.url.trim(),
|
|
28979
|
-
line: def.line,
|
|
28980
|
-
urlStart: def.urlStart,
|
|
28981
|
-
urlEnd: def.urlEnd
|
|
28982
|
-
});
|
|
28983
|
-
}
|
|
29197
|
+
const refLink = linkFromReference(String(node2.identifier).toLowerCase(), refDefs);
|
|
29198
|
+
if (refLink)
|
|
29199
|
+
links.push(refLink);
|
|
28984
29200
|
}
|
|
28985
29201
|
});
|
|
28986
29202
|
return links;
|
|
28987
29203
|
}
|
|
29204
|
+
function textFromPhrasingNode(node2) {
|
|
29205
|
+
if (node2.type === "text" || node2.type === "inlineCode") {
|
|
29206
|
+
return "value" in node2 && node2.value !== undefined ? String(node2.value) : "";
|
|
29207
|
+
}
|
|
29208
|
+
return "";
|
|
29209
|
+
}
|
|
28988
29210
|
function phrasingText(nodes) {
|
|
28989
29211
|
if (!nodes?.length)
|
|
28990
29212
|
return "";
|
|
28991
29213
|
let out = "";
|
|
28992
29214
|
for (const node2 of nodes) {
|
|
28993
|
-
|
|
28994
|
-
|
|
28995
|
-
continue;
|
|
28996
|
-
}
|
|
28997
|
-
if (node2.children?.length) {
|
|
29215
|
+
out += textFromPhrasingNode(node2);
|
|
29216
|
+
if (node2.children?.length)
|
|
28998
29217
|
out += phrasingText(node2.children);
|
|
28999
|
-
}
|
|
29000
29218
|
}
|
|
29001
29219
|
return out;
|
|
29002
29220
|
}
|
|
@@ -29086,60 +29304,86 @@ function replaceAnchorInTarget(target, oldAnchor, newAnchor) {
|
|
|
29086
29304
|
return target;
|
|
29087
29305
|
return `${pathPart}#${newAnchor}${queryPart}`;
|
|
29088
29306
|
}
|
|
29307
|
+
function anchorTargetReplacement(filePath, target) {
|
|
29308
|
+
if (isExternalLink(target) && !target.startsWith("#"))
|
|
29309
|
+
return null;
|
|
29310
|
+
if (isPlaceholderLink(target))
|
|
29311
|
+
return null;
|
|
29312
|
+
const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
|
|
29313
|
+
if (!anchor)
|
|
29314
|
+
return null;
|
|
29315
|
+
const resolved = resolveLink(filePath, target);
|
|
29316
|
+
if (!existsSync9(resolved))
|
|
29317
|
+
return null;
|
|
29318
|
+
const targetContent = readFileSync7(resolved, "utf8");
|
|
29319
|
+
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
29320
|
+
const anchorSlug = slugifyAnchor(anchor);
|
|
29321
|
+
if (slugs.has(anchorSlug))
|
|
29322
|
+
return null;
|
|
29323
|
+
const match = findBestAnchorMatch(anchorSlug, slugs);
|
|
29324
|
+
if (!match)
|
|
29325
|
+
return null;
|
|
29326
|
+
const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
|
|
29327
|
+
if (nextTarget === target)
|
|
29328
|
+
return null;
|
|
29329
|
+
return { nextTarget, match, anchor };
|
|
29330
|
+
}
|
|
29331
|
+
function pendingAnchorForLink(input) {
|
|
29332
|
+
const { filePath, content: content3, relFile, link: link2 } = input;
|
|
29333
|
+
const { target, line, urlStart, urlEnd } = link2;
|
|
29334
|
+
const replacement = anchorTargetReplacement(filePath, target);
|
|
29335
|
+
if (!replacement)
|
|
29336
|
+
return null;
|
|
29337
|
+
if (urlStart === undefined || urlEnd === undefined || content3.slice(urlStart, urlEnd) !== target) {
|
|
29338
|
+
return null;
|
|
29339
|
+
}
|
|
29340
|
+
const lineLabel = line ? `${relFile}:${line}` : relFile;
|
|
29341
|
+
return {
|
|
29342
|
+
urlStart,
|
|
29343
|
+
urlEnd,
|
|
29344
|
+
from: target,
|
|
29345
|
+
to: replacement.nextTarget,
|
|
29346
|
+
description: `${lineLabel} #${replacement.anchor} → #${replacement.match.slug} (score ${replacement.match.score.toFixed(2)})`
|
|
29347
|
+
};
|
|
29348
|
+
}
|
|
29349
|
+
function applyPendingEdits(content3, pending) {
|
|
29350
|
+
const uniqueBySpan = new Map;
|
|
29351
|
+
for (const edit of pending) {
|
|
29352
|
+
uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
|
|
29353
|
+
}
|
|
29354
|
+
const uniquePending = [...uniqueBySpan.values()].sort((a, b) => b.urlStart - a.urlStart);
|
|
29355
|
+
let updated = content3;
|
|
29356
|
+
const descriptions = [];
|
|
29357
|
+
for (const edit of uniquePending) {
|
|
29358
|
+
if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
|
|
29359
|
+
continue;
|
|
29360
|
+
updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
|
|
29361
|
+
descriptions.push(edit.description);
|
|
29362
|
+
}
|
|
29363
|
+
if (updated === content3 || descriptions.length === 0)
|
|
29364
|
+
return null;
|
|
29365
|
+
return { content: updated, descriptions };
|
|
29366
|
+
}
|
|
29367
|
+
function collectFileAnchorFixes(ctx, filePath) {
|
|
29368
|
+
const content3 = readFileContent(filePath);
|
|
29369
|
+
const links = extractLinksFromMarkdown(content3, filePath);
|
|
29370
|
+
const relFile = relPath(filePath, ctx.root);
|
|
29371
|
+
const pending = [];
|
|
29372
|
+
for (const link2 of links) {
|
|
29373
|
+
const edit = pendingAnchorForLink({ ctx, filePath, content: content3, relFile, link: link2 });
|
|
29374
|
+
if (edit)
|
|
29375
|
+
pending.push(edit);
|
|
29376
|
+
}
|
|
29377
|
+
if (pending.length === 0)
|
|
29378
|
+
return null;
|
|
29379
|
+
return applyPendingEdits(content3, pending);
|
|
29380
|
+
}
|
|
29089
29381
|
function collectAnchorFixes(ctx) {
|
|
29090
29382
|
const editsByFile = new Map;
|
|
29091
29383
|
for (const filePath of ctx.files) {
|
|
29092
|
-
const
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
const relFile = relPath(filePath, ctx.root);
|
|
29096
|
-
for (const { target, line, urlStart, urlEnd } of links) {
|
|
29097
|
-
if (isExternalLink(target) && !target.startsWith("#"))
|
|
29098
|
-
continue;
|
|
29099
|
-
if (isPlaceholderLink(target))
|
|
29100
|
-
continue;
|
|
29101
|
-
const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
|
|
29102
|
-
if (!anchor)
|
|
29103
|
-
continue;
|
|
29104
|
-
const resolved = resolveLink(filePath, target);
|
|
29105
|
-
if (!existsSync9(resolved))
|
|
29106
|
-
continue;
|
|
29107
|
-
const targetContent = readFileSync7(resolved, "utf8");
|
|
29108
|
-
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
29109
|
-
const anchorSlug = slugifyAnchor(anchor);
|
|
29110
|
-
if (slugs.has(anchorSlug))
|
|
29111
|
-
continue;
|
|
29112
|
-
const match = findBestAnchorMatch(anchorSlug, slugs);
|
|
29113
|
-
if (!match)
|
|
29114
|
-
continue;
|
|
29115
|
-
const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
|
|
29116
|
-
if (nextTarget === target)
|
|
29117
|
-
continue;
|
|
29118
|
-
const lineLabel = line ? `${relFile}:${line}` : relFile;
|
|
29119
|
-
const description = `${lineLabel} #${anchor} → #${match.slug} (score ${match.score.toFixed(2)})`;
|
|
29120
|
-
if (urlStart !== undefined && urlEnd !== undefined && content3.slice(urlStart, urlEnd) === target) {
|
|
29121
|
-
pending.push({ urlStart, urlEnd, from: target, to: nextTarget, description });
|
|
29122
|
-
}
|
|
29123
|
-
}
|
|
29124
|
-
if (pending.length === 0)
|
|
29125
|
-
continue;
|
|
29126
|
-
const uniqueBySpan = new Map;
|
|
29127
|
-
for (const edit of pending) {
|
|
29128
|
-
uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
|
|
29129
|
-
}
|
|
29130
|
-
const uniquePending = [...uniqueBySpan.values()];
|
|
29131
|
-
uniquePending.sort((a, b) => b.urlStart - a.urlStart);
|
|
29132
|
-
let updated = content3;
|
|
29133
|
-
const descriptions = [];
|
|
29134
|
-
for (const edit of uniquePending) {
|
|
29135
|
-
if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
|
|
29136
|
-
continue;
|
|
29137
|
-
updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
|
|
29138
|
-
descriptions.push(edit.description);
|
|
29139
|
-
}
|
|
29140
|
-
if (updated === content3 || descriptions.length === 0)
|
|
29141
|
-
continue;
|
|
29142
|
-
editsByFile.set(filePath, { content: updated, descriptions });
|
|
29384
|
+
const result = collectFileAnchorFixes(ctx, filePath);
|
|
29385
|
+
if (result)
|
|
29386
|
+
editsByFile.set(filePath, result);
|
|
29143
29387
|
}
|
|
29144
29388
|
const edits = [];
|
|
29145
29389
|
for (const [absPath, { content: content3, descriptions }] of editsByFile) {
|
|
@@ -29182,32 +29426,37 @@ function bumpDocMetaLastReviewed(content3, gitDate) {
|
|
|
29182
29426
|
return null;
|
|
29183
29427
|
return replaceDocMetaLastReviewed(content3, gitDate);
|
|
29184
29428
|
}
|
|
29429
|
+
function docMetaFixForPath(ctx, relPath2) {
|
|
29430
|
+
const abs = join9(ctx.root, relPath2);
|
|
29431
|
+
if (!existsSync10(abs))
|
|
29432
|
+
return null;
|
|
29433
|
+
const content3 = readFileSync8(abs, "utf8");
|
|
29434
|
+
if (!DOC_META_RE.test(content3))
|
|
29435
|
+
return null;
|
|
29436
|
+
const reviewedStr = docMetaLastReviewed(content3);
|
|
29437
|
+
if (!reviewedStr)
|
|
29438
|
+
return null;
|
|
29439
|
+
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29440
|
+
if (Number.isNaN(reviewed.getTime()))
|
|
29441
|
+
return null;
|
|
29442
|
+
const gitDate = lastGitCommitDate(relPath2, ctx.root);
|
|
29443
|
+
if (!gitDate)
|
|
29444
|
+
return null;
|
|
29445
|
+
const updated = bumpDocMetaLastReviewed(content3, gitDate);
|
|
29446
|
+
if (!updated)
|
|
29447
|
+
return null;
|
|
29448
|
+
return {
|
|
29449
|
+
file: relPath2,
|
|
29450
|
+
description: `last-reviewed ${reviewedStr} → ${gitDate}`,
|
|
29451
|
+
content: updated
|
|
29452
|
+
};
|
|
29453
|
+
}
|
|
29185
29454
|
function collectDocMetaFixes(ctx) {
|
|
29186
29455
|
const edits = [];
|
|
29187
29456
|
for (const relPath2 of ctx.docMetaPaths) {
|
|
29188
|
-
const
|
|
29189
|
-
if (
|
|
29190
|
-
|
|
29191
|
-
const content3 = readFileSync8(abs, "utf8");
|
|
29192
|
-
if (!DOC_META_RE.test(content3))
|
|
29193
|
-
continue;
|
|
29194
|
-
const reviewedStr = docMetaLastReviewed(content3);
|
|
29195
|
-
if (!reviewedStr)
|
|
29196
|
-
continue;
|
|
29197
|
-
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29198
|
-
if (Number.isNaN(reviewed.getTime()))
|
|
29199
|
-
continue;
|
|
29200
|
-
const gitDate = lastGitCommitDate(relPath2, ctx.root);
|
|
29201
|
-
if (!gitDate)
|
|
29202
|
-
continue;
|
|
29203
|
-
const updated = bumpDocMetaLastReviewed(content3, gitDate);
|
|
29204
|
-
if (!updated)
|
|
29205
|
-
continue;
|
|
29206
|
-
edits.push({
|
|
29207
|
-
file: relPath2,
|
|
29208
|
-
description: `last-reviewed ${reviewedStr} → ${gitDate}`,
|
|
29209
|
-
content: updated
|
|
29210
|
-
});
|
|
29457
|
+
const fix = docMetaFixForPath(ctx, relPath2);
|
|
29458
|
+
if (fix)
|
|
29459
|
+
edits.push(fix);
|
|
29211
29460
|
}
|
|
29212
29461
|
return edits;
|
|
29213
29462
|
}
|
|
@@ -29249,28 +29498,31 @@ function overlayLastReviewed(targetContent, metaContent) {
|
|
|
29249
29498
|
function underRoot(rootAbs, candidateAbs) {
|
|
29250
29499
|
return candidateAbs === rootAbs || candidateAbs.startsWith(rootAbs + sep3);
|
|
29251
29500
|
}
|
|
29252
|
-
function
|
|
29253
|
-
|
|
29254
|
-
|
|
29255
|
-
|
|
29256
|
-
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29257
|
-
}
|
|
29501
|
+
function shouldStopPathWalk(rootResolved, parent, cursor) {
|
|
29502
|
+
return parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved;
|
|
29503
|
+
}
|
|
29504
|
+
function resolveExistingRealPath(rootResolved, abs, relFile) {
|
|
29258
29505
|
const rootReal = existsSync11(rootResolved) ? realpathSync5(rootResolved) : rootResolved;
|
|
29259
29506
|
let cursor = abs;
|
|
29260
|
-
while (
|
|
29261
|
-
if (existsSync11(cursor)) {
|
|
29262
|
-
const real = realpathSync5(cursor);
|
|
29263
|
-
if (!underRoot(rootReal, real)) {
|
|
29264
|
-
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29265
|
-
}
|
|
29266
|
-
return abs;
|
|
29267
|
-
}
|
|
29507
|
+
while (!existsSync11(cursor)) {
|
|
29268
29508
|
const parent = dirname6(cursor);
|
|
29269
|
-
if (
|
|
29509
|
+
if (shouldStopPathWalk(rootResolved, parent, cursor))
|
|
29270
29510
|
return abs;
|
|
29271
|
-
}
|
|
29272
29511
|
cursor = parent;
|
|
29273
29512
|
}
|
|
29513
|
+
const real = realpathSync5(cursor);
|
|
29514
|
+
if (!underRoot(rootReal, real)) {
|
|
29515
|
+
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29516
|
+
}
|
|
29517
|
+
return abs;
|
|
29518
|
+
}
|
|
29519
|
+
function resolveWritePath(root2, relFile) {
|
|
29520
|
+
const rootResolved = resolve6(root2);
|
|
29521
|
+
const abs = resolve6(rootResolved, relFile);
|
|
29522
|
+
if (!underRoot(rootResolved, abs)) {
|
|
29523
|
+
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29524
|
+
}
|
|
29525
|
+
return resolveExistingRealPath(rootResolved, abs, relFile);
|
|
29274
29526
|
}
|
|
29275
29527
|
function applyFixes(ctx, options) {
|
|
29276
29528
|
const kinds = new Set(options.kinds);
|
|
@@ -29314,13 +29566,14 @@ function fixKindsForOnly(kinds, only) {
|
|
|
29314
29566
|
}
|
|
29315
29567
|
|
|
29316
29568
|
// src/audit/core/report.ts
|
|
29317
|
-
function issue(rule, file,
|
|
29569
|
+
function issue(rule, file, details) {
|
|
29570
|
+
const message = typeof details === "string" ? details : details.message;
|
|
29318
29571
|
return {
|
|
29319
29572
|
rule,
|
|
29320
29573
|
file,
|
|
29321
|
-
link:
|
|
29574
|
+
link: typeof details === "string" ? undefined : details.link,
|
|
29322
29575
|
message,
|
|
29323
|
-
severity:
|
|
29576
|
+
severity: typeof details === "string" ? "error" : details.severity ?? "error"
|
|
29324
29577
|
};
|
|
29325
29578
|
}
|
|
29326
29579
|
function finalizeIssues(issues, strict) {
|
|
@@ -29333,31 +29586,39 @@ function printReport(issues, options) {
|
|
|
29333
29586
|
const errors2 = finalized.filter((i) => i.severity === "error");
|
|
29334
29587
|
const warnings = finalized.filter((i) => i.severity === "warning");
|
|
29335
29588
|
const label = options.label ?? "Audit";
|
|
29336
|
-
|
|
29337
|
-
|
|
29338
|
-
|
|
29339
|
-
|
|
29340
|
-
|
|
29341
|
-
|
|
29342
|
-
|
|
29343
|
-
|
|
29344
|
-
|
|
29345
|
-
|
|
29346
|
-
|
|
29347
|
-
|
|
29589
|
+
const ctx = { label, options, errors: errors2, warnings, finalized };
|
|
29590
|
+
if (options.json)
|
|
29591
|
+
return printJsonReport(ctx);
|
|
29592
|
+
return printTextReport(ctx);
|
|
29593
|
+
}
|
|
29594
|
+
function printJsonReport(ctx) {
|
|
29595
|
+
console.log(JSON.stringify({
|
|
29596
|
+
label: ctx.label,
|
|
29597
|
+
fileCount: ctx.options.fileCount,
|
|
29598
|
+
errors: ctx.errors.length,
|
|
29599
|
+
warnings: ctx.warnings.length,
|
|
29600
|
+
issues: ctx.finalized
|
|
29601
|
+
}, null, 2));
|
|
29602
|
+
return ctx.errors.length > 0 ? 1 : 0;
|
|
29603
|
+
}
|
|
29604
|
+
function printWarnings(label, warnings) {
|
|
29605
|
+
if (warnings.length === 0)
|
|
29606
|
+
return;
|
|
29607
|
+
console.log(`${label} warnings:
|
|
29348
29608
|
`);
|
|
29349
|
-
|
|
29350
|
-
|
|
29351
|
-
|
|
29352
|
-
}
|
|
29353
|
-
console.log("");
|
|
29354
|
-
}
|
|
29355
|
-
if (errors2.length === 0) {
|
|
29356
|
-
const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
|
|
29357
|
-
const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
|
|
29358
|
-
console.log(`${label} passed${countNote}.`);
|
|
29359
|
-
return 0;
|
|
29609
|
+
for (const i of warnings) {
|
|
29610
|
+
const linkPart = i.link ? ` (${i.link})` : "";
|
|
29611
|
+
console.log(`- ${i.file}${linkPart}: ${i.message}`);
|
|
29360
29612
|
}
|
|
29613
|
+
console.log("");
|
|
29614
|
+
}
|
|
29615
|
+
function printSuccess(label, options, warnings) {
|
|
29616
|
+
const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
|
|
29617
|
+
const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
|
|
29618
|
+
console.log(`${label} passed${countNote}.`);
|
|
29619
|
+
return 0;
|
|
29620
|
+
}
|
|
29621
|
+
function printErrors(label, errors2) {
|
|
29361
29622
|
console.log(`${label} failed:
|
|
29362
29623
|
`);
|
|
29363
29624
|
for (const i of errors2) {
|
|
@@ -29366,6 +29627,13 @@ function printReport(issues, options) {
|
|
|
29366
29627
|
}
|
|
29367
29628
|
return 1;
|
|
29368
29629
|
}
|
|
29630
|
+
function printTextReport(ctx) {
|
|
29631
|
+
const { label, options, errors: errors2, warnings } = ctx;
|
|
29632
|
+
printWarnings(label, warnings);
|
|
29633
|
+
if (errors2.length === 0)
|
|
29634
|
+
return printSuccess(label, options, warnings);
|
|
29635
|
+
return printErrors(label, errors2);
|
|
29636
|
+
}
|
|
29369
29637
|
|
|
29370
29638
|
// src/references/check.ts
|
|
29371
29639
|
import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
@@ -29425,73 +29693,86 @@ function findSharedRefLinks(content3, sourceFile) {
|
|
|
29425
29693
|
}
|
|
29426
29694
|
return links;
|
|
29427
29695
|
}
|
|
29696
|
+
function findSiblingRefLinks(root2, content3, sourceFile) {
|
|
29697
|
+
const links = [];
|
|
29698
|
+
if (!/\/references\//.test(sourceFile))
|
|
29699
|
+
return links;
|
|
29700
|
+
const refsIdx = sourceFile.lastIndexOf("/references/");
|
|
29701
|
+
const withinRefs = sourceFile.slice(refsIdx + "/references/".length);
|
|
29702
|
+
const withinDir = withinRefs.includes("/") ? withinRefs.slice(0, withinRefs.lastIndexOf("/")) : "";
|
|
29703
|
+
const siblingRe = /\((?!https?:|#|\.\.\/)([a-z0-9./_-]+\.md)\)/gi;
|
|
29704
|
+
for (const match of content3.matchAll(siblingRe)) {
|
|
29705
|
+
const raw = normalizeRelPath(match[1] ?? "");
|
|
29706
|
+
if (!raw)
|
|
29707
|
+
continue;
|
|
29708
|
+
const refPath = withinDir ? normalizeRelPath(join10(withinDir, raw)) : raw;
|
|
29709
|
+
if (!canonicalExists(root2, refPath))
|
|
29710
|
+
continue;
|
|
29711
|
+
links.push({ refPath, sourceFile });
|
|
29712
|
+
}
|
|
29713
|
+
return links;
|
|
29714
|
+
}
|
|
29428
29715
|
function findLocalCanonicalLinks(root2, content3, sourceFile) {
|
|
29429
29716
|
const links = [];
|
|
29430
29717
|
const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
|
|
29431
29718
|
for (const match of content3.matchAll(localRefRe)) {
|
|
29432
29719
|
const refPath = normalizeRelPath(match[1] ?? "");
|
|
29433
|
-
if (!refPath
|
|
29720
|
+
if (!(refPath && canonicalExists(root2, refPath)))
|
|
29434
29721
|
continue;
|
|
29435
29722
|
links.push({ refPath, sourceFile });
|
|
29436
29723
|
}
|
|
29437
|
-
|
|
29438
|
-
|
|
29439
|
-
|
|
29440
|
-
|
|
29441
|
-
|
|
29442
|
-
|
|
29443
|
-
|
|
29444
|
-
|
|
29445
|
-
|
|
29446
|
-
|
|
29447
|
-
|
|
29448
|
-
|
|
29724
|
+
links.push(...findSiblingRefLinks(root2, content3, sourceFile));
|
|
29725
|
+
return links;
|
|
29726
|
+
}
|
|
29727
|
+
function collectLinksForFile(root2, relFile) {
|
|
29728
|
+
const content3 = readFileSync9(join10(root2, relFile), "utf8");
|
|
29729
|
+
if (isGeneratedReference(content3))
|
|
29730
|
+
return [];
|
|
29731
|
+
return [
|
|
29732
|
+
...findSharedRefLinks(content3, relFile),
|
|
29733
|
+
...findLocalCanonicalLinks(root2, content3, relFile)
|
|
29734
|
+
];
|
|
29735
|
+
}
|
|
29736
|
+
function expandTransitiveRefs(input) {
|
|
29737
|
+
const { root: root2, slug: slug2, refPaths, links } = input;
|
|
29738
|
+
const queue = [...refPaths];
|
|
29739
|
+
while (queue.length > 0) {
|
|
29740
|
+
const refPath = queue.pop();
|
|
29741
|
+
if (!(refPath && canonicalExists(root2, refPath)))
|
|
29742
|
+
continue;
|
|
29743
|
+
const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
|
|
29744
|
+
const syntheticSource = generatedRefPath(slug2, refPath);
|
|
29745
|
+
for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
|
|
29746
|
+
if (refPaths.has(link2.refPath))
|
|
29449
29747
|
continue;
|
|
29450
|
-
|
|
29748
|
+
refPaths.add(link2.refPath);
|
|
29749
|
+
links.push(link2);
|
|
29750
|
+
queue.push(link2.refPath);
|
|
29451
29751
|
}
|
|
29452
29752
|
}
|
|
29453
|
-
|
|
29753
|
+
}
|
|
29754
|
+
function planForSkill(root2, slug2) {
|
|
29755
|
+
const skillDir = join10(root2, slug2);
|
|
29756
|
+
if (!existsSync12(join10(skillDir, "SKILL.md")))
|
|
29757
|
+
return null;
|
|
29758
|
+
const refPaths = new Set;
|
|
29759
|
+
const links = [];
|
|
29760
|
+
for (const relFile of walkMarkdownFiles(skillDir, root2)) {
|
|
29761
|
+
for (const link2 of collectLinksForFile(root2, relFile)) {
|
|
29762
|
+
refPaths.add(link2.refPath);
|
|
29763
|
+
links.push(link2);
|
|
29764
|
+
}
|
|
29765
|
+
}
|
|
29766
|
+
expandTransitiveRefs({ root: root2, slug: slug2, refPaths, links });
|
|
29767
|
+
return refPaths.size > 0 ? { skill: slug2, refPaths, links } : null;
|
|
29454
29768
|
}
|
|
29455
29769
|
function discoverSkillReferencePlans(root2, ownership) {
|
|
29456
29770
|
const index2 = buildSkillIndex(root2, ownership);
|
|
29457
29771
|
const plans = [];
|
|
29458
29772
|
for (const slug2 of index2.ownedSlugs) {
|
|
29459
|
-
const
|
|
29460
|
-
if (
|
|
29461
|
-
|
|
29462
|
-
const refPaths = new Set;
|
|
29463
|
-
const links = [];
|
|
29464
|
-
for (const relFile of walkMarkdownFiles(skillDir, root2)) {
|
|
29465
|
-
const content3 = readFileSync9(join10(root2, relFile), "utf8");
|
|
29466
|
-
if (isGeneratedReference(content3))
|
|
29467
|
-
continue;
|
|
29468
|
-
for (const link2 of findSharedRefLinks(content3, relFile)) {
|
|
29469
|
-
refPaths.add(link2.refPath);
|
|
29470
|
-
links.push(link2);
|
|
29471
|
-
}
|
|
29472
|
-
for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
|
|
29473
|
-
refPaths.add(link2.refPath);
|
|
29474
|
-
links.push(link2);
|
|
29475
|
-
}
|
|
29476
|
-
}
|
|
29477
|
-
const queue = [...refPaths];
|
|
29478
|
-
while (queue.length > 0) {
|
|
29479
|
-
const refPath = queue.pop();
|
|
29480
|
-
if (!refPath || !canonicalExists(root2, refPath))
|
|
29481
|
-
continue;
|
|
29482
|
-
const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
|
|
29483
|
-
const syntheticSource = generatedRefPath(slug2, refPath);
|
|
29484
|
-
for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
|
|
29485
|
-
if (refPaths.has(link2.refPath))
|
|
29486
|
-
continue;
|
|
29487
|
-
refPaths.add(link2.refPath);
|
|
29488
|
-
links.push(link2);
|
|
29489
|
-
queue.push(link2.refPath);
|
|
29490
|
-
}
|
|
29491
|
-
}
|
|
29492
|
-
if (refPaths.size > 0) {
|
|
29493
|
-
plans.push({ skill: slug2, refPaths, links });
|
|
29494
|
-
}
|
|
29773
|
+
const plan = planForSkill(root2, slug2);
|
|
29774
|
+
if (plan)
|
|
29775
|
+
plans.push(plan);
|
|
29495
29776
|
}
|
|
29496
29777
|
return plans.sort((a, b) => a.skill.localeCompare(b.skill));
|
|
29497
29778
|
}
|
|
@@ -29522,30 +29803,75 @@ function rewriteSharedRefLinks(content3, sourceFile, skill) {
|
|
|
29522
29803
|
}
|
|
29523
29804
|
|
|
29524
29805
|
// src/references/check.ts
|
|
29806
|
+
function walkMarkdown(dir, onFile) {
|
|
29807
|
+
if (!existsSync13(dir))
|
|
29808
|
+
return;
|
|
29809
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
29810
|
+
if (entry.name.startsWith("."))
|
|
29811
|
+
continue;
|
|
29812
|
+
const fullPath = join11(dir, entry.name);
|
|
29813
|
+
if (entry.isDirectory()) {
|
|
29814
|
+
walkMarkdown(fullPath, onFile);
|
|
29815
|
+
continue;
|
|
29816
|
+
}
|
|
29817
|
+
if (!entry.name.endsWith(".md"))
|
|
29818
|
+
continue;
|
|
29819
|
+
onFile(fullPath);
|
|
29820
|
+
}
|
|
29821
|
+
}
|
|
29525
29822
|
function listAllGeneratedFiles(root2) {
|
|
29526
29823
|
const files = [];
|
|
29527
|
-
|
|
29528
|
-
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
if (entry.name.startsWith("."))
|
|
29532
|
-
continue;
|
|
29533
|
-
const fullPath = join11(dir, entry.name);
|
|
29534
|
-
if (entry.isDirectory()) {
|
|
29535
|
-
walk(fullPath);
|
|
29536
|
-
continue;
|
|
29537
|
-
}
|
|
29538
|
-
if (!entry.name.endsWith(".md"))
|
|
29539
|
-
continue;
|
|
29540
|
-
const content3 = readFileSync10(fullPath, "utf8");
|
|
29541
|
-
if (isGeneratedReference(content3)) {
|
|
29542
|
-
files.push(normalizeRelPath(relative7(root2, fullPath)));
|
|
29543
|
-
}
|
|
29824
|
+
walkMarkdown(root2, (fullPath) => {
|
|
29825
|
+
const content3 = readFileSync10(fullPath, "utf8");
|
|
29826
|
+
if (isGeneratedReference(content3)) {
|
|
29827
|
+
files.push(normalizeRelPath(relative7(root2, fullPath)));
|
|
29544
29828
|
}
|
|
29545
|
-
};
|
|
29546
|
-
walk(root2);
|
|
29829
|
+
});
|
|
29547
29830
|
return files;
|
|
29548
29831
|
}
|
|
29832
|
+
function checkNeededCopy(root2, targetRel) {
|
|
29833
|
+
const targetPath = join11(root2, targetRel);
|
|
29834
|
+
if (!existsSync13(targetPath)) {
|
|
29835
|
+
return issue("generated-references", targetRel, "missing generated copy — run skeleton references sync");
|
|
29836
|
+
}
|
|
29837
|
+
const generated = readFileSync10(targetPath, "utf8");
|
|
29838
|
+
if (!isGeneratedReference(generated)) {
|
|
29839
|
+
return issue("generated-references", targetRel, "expected generated-reference provenance header");
|
|
29840
|
+
}
|
|
29841
|
+
const body = stripGeneratedHeader(generated);
|
|
29842
|
+
const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
|
|
29843
|
+
const canonicalPath = join11(root2, sourceRel);
|
|
29844
|
+
if (!existsSync13(canonicalPath)) {
|
|
29845
|
+
return issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`);
|
|
29846
|
+
}
|
|
29847
|
+
const canonical = readFileSync10(canonicalPath, "utf8");
|
|
29848
|
+
if (body !== canonical) {
|
|
29849
|
+
return issue("generated-references", targetRel, "stale generated copy — run skeleton references sync");
|
|
29850
|
+
}
|
|
29851
|
+
return null;
|
|
29852
|
+
}
|
|
29853
|
+
function checkOrphanedCopies(root2, needed, skillIndex) {
|
|
29854
|
+
const issues = [];
|
|
29855
|
+
for (const generatedRel of listAllGeneratedFiles(root2)) {
|
|
29856
|
+
if (isForeignSkillPath(generatedRel, skillIndex))
|
|
29857
|
+
continue;
|
|
29858
|
+
if (!needed.has(generatedRel)) {
|
|
29859
|
+
issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
|
|
29860
|
+
}
|
|
29861
|
+
}
|
|
29862
|
+
return issues;
|
|
29863
|
+
}
|
|
29864
|
+
function checkStaleSharedLinks(root2, skillDir) {
|
|
29865
|
+
const issues = [];
|
|
29866
|
+
walkMarkdown(skillDir, (fullPath) => {
|
|
29867
|
+
const relFile = normalizeRelPath(relative7(root2, fullPath));
|
|
29868
|
+
const content3 = readFileSync10(fullPath, "utf8");
|
|
29869
|
+
if (!content3.match(SHARED_REF_LINK_RE))
|
|
29870
|
+
return;
|
|
29871
|
+
issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
|
|
29872
|
+
});
|
|
29873
|
+
return issues;
|
|
29874
|
+
}
|
|
29549
29875
|
function runGeneratedReferencesCheck(root2, ownership) {
|
|
29550
29876
|
const issues = [];
|
|
29551
29877
|
const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
|
|
@@ -29560,58 +29886,13 @@ function runGeneratedReferencesCheck(root2, ownership) {
|
|
|
29560
29886
|
}
|
|
29561
29887
|
}
|
|
29562
29888
|
for (const targetRel of needed) {
|
|
29563
|
-
const
|
|
29564
|
-
if (
|
|
29565
|
-
issues.push(
|
|
29566
|
-
continue;
|
|
29567
|
-
}
|
|
29568
|
-
const generated = readFileSync10(targetPath, "utf8");
|
|
29569
|
-
if (!isGeneratedReference(generated)) {
|
|
29570
|
-
issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
|
|
29571
|
-
continue;
|
|
29572
|
-
}
|
|
29573
|
-
const body = stripGeneratedHeader(generated);
|
|
29574
|
-
const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
|
|
29575
|
-
const canonicalPath = join11(root2, sourceRel);
|
|
29576
|
-
if (!existsSync13(canonicalPath)) {
|
|
29577
|
-
issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
|
|
29578
|
-
continue;
|
|
29579
|
-
}
|
|
29580
|
-
const canonical = readFileSync10(canonicalPath, "utf8");
|
|
29581
|
-
if (body !== canonical) {
|
|
29582
|
-
issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
|
|
29583
|
-
}
|
|
29584
|
-
}
|
|
29585
|
-
for (const generatedRel of listAllGeneratedFiles(root2)) {
|
|
29586
|
-
if (isForeignSkillPath(generatedRel, skillIndex))
|
|
29587
|
-
continue;
|
|
29588
|
-
if (!needed.has(generatedRel)) {
|
|
29589
|
-
issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
|
|
29590
|
-
}
|
|
29889
|
+
const found = checkNeededCopy(root2, targetRel);
|
|
29890
|
+
if (found)
|
|
29891
|
+
issues.push(found);
|
|
29591
29892
|
}
|
|
29893
|
+
issues.push(...checkOrphanedCopies(root2, needed, skillIndex));
|
|
29592
29894
|
for (const plan of plans) {
|
|
29593
|
-
|
|
29594
|
-
if (!existsSync13(skillDir))
|
|
29595
|
-
continue;
|
|
29596
|
-
const walk = (dir) => {
|
|
29597
|
-
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
29598
|
-
if (entry.name.startsWith("."))
|
|
29599
|
-
continue;
|
|
29600
|
-
const fullPath = join11(dir, entry.name);
|
|
29601
|
-
if (entry.isDirectory()) {
|
|
29602
|
-
walk(fullPath);
|
|
29603
|
-
continue;
|
|
29604
|
-
}
|
|
29605
|
-
if (!entry.name.endsWith(".md"))
|
|
29606
|
-
continue;
|
|
29607
|
-
const relFile = normalizeRelPath(relative7(root2, fullPath));
|
|
29608
|
-
const content3 = readFileSync10(fullPath, "utf8");
|
|
29609
|
-
if (content3.match(SHARED_REF_LINK_RE)) {
|
|
29610
|
-
issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
|
|
29611
|
-
}
|
|
29612
|
-
}
|
|
29613
|
-
};
|
|
29614
|
-
walk(skillDir);
|
|
29895
|
+
issues.push(...checkStaleSharedLinks(root2, join11(root2, plan.skill)));
|
|
29615
29896
|
}
|
|
29616
29897
|
return issues;
|
|
29617
29898
|
}
|
|
@@ -29638,6 +29919,51 @@ var bannedRule = { id: "banned", run: runBannedRule };
|
|
|
29638
29919
|
// src/audit/rules/doc-meta.ts
|
|
29639
29920
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
29640
29921
|
import { join as join12 } from "node:path";
|
|
29922
|
+
function checkDocMetaBanner(relPath2, content3) {
|
|
29923
|
+
if (DOC_META_RE.test(content3))
|
|
29924
|
+
return null;
|
|
29925
|
+
return issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)");
|
|
29926
|
+
}
|
|
29927
|
+
function checkStaleReview(input) {
|
|
29928
|
+
const { relPath: relPath2, content: content3, today, staleDays } = input;
|
|
29929
|
+
const reviewedStr = docMetaLastReviewed(content3);
|
|
29930
|
+
if (!reviewedStr)
|
|
29931
|
+
return null;
|
|
29932
|
+
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29933
|
+
if (Number.isNaN(reviewed.getTime()))
|
|
29934
|
+
return null;
|
|
29935
|
+
const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
|
|
29936
|
+
if (ageDays <= staleDays)
|
|
29937
|
+
return null;
|
|
29938
|
+
return issue("doc-meta", relPath2, {
|
|
29939
|
+
message: `doc-meta last-reviewed ${reviewedStr} is stale (>${staleDays} days)`,
|
|
29940
|
+
severity: "warning"
|
|
29941
|
+
});
|
|
29942
|
+
}
|
|
29943
|
+
function checkGitFreshness(input) {
|
|
29944
|
+
const { relPath: relPath2, content: content3, root: root2, lockedSkillSlugs } = input;
|
|
29945
|
+
const reviewedStr = docMetaLastReviewed(content3);
|
|
29946
|
+
if (!reviewedStr)
|
|
29947
|
+
return null;
|
|
29948
|
+
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29949
|
+
if (Number.isNaN(reviewed.getTime()))
|
|
29950
|
+
return null;
|
|
29951
|
+
const slug2 = slugFromPath(relPath2, root2);
|
|
29952
|
+
if (slug2 !== null && lockedSkillSlugs.has(slug2))
|
|
29953
|
+
return null;
|
|
29954
|
+
const gitDate = lastGitCommitDate(relPath2, root2);
|
|
29955
|
+
if (!gitDate)
|
|
29956
|
+
return null;
|
|
29957
|
+
const committed = new Date(`${gitDate}T00:00:00Z`);
|
|
29958
|
+
if (Number.isNaN(committed.getTime()))
|
|
29959
|
+
return null;
|
|
29960
|
+
if (committed.getTime() <= reviewed.getTime())
|
|
29961
|
+
return null;
|
|
29962
|
+
return issue("doc-meta", relPath2, {
|
|
29963
|
+
message: `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`,
|
|
29964
|
+
severity: "warning"
|
|
29965
|
+
});
|
|
29966
|
+
}
|
|
29641
29967
|
function runDocMetaRule(ctx) {
|
|
29642
29968
|
const issues = [];
|
|
29643
29969
|
const today = new Date;
|
|
@@ -29646,32 +29972,27 @@ function runDocMetaRule(ctx) {
|
|
|
29646
29972
|
if (!existsSync14(abs))
|
|
29647
29973
|
continue;
|
|
29648
29974
|
const content3 = readFileSync11(abs, "utf8");
|
|
29649
|
-
|
|
29650
|
-
|
|
29651
|
-
|
|
29652
|
-
}
|
|
29653
|
-
const reviewedStr = docMetaLastReviewed(content3);
|
|
29654
|
-
if (!reviewedStr)
|
|
29655
|
-
continue;
|
|
29656
|
-
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29657
|
-
if (Number.isNaN(reviewed.getTime()))
|
|
29975
|
+
const banner = checkDocMetaBanner(relPath2, content3);
|
|
29976
|
+
if (banner) {
|
|
29977
|
+
issues.push(banner);
|
|
29658
29978
|
continue;
|
|
29659
|
-
const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
|
|
29660
|
-
if (ageDays > ctx.config.daysUntilStale) {
|
|
29661
|
-
issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
|
|
29662
|
-
}
|
|
29663
|
-
const slug2 = slugFromPath(relPath2, ctx.root);
|
|
29664
|
-
if (slug2 !== null && ctx.lockedSkillSlugs.has(slug2))
|
|
29665
|
-
continue;
|
|
29666
|
-
const gitDate = lastGitCommitDate(relPath2, ctx.root);
|
|
29667
|
-
if (!gitDate)
|
|
29668
|
-
continue;
|
|
29669
|
-
const committed = new Date(`${gitDate}T00:00:00Z`);
|
|
29670
|
-
if (Number.isNaN(committed.getTime()))
|
|
29671
|
-
continue;
|
|
29672
|
-
if (committed.getTime() > reviewed.getTime()) {
|
|
29673
|
-
issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
|
|
29674
29979
|
}
|
|
29980
|
+
const stale = checkStaleReview({
|
|
29981
|
+
relPath: relPath2,
|
|
29982
|
+
content: content3,
|
|
29983
|
+
today,
|
|
29984
|
+
staleDays: ctx.config.daysUntilStale
|
|
29985
|
+
});
|
|
29986
|
+
if (stale)
|
|
29987
|
+
issues.push(stale);
|
|
29988
|
+
const git = checkGitFreshness({
|
|
29989
|
+
relPath: relPath2,
|
|
29990
|
+
content: content3,
|
|
29991
|
+
root: ctx.root,
|
|
29992
|
+
lockedSkillSlugs: ctx.lockedSkillSlugs
|
|
29993
|
+
});
|
|
29994
|
+
if (git)
|
|
29995
|
+
issues.push(git);
|
|
29675
29996
|
}
|
|
29676
29997
|
return issues;
|
|
29677
29998
|
}
|
|
@@ -29686,57 +30007,88 @@ function resolveLink2(sourceFile, target) {
|
|
|
29686
30007
|
return sourceFile;
|
|
29687
30008
|
return resolve7(dirname7(sourceFile), withoutAnchor);
|
|
29688
30009
|
}
|
|
29689
|
-
function
|
|
29690
|
-
const
|
|
29691
|
-
|
|
29692
|
-
|
|
29693
|
-
|
|
29694
|
-
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29698
|
-
|
|
29699
|
-
|
|
29700
|
-
|
|
29701
|
-
|
|
29702
|
-
|
|
29703
|
-
|
|
29704
|
-
|
|
29705
|
-
|
|
29706
|
-
|
|
29707
|
-
|
|
29708
|
-
|
|
29709
|
-
|
|
29710
|
-
|
|
29711
|
-
|
|
29712
|
-
|
|
29713
|
-
return issues;
|
|
29714
|
-
}
|
|
29715
|
-
}
|
|
29716
|
-
if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
|
|
29717
|
-
const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
|
|
29718
|
-
if (!existsSync15(agentPath)) {
|
|
29719
|
-
issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
|
|
29720
|
-
}
|
|
29721
|
-
return issues;
|
|
29722
|
-
}
|
|
29723
|
-
if (pathPart && !existsSync15(resolved)) {
|
|
29724
|
-
issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
|
|
29725
|
-
link: linkLabel
|
|
29726
|
-
}));
|
|
29727
|
-
return issues;
|
|
29728
|
-
}
|
|
29729
|
-
if (anchor && existsSync15(resolved)) {
|
|
29730
|
-
const targetContent = readFileSync12(resolved, "utf8");
|
|
29731
|
-
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
29732
|
-
const anchorSlug = slugifyAnchor(anchor);
|
|
29733
|
-
if (!slugs.has(anchorSlug)) {
|
|
29734
|
-
issues.push(issue("links", relSource, `broken anchor → #${anchor} in ${relTarget}`, {
|
|
29735
|
-
link: linkLabel
|
|
29736
|
-
}));
|
|
29737
|
-
}
|
|
30010
|
+
function checkRetiredSkill(input) {
|
|
30011
|
+
const skillMatch = SKILL_LINK_IN_TARGET_RE.exec(input.target);
|
|
30012
|
+
const slug2 = skillMatch?.[1];
|
|
30013
|
+
if (!(slug2 && input.ctx.retiredSkills.has(slug2)))
|
|
30014
|
+
return null;
|
|
30015
|
+
return issue("links", relPath(input.sourceFile, input.ctx.root), {
|
|
30016
|
+
message: `references retired skill "${slug2}/SKILL.md"`,
|
|
30017
|
+
link: input.linkLabel
|
|
30018
|
+
});
|
|
30019
|
+
}
|
|
30020
|
+
function checkMissingSkill(input, relSource) {
|
|
30021
|
+
if (!input.target.includes("/SKILL.md"))
|
|
30022
|
+
return null;
|
|
30023
|
+
const slug2 = SKILL_LINK_IN_TARGET_RE.exec(input.target)?.[1];
|
|
30024
|
+
if (!(slug2 && !resolveSkillPath(input.ctx.skillIndex, input.ctx.root, slug2)))
|
|
30025
|
+
return null;
|
|
30026
|
+
return issue("links", relSource, {
|
|
30027
|
+
message: `missing skill "${slug2}/SKILL.md"`,
|
|
30028
|
+
link: input.linkLabel
|
|
30029
|
+
});
|
|
30030
|
+
}
|
|
30031
|
+
function checkAgentFile(input, resolved, relSource) {
|
|
30032
|
+
if (!((input.target.includes(".claude/agents/") || input.target.includes(".cursor/agents/")) && input.target.endsWith(".md"))) {
|
|
30033
|
+
return null;
|
|
29738
30034
|
}
|
|
29739
|
-
|
|
30035
|
+
const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
|
|
30036
|
+
if (existsSync15(agentPath))
|
|
30037
|
+
return null;
|
|
30038
|
+
return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
|
|
30039
|
+
}
|
|
30040
|
+
function checkBrokenPath(ctx) {
|
|
30041
|
+
const { input, pathPart, resolved, relSource, relTarget } = ctx;
|
|
30042
|
+
if (!(pathPart && !existsSync15(resolved)))
|
|
30043
|
+
return null;
|
|
30044
|
+
return issue("links", relSource, {
|
|
30045
|
+
message: `broken link → ${relTarget}`,
|
|
30046
|
+
link: input.linkLabel
|
|
30047
|
+
});
|
|
30048
|
+
}
|
|
30049
|
+
function checkBrokenAnchor(ctx) {
|
|
30050
|
+
const { input, anchor, resolved, relSource, relTarget } = ctx;
|
|
30051
|
+
if (!(anchor && existsSync15(resolved)))
|
|
30052
|
+
return null;
|
|
30053
|
+
const targetContent = readFileSync12(resolved, "utf8");
|
|
30054
|
+
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
30055
|
+
const anchorSlug = slugifyAnchor(anchor);
|
|
30056
|
+
if (slugs.has(anchorSlug))
|
|
30057
|
+
return null;
|
|
30058
|
+
return issue("links", relSource, {
|
|
30059
|
+
message: `broken anchor → #${anchor} in ${relTarget}`,
|
|
30060
|
+
link: input.linkLabel
|
|
30061
|
+
});
|
|
30062
|
+
}
|
|
30063
|
+
function resolveTargetParts(sourceFile, target, root2) {
|
|
30064
|
+
const relSource = relPath(sourceFile, root2);
|
|
30065
|
+
const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
|
|
30066
|
+
const pathPart = target.split("#")[0]?.split("?")[0] ?? "";
|
|
30067
|
+
const resolved = resolveLink2(sourceFile, target);
|
|
30068
|
+
const relTarget = relPath(resolved, root2);
|
|
30069
|
+
return { relSource, anchor, pathPart, resolved, relTarget };
|
|
30070
|
+
}
|
|
30071
|
+
function validateTarget(input) {
|
|
30072
|
+
const { ctx, sourceFile, target } = input;
|
|
30073
|
+
if (isExternalLink(target) && !target.startsWith("#"))
|
|
30074
|
+
return [];
|
|
30075
|
+
if (isPlaceholderLink(target))
|
|
30076
|
+
return [];
|
|
30077
|
+
const parts = resolveTargetParts(sourceFile, target, ctx.root);
|
|
30078
|
+
const retired = checkRetiredSkill(input);
|
|
30079
|
+
if (retired)
|
|
30080
|
+
return [retired];
|
|
30081
|
+
const missingSkill = checkMissingSkill(input, parts.relSource);
|
|
30082
|
+
if (missingSkill)
|
|
30083
|
+
return [missingSkill];
|
|
30084
|
+
const agent = checkAgentFile(input, parts.resolved, parts.relSource);
|
|
30085
|
+
if (agent)
|
|
30086
|
+
return [agent];
|
|
30087
|
+
const brokenPath = checkBrokenPath({ input, ...parts });
|
|
30088
|
+
if (brokenPath)
|
|
30089
|
+
return [brokenPath];
|
|
30090
|
+
const brokenAnchor = checkBrokenAnchor({ input, ...parts });
|
|
30091
|
+
return brokenAnchor ? [brokenAnchor] : [];
|
|
29740
30092
|
}
|
|
29741
30093
|
function runLinksRule(ctx) {
|
|
29742
30094
|
const issues = [];
|
|
@@ -29745,7 +30097,7 @@ function runLinksRule(ctx) {
|
|
|
29745
30097
|
const links = extractLinksFromMarkdown(content3, filePath);
|
|
29746
30098
|
for (const { target, line } of links) {
|
|
29747
30099
|
const linkLabel = line ? `line ${line}` : target;
|
|
29748
|
-
issues.push(...validateTarget(ctx, filePath, target, linkLabel));
|
|
30100
|
+
issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
|
|
29749
30101
|
}
|
|
29750
30102
|
}
|
|
29751
30103
|
return issues;
|
|
@@ -29753,6 +30105,44 @@ function runLinksRule(ctx) {
|
|
|
29753
30105
|
var linksRule = { id: "links", run: runLinksRule };
|
|
29754
30106
|
|
|
29755
30107
|
// src/audit/rules/prose-policy.ts
|
|
30108
|
+
function checkDraftEntry(input) {
|
|
30109
|
+
const { rel, lines, entry, draftPrefixes } = input;
|
|
30110
|
+
const issues = [];
|
|
30111
|
+
for (let i = 0;i < lines.length; i++) {
|
|
30112
|
+
if (entry.regex?.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
|
|
30113
|
+
issues.push(issue("prose-policy", rel, {
|
|
30114
|
+
message: entry.message,
|
|
30115
|
+
link: `line ${i + 1}`,
|
|
30116
|
+
severity: entry.severity
|
|
30117
|
+
}));
|
|
30118
|
+
}
|
|
30119
|
+
}
|
|
30120
|
+
return issues;
|
|
30121
|
+
}
|
|
30122
|
+
function checkLineEntry(rel, lines, entry) {
|
|
30123
|
+
const issues = [];
|
|
30124
|
+
for (let i = 0;i < lines.length; i++) {
|
|
30125
|
+
if (entry.regex?.test(lines[i] ?? "")) {
|
|
30126
|
+
issues.push(issue("prose-policy", rel, {
|
|
30127
|
+
message: entry.message,
|
|
30128
|
+
link: `line ${i + 1}`,
|
|
30129
|
+
severity: entry.severity
|
|
30130
|
+
}));
|
|
30131
|
+
}
|
|
30132
|
+
}
|
|
30133
|
+
return issues;
|
|
30134
|
+
}
|
|
30135
|
+
function checkPolicyEntry(input) {
|
|
30136
|
+
const { rel, content: content3, lines, entry, draftPrefixes } = input;
|
|
30137
|
+
if (entry.id === "draft-marker") {
|
|
30138
|
+
return checkDraftEntry({ rel, lines, entry, draftPrefixes });
|
|
30139
|
+
}
|
|
30140
|
+
const isMultiline = entry.pattern?.includes("[\\s\\S]");
|
|
30141
|
+
if (isMultiline) {
|
|
30142
|
+
return entry.regex?.test(content3) ? [issue("prose-policy", rel, { message: entry.message, severity: entry.severity })] : [];
|
|
30143
|
+
}
|
|
30144
|
+
return checkLineEntry(rel, lines, entry);
|
|
30145
|
+
}
|
|
29756
30146
|
function runProsePolicyRule(ctx) {
|
|
29757
30147
|
if (ctx.policies.length === 0)
|
|
29758
30148
|
return [];
|
|
@@ -29765,36 +30155,9 @@ function runProsePolicyRule(ctx) {
|
|
|
29765
30155
|
`);
|
|
29766
30156
|
const policies = policiesForFile(ctx.policies, rel);
|
|
29767
30157
|
for (const entry of policies) {
|
|
29768
|
-
if (entry.mode === "fingerprint")
|
|
30158
|
+
if (entry.mode === "fingerprint" || !entry.regex)
|
|
29769
30159
|
continue;
|
|
29770
|
-
|
|
29771
|
-
continue;
|
|
29772
|
-
if (entry.id === "draft-marker") {
|
|
29773
|
-
for (let i = 0;i < lines.length; i++) {
|
|
29774
|
-
if (entry.regex.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
|
|
29775
|
-
issues.push(issue("prose-policy", rel, entry.message, {
|
|
29776
|
-
link: `line ${i + 1}`,
|
|
29777
|
-
severity: entry.severity
|
|
29778
|
-
}));
|
|
29779
|
-
}
|
|
29780
|
-
}
|
|
29781
|
-
continue;
|
|
29782
|
-
}
|
|
29783
|
-
const isMultiline = entry.pattern?.includes("[\\s\\S]");
|
|
29784
|
-
if (isMultiline) {
|
|
29785
|
-
if (entry.regex.test(content3)) {
|
|
29786
|
-
issues.push(issue("prose-policy", rel, entry.message, { severity: entry.severity }));
|
|
29787
|
-
}
|
|
29788
|
-
continue;
|
|
29789
|
-
}
|
|
29790
|
-
for (let i = 0;i < lines.length; i++) {
|
|
29791
|
-
if (entry.regex.test(lines[i] ?? "")) {
|
|
29792
|
-
issues.push(issue("prose-policy", rel, entry.message, {
|
|
29793
|
-
link: `line ${i + 1}`,
|
|
29794
|
-
severity: entry.severity
|
|
29795
|
-
}));
|
|
29796
|
-
}
|
|
29797
|
-
}
|
|
30160
|
+
issues.push(...checkPolicyEntry({ rel, content: content3, lines, entry, draftPrefixes }));
|
|
29798
30161
|
}
|
|
29799
30162
|
}
|
|
29800
30163
|
return issues;
|
|
@@ -29804,12 +30167,8 @@ var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
|
|
|
29804
30167
|
// src/audit/rules/registry.ts
|
|
29805
30168
|
import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
|
|
29806
30169
|
import { join as join13 } from "node:path";
|
|
29807
|
-
function
|
|
30170
|
+
function checkRegistryEntries(ctx) {
|
|
29808
30171
|
const issues = [];
|
|
29809
|
-
const registry = new Set(ctx.registryPaths);
|
|
29810
|
-
if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
|
|
29811
|
-
issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
|
|
29812
|
-
}
|
|
29813
30172
|
for (const rel of ctx.registryPaths) {
|
|
29814
30173
|
const abs = join13(ctx.root, rel);
|
|
29815
30174
|
if (!existsSync16(abs)) {
|
|
@@ -29821,11 +30180,15 @@ function runRegistryRule(ctx) {
|
|
|
29821
30180
|
issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
|
|
29822
30181
|
}
|
|
29823
30182
|
}
|
|
30183
|
+
return issues;
|
|
30184
|
+
}
|
|
30185
|
+
function checkUnregisteredBanners(ctx, registry) {
|
|
30186
|
+
const issues = [];
|
|
29824
30187
|
for (const filePath of ctx.files) {
|
|
29825
30188
|
const rel = relPath(filePath, ctx.root);
|
|
29826
30189
|
if (rel === REGISTRY_REL_PATH)
|
|
29827
30190
|
continue;
|
|
29828
|
-
if (!rel.endsWith(".md")
|
|
30191
|
+
if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
|
|
29829
30192
|
continue;
|
|
29830
30193
|
const content3 = readFileSync13(filePath, "utf8");
|
|
29831
30194
|
if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
|
|
@@ -29836,6 +30199,16 @@ function runRegistryRule(ctx) {
|
|
|
29836
30199
|
}
|
|
29837
30200
|
return issues;
|
|
29838
30201
|
}
|
|
30202
|
+
function runRegistryRule(ctx) {
|
|
30203
|
+
const issues = [];
|
|
30204
|
+
const registry = new Set(ctx.registryPaths);
|
|
30205
|
+
if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
|
|
30206
|
+
issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
|
|
30207
|
+
}
|
|
30208
|
+
issues.push(...checkRegistryEntries(ctx));
|
|
30209
|
+
issues.push(...checkUnregisteredBanners(ctx, registry));
|
|
30210
|
+
return issues;
|
|
30211
|
+
}
|
|
29839
30212
|
var registryRule = { id: "registry", run: runRegistryRule };
|
|
29840
30213
|
|
|
29841
30214
|
// src/audit/rules/scan-gaps.ts
|
|
@@ -29847,7 +30220,10 @@ function runCoverageGapsRule(ctx) {
|
|
|
29847
30220
|
for (const rel of candidates) {
|
|
29848
30221
|
if (scanned.has(rel))
|
|
29849
30222
|
continue;
|
|
29850
|
-
issues.push(issue("coverage-gaps", rel,
|
|
30223
|
+
issues.push(issue("coverage-gaps", rel, {
|
|
30224
|
+
message: "markdown outside audit scan perimeter — extend .skeleton/config.yaml scan.include",
|
|
30225
|
+
severity: "warning"
|
|
30226
|
+
}));
|
|
29851
30227
|
}
|
|
29852
30228
|
return issues;
|
|
29853
30229
|
}
|
|
@@ -29917,70 +30293,79 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
|
29917
30293
|
}
|
|
29918
30294
|
return issues;
|
|
29919
30295
|
}
|
|
30296
|
+
function taxonomyIssuesForReadme(input) {
|
|
30297
|
+
const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
|
|
30298
|
+
const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
|
|
30299
|
+
if (!existsSync17(readmePath))
|
|
30300
|
+
return [];
|
|
30301
|
+
const readme = readFileSync14(readmePath, "utf8");
|
|
30302
|
+
if (!readme.includes("## Taxonomy"))
|
|
30303
|
+
return [];
|
|
30304
|
+
const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
|
|
30305
|
+
const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
|
|
30306
|
+
const foreign = new Set(index2.foreignSlugs);
|
|
30307
|
+
const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
|
|
30308
|
+
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
30309
|
+
const issues = [];
|
|
30310
|
+
for (const slug2 of publicSlugs) {
|
|
30311
|
+
if (!taxonomySlugs.includes(slug2)) {
|
|
30312
|
+
issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
|
|
30313
|
+
}
|
|
30314
|
+
}
|
|
30315
|
+
for (const slug2 of taxonomySlugs) {
|
|
30316
|
+
if (!nestedSlugs.includes(slug2)) {
|
|
30317
|
+
issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
|
|
30318
|
+
}
|
|
30319
|
+
}
|
|
30320
|
+
return issues;
|
|
30321
|
+
}
|
|
29920
30322
|
function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
|
|
29921
30323
|
const issues = [];
|
|
29922
30324
|
const nonPublic = new Set(nonPublicSkills(ctx.config));
|
|
29923
30325
|
for (const skillRoot of index2.roots) {
|
|
29924
30326
|
if (skillRoot.kind !== "nested")
|
|
29925
30327
|
continue;
|
|
29926
|
-
|
|
29927
|
-
|
|
29928
|
-
|
|
29929
|
-
|
|
29930
|
-
|
|
30328
|
+
issues.push(...taxonomyIssuesForReadme({ ctx, index: index2, skillRoot, diskSlugs, nonPublic }));
|
|
30329
|
+
}
|
|
30330
|
+
return issues;
|
|
30331
|
+
}
|
|
30332
|
+
function slugsForRoot(skillRoot, index2, owned) {
|
|
30333
|
+
return skillRoot.kind === "nested" ? index2.ownedSlugs : index2.flatSlugs.filter((slug2) => owned.has(slug2));
|
|
30334
|
+
}
|
|
30335
|
+
function auditSkillRoot(input) {
|
|
30336
|
+
const { ctx, index: index2, skillRoot, owned } = input;
|
|
30337
|
+
const issues = [];
|
|
30338
|
+
const base = skillRoot.kind === "nested" ? join14(ctx.root, skillRoot.relPath) : ctx.root;
|
|
30339
|
+
for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
|
|
30340
|
+
const skillDir = join14(base, slug2);
|
|
30341
|
+
if (!existsSync17(skillDir))
|
|
29931
30342
|
continue;
|
|
29932
|
-
const
|
|
29933
|
-
|
|
29934
|
-
const foreign = new Set(index2.foreignSlugs);
|
|
29935
|
-
const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
|
|
29936
|
-
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
29937
|
-
for (const slug2 of publicSlugs) {
|
|
29938
|
-
if (!taxonomySlugs.includes(slug2)) {
|
|
29939
|
-
issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
|
|
29940
|
-
}
|
|
29941
|
-
}
|
|
29942
|
-
for (const slug2 of taxonomySlugs) {
|
|
29943
|
-
if (!nestedSlugs.includes(slug2)) {
|
|
29944
|
-
issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
|
|
29945
|
-
}
|
|
30343
|
+
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
30344
|
+
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29946
30345
|
}
|
|
29947
30346
|
}
|
|
29948
30347
|
return issues;
|
|
29949
30348
|
}
|
|
30349
|
+
function auditOwnedSkillFiles(ctx, index2) {
|
|
30350
|
+
const owned = new Set(index2.ownedSlugs);
|
|
30351
|
+
const issues = [];
|
|
30352
|
+
for (const skillRoot of index2.roots) {
|
|
30353
|
+
issues.push(...auditSkillRoot({ ctx, index: index2, skillRoot, owned }));
|
|
30354
|
+
}
|
|
30355
|
+
return issues;
|
|
30356
|
+
}
|
|
29950
30357
|
function runSkillIndexRule(ctx) {
|
|
29951
30358
|
const issues = [];
|
|
29952
30359
|
const index2 = ctx.skillIndex;
|
|
29953
30360
|
const diskSlugs = listSkillSlugs(index2);
|
|
29954
30361
|
for (const warning of index2.provenance.warnings) {
|
|
29955
|
-
issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json",
|
|
30362
|
+
issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json", {
|
|
30363
|
+
message: `skill provenance: ${warning}`,
|
|
29956
30364
|
severity: "warning"
|
|
29957
30365
|
}));
|
|
29958
30366
|
}
|
|
29959
30367
|
issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
|
|
29960
|
-
|
|
29961
|
-
for (const skillRoot of index2.roots) {
|
|
29962
|
-
if (skillRoot.kind === "nested") {
|
|
29963
|
-
for (const slug2 of index2.ownedSlugs) {
|
|
29964
|
-
const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
|
|
29965
|
-
if (!existsSync17(skillDir))
|
|
29966
|
-
continue;
|
|
29967
|
-
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29968
|
-
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29969
|
-
}
|
|
29970
|
-
}
|
|
29971
|
-
continue;
|
|
29972
|
-
}
|
|
29973
|
-
for (const slug2 of index2.flatSlugs) {
|
|
29974
|
-
if (!owned.has(slug2))
|
|
29975
|
-
continue;
|
|
29976
|
-
const skillDir = join14(ctx.root, slug2);
|
|
29977
|
-
if (!existsSync17(skillDir))
|
|
29978
|
-
continue;
|
|
29979
|
-
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29980
|
-
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29981
|
-
}
|
|
29982
|
-
}
|
|
29983
|
-
}
|
|
30368
|
+
issues.push(...auditOwnedSkillFiles(ctx, index2));
|
|
29984
30369
|
return issues;
|
|
29985
30370
|
}
|
|
29986
30371
|
var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
|
|
@@ -30008,6 +30393,19 @@ var skillsRules = [
|
|
|
30008
30393
|
prosePolicyRule
|
|
30009
30394
|
];
|
|
30010
30395
|
var allRules = [...docsRules, ...skillsRules];
|
|
30396
|
+
function attachPluginRule(docs, skills, rule) {
|
|
30397
|
+
const suites = rule.suites ?? ["docs"];
|
|
30398
|
+
const inDocs = suites.includes("docs");
|
|
30399
|
+
const inSkills = suites.includes("skills");
|
|
30400
|
+
if (!(inDocs || inSkills)) {
|
|
30401
|
+
const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
|
|
30402
|
+
throw new Error(`Plugin rule "${rule.id}" suites attach to no known suite (got ${listed}; allowed: docs, skills). ` + `"self" is the union of docs+skills — put the rule in docs and/or skills.`);
|
|
30403
|
+
}
|
|
30404
|
+
if (inDocs)
|
|
30405
|
+
docs.push(rule);
|
|
30406
|
+
if (inSkills)
|
|
30407
|
+
skills.push(rule);
|
|
30408
|
+
}
|
|
30011
30409
|
function assembleRules(pluginRules = []) {
|
|
30012
30410
|
const coreIds = new Set(allRules.map((rule) => rule.id));
|
|
30013
30411
|
const seenPlugin = new Set;
|
|
@@ -30020,17 +30418,7 @@ function assembleRules(pluginRules = []) {
|
|
|
30020
30418
|
const docs = [...docsRules];
|
|
30021
30419
|
const skills = [...skillsRules];
|
|
30022
30420
|
for (const rule of pluginRules) {
|
|
30023
|
-
|
|
30024
|
-
const inDocs = suites.includes("docs");
|
|
30025
|
-
const inSkills = suites.includes("skills");
|
|
30026
|
-
if (!inDocs && !inSkills) {
|
|
30027
|
-
const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
|
|
30028
|
-
throw new Error(`Plugin rule "${rule.id}" suites attach to no known suite (got ${listed}; allowed: docs, skills). ` + `"self" is the union of docs+skills — put the rule in docs and/or skills.`);
|
|
30029
|
-
}
|
|
30030
|
-
if (inDocs)
|
|
30031
|
-
docs.push(rule);
|
|
30032
|
-
if (inSkills)
|
|
30033
|
-
skills.push(rule);
|
|
30421
|
+
attachPluginRule(docs, skills, rule);
|
|
30034
30422
|
}
|
|
30035
30423
|
const selfById = new Map;
|
|
30036
30424
|
for (const rule of [...docs, ...skills]) {
|
|
@@ -30053,46 +30441,70 @@ function rulesForSuite(suite, pluginRules = []) {
|
|
|
30053
30441
|
}
|
|
30054
30442
|
|
|
30055
30443
|
// src/audit/run.ts
|
|
30444
|
+
function parseFixArg(argv, index2) {
|
|
30445
|
+
const next = argv[index2 + 1];
|
|
30446
|
+
if (next && !next.startsWith("-")) {
|
|
30447
|
+
if (next !== "doc-meta" && next !== "anchors") {
|
|
30448
|
+
throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
|
|
30449
|
+
}
|
|
30450
|
+
return { fix: next, nextIndex: index2 + 1 };
|
|
30451
|
+
}
|
|
30452
|
+
return { fix: true, nextIndex: index2 };
|
|
30453
|
+
}
|
|
30454
|
+
function applyAuditFlag(input) {
|
|
30455
|
+
const { state, arg, argv, index: index2 } = input;
|
|
30456
|
+
if (arg.startsWith("--suite=")) {
|
|
30457
|
+
state.suite = arg.slice("--suite=".length);
|
|
30458
|
+
return index2;
|
|
30459
|
+
}
|
|
30460
|
+
if (arg === "--strict") {
|
|
30461
|
+
state.strict = true;
|
|
30462
|
+
return index2;
|
|
30463
|
+
}
|
|
30464
|
+
if (arg === "--json") {
|
|
30465
|
+
state.json = true;
|
|
30466
|
+
return index2;
|
|
30467
|
+
}
|
|
30468
|
+
if (arg === "--dry-run") {
|
|
30469
|
+
state.dryRun = true;
|
|
30470
|
+
return index2;
|
|
30471
|
+
}
|
|
30472
|
+
if (arg.startsWith("--dry-run=")) {
|
|
30473
|
+
throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
|
|
30474
|
+
}
|
|
30475
|
+
if (arg === "--fix") {
|
|
30476
|
+
const parsed = parseFixArg(argv, index2);
|
|
30477
|
+
state.fix = parsed.fix;
|
|
30478
|
+
return parsed.nextIndex;
|
|
30479
|
+
}
|
|
30480
|
+
if (arg.startsWith("--fix=")) {
|
|
30481
|
+
state.fix = arg.slice("--fix=".length);
|
|
30482
|
+
return index2;
|
|
30483
|
+
}
|
|
30484
|
+
if (arg.startsWith("--paths=")) {
|
|
30485
|
+
state.paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
|
|
30486
|
+
return index2;
|
|
30487
|
+
}
|
|
30488
|
+
if (arg.startsWith("--only=")) {
|
|
30489
|
+
state.only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
|
|
30490
|
+
return index2;
|
|
30491
|
+
}
|
|
30492
|
+
return index2;
|
|
30493
|
+
}
|
|
30056
30494
|
function parseAuditArgs(argv) {
|
|
30057
|
-
|
|
30058
|
-
|
|
30059
|
-
|
|
30060
|
-
|
|
30061
|
-
|
|
30062
|
-
|
|
30063
|
-
|
|
30495
|
+
const state = {
|
|
30496
|
+
suite: "docs",
|
|
30497
|
+
strict: false,
|
|
30498
|
+
json: false,
|
|
30499
|
+
dryRun: false,
|
|
30500
|
+
paths: [],
|
|
30501
|
+
only: null,
|
|
30502
|
+
fix: null
|
|
30503
|
+
};
|
|
30064
30504
|
for (let i = 0;i < argv.length; i++) {
|
|
30065
|
-
|
|
30066
|
-
if (arg.startsWith("--suite=")) {
|
|
30067
|
-
suite = arg.slice("--suite=".length);
|
|
30068
|
-
} else if (arg === "--strict") {
|
|
30069
|
-
strict = true;
|
|
30070
|
-
} else if (arg === "--json") {
|
|
30071
|
-
json = true;
|
|
30072
|
-
} else if (arg === "--dry-run") {
|
|
30073
|
-
dryRun = true;
|
|
30074
|
-
} else if (arg.startsWith("--dry-run=")) {
|
|
30075
|
-
throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
|
|
30076
|
-
} else if (arg === "--fix") {
|
|
30077
|
-
const next = argv[i + 1];
|
|
30078
|
-
if (next && !next.startsWith("-")) {
|
|
30079
|
-
if (next !== "doc-meta" && next !== "anchors") {
|
|
30080
|
-
throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
|
|
30081
|
-
}
|
|
30082
|
-
fix = next;
|
|
30083
|
-
i++;
|
|
30084
|
-
} else {
|
|
30085
|
-
fix = true;
|
|
30086
|
-
}
|
|
30087
|
-
} else if (arg.startsWith("--fix=")) {
|
|
30088
|
-
fix = arg.slice("--fix=".length);
|
|
30089
|
-
} else if (arg.startsWith("--paths=")) {
|
|
30090
|
-
paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
|
|
30091
|
-
} else if (arg.startsWith("--only=")) {
|
|
30092
|
-
only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
|
|
30093
|
-
}
|
|
30505
|
+
i = applyAuditFlag({ state, arg: argv[i] ?? "", argv, index: i });
|
|
30094
30506
|
}
|
|
30095
|
-
return
|
|
30507
|
+
return state;
|
|
30096
30508
|
}
|
|
30097
30509
|
function labelForSuite(suite) {
|
|
30098
30510
|
switch (suite) {
|
|
@@ -30115,6 +30527,29 @@ function shouldRunRule(rule, options, pathScoped) {
|
|
|
30115
30527
|
return false;
|
|
30116
30528
|
return true;
|
|
30117
30529
|
}
|
|
30530
|
+
async function runAuditFixes(options, ctx, loaded) {
|
|
30531
|
+
if (options.fix === null || options.fix === undefined)
|
|
30532
|
+
return null;
|
|
30533
|
+
if (options.suite !== "docs") {
|
|
30534
|
+
console.error("--fix is supported only for audit docs");
|
|
30535
|
+
return 1;
|
|
30536
|
+
}
|
|
30537
|
+
const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
|
|
30538
|
+
if (kinds.length === 0) {
|
|
30539
|
+
console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
|
|
30540
|
+
return 1;
|
|
30541
|
+
}
|
|
30542
|
+
applyFixes(ctx, { kinds, dryRun: options.dryRun });
|
|
30543
|
+
if (!options.dryRun) {
|
|
30544
|
+
const refreshed = createContext({
|
|
30545
|
+
root: options.root,
|
|
30546
|
+
paths: options.paths.length > 0 ? options.paths : undefined,
|
|
30547
|
+
policies: loaded.policies
|
|
30548
|
+
});
|
|
30549
|
+
Object.assign(ctx, refreshed);
|
|
30550
|
+
}
|
|
30551
|
+
return null;
|
|
30552
|
+
}
|
|
30118
30553
|
async function runAudit(options) {
|
|
30119
30554
|
const pathScoped = options.paths.length > 0;
|
|
30120
30555
|
const base = createContext({
|
|
@@ -30124,26 +30559,9 @@ async function runAudit(options) {
|
|
|
30124
30559
|
});
|
|
30125
30560
|
const loaded = await loadPlugins(base.root, base.config);
|
|
30126
30561
|
const ctx = { ...base, policies: loaded.policies };
|
|
30127
|
-
|
|
30128
|
-
|
|
30129
|
-
|
|
30130
|
-
return 1;
|
|
30131
|
-
}
|
|
30132
|
-
const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
|
|
30133
|
-
if (kinds.length === 0) {
|
|
30134
|
-
console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
|
|
30135
|
-
return 1;
|
|
30136
|
-
}
|
|
30137
|
-
applyFixes(ctx, { kinds, dryRun: options.dryRun });
|
|
30138
|
-
if (!options.dryRun) {
|
|
30139
|
-
const refreshed = createContext({
|
|
30140
|
-
root: options.root,
|
|
30141
|
-
paths: options.paths.length > 0 ? options.paths : undefined,
|
|
30142
|
-
policies: loaded.policies
|
|
30143
|
-
});
|
|
30144
|
-
Object.assign(ctx, refreshed);
|
|
30145
|
-
}
|
|
30146
|
-
}
|
|
30562
|
+
const fixExit = await runAuditFixes(options, ctx, loaded);
|
|
30563
|
+
if (fixExit !== null)
|
|
30564
|
+
return fixExit;
|
|
30147
30565
|
const rules = rulesForSuite(options.suite, loaded.rules).filter((r) => !options.only || options.only.has(r.id));
|
|
30148
30566
|
const skipGlobalsForPaths = pathScoped && !options.globalOnly;
|
|
30149
30567
|
const issues = [];
|
|
@@ -30173,7 +30591,7 @@ function customizePathForSlug(root2, slug2) {
|
|
|
30173
30591
|
return join15(customizeDir(root2), `${slug2}.md`);
|
|
30174
30592
|
}
|
|
30175
30593
|
function findCustomizeViaRegistry(root2, slug2) {
|
|
30176
|
-
for (const rel of
|
|
30594
|
+
for (const rel of parseRegistry(root2).paths) {
|
|
30177
30595
|
const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
|
|
30178
30596
|
if (normalizeRelPath(rel) === expected && existsSync18(join15(root2, rel))) {
|
|
30179
30597
|
return rel;
|
|
@@ -30226,11 +30644,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
|
|
|
30226
30644
|
function resolveCustomize(root2, slug2) {
|
|
30227
30645
|
const slugFile = resolveSlugFile(root2, slug2);
|
|
30228
30646
|
const alwaysNames = alwaysIncludeBasenames(root2);
|
|
30229
|
-
const skip = slugFile.path
|
|
30647
|
+
const skip = slugFile.path !== null && slugFile.path !== undefined ? basename3(slugFile.path) : null;
|
|
30230
30648
|
const always = readAlwaysInclude(root2, alwaysNames, skip);
|
|
30231
30649
|
const parts = [];
|
|
30232
30650
|
const included = [];
|
|
30233
|
-
if (slugFile.content
|
|
30651
|
+
if (slugFile.content !== null && slugFile.content !== undefined && slugFile.content.trim().length > 0) {
|
|
30234
30652
|
parts.push(slugFile.content.trimEnd());
|
|
30235
30653
|
if (slugFile.path)
|
|
30236
30654
|
included.push(slugFile.path);
|
|
@@ -30258,6 +30676,7 @@ function resolveCustomizeFromRoot(slug2, startDir) {
|
|
|
30258
30676
|
}
|
|
30259
30677
|
|
|
30260
30678
|
// src/hooks/run.ts
|
|
30679
|
+
import process3 from "node:process";
|
|
30261
30680
|
function parsePayload(raw) {
|
|
30262
30681
|
if (!raw.trim())
|
|
30263
30682
|
return {};
|
|
@@ -30288,7 +30707,7 @@ function extractSkillSlug(payload) {
|
|
|
30288
30707
|
}
|
|
30289
30708
|
const path2 = extractPath(payload);
|
|
30290
30709
|
if (path2)
|
|
30291
|
-
return slugFromPath(path2,
|
|
30710
|
+
return slugFromPath(path2, process3.cwd());
|
|
30292
30711
|
return null;
|
|
30293
30712
|
}
|
|
30294
30713
|
function cursorResponse(content3) {
|
|
@@ -30343,6 +30762,7 @@ Customize override for /${slug2} (from ${from}):
|
|
|
30343
30762
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
30344
30763
|
import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
|
|
30345
30764
|
import { join as join19 } from "node:path";
|
|
30765
|
+
import process4 from "node:process";
|
|
30346
30766
|
|
|
30347
30767
|
// src/init/merge-hooks.ts
|
|
30348
30768
|
import { existsSync as existsSync21, mkdirSync, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -30412,7 +30832,7 @@ function walkNodeModulesCli(cwd) {
|
|
|
30412
30832
|
}
|
|
30413
30833
|
function isInsidePackageRoot(cwd) {
|
|
30414
30834
|
const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
|
|
30415
|
-
return rel === "" || !rel.startsWith("..")
|
|
30835
|
+
return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
|
|
30416
30836
|
}
|
|
30417
30837
|
function nodeCliHookCommand(cliPath) {
|
|
30418
30838
|
return `node ${cliPath} hook customize`;
|
|
@@ -30462,6 +30882,36 @@ function writeJson(path2, value) {
|
|
|
30462
30882
|
function deepEqual(a, b) {
|
|
30463
30883
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
30464
30884
|
}
|
|
30885
|
+
function mergeExistingCursorHook(input) {
|
|
30886
|
+
const { postToolUse, skeletonIdx, canonical, opts } = input;
|
|
30887
|
+
const current = postToolUse[skeletonIdx];
|
|
30888
|
+
const userEdited = current && !opts.forceHooks && (current.matcher !== canonical.matcher || !isSkeletonHookCommand(current.command));
|
|
30889
|
+
if (userEdited) {
|
|
30890
|
+
return {
|
|
30891
|
+
platform: "cursor",
|
|
30892
|
+
action: "conflict",
|
|
30893
|
+
message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
|
|
30894
|
+
};
|
|
30895
|
+
}
|
|
30896
|
+
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
|
|
30897
|
+
const merged = { ...extras, ...canonical };
|
|
30898
|
+
if (deepEqual(current, merged))
|
|
30899
|
+
return { platform: "cursor", action: "skipped" };
|
|
30900
|
+
postToolUse[skeletonIdx] = merged;
|
|
30901
|
+
return null;
|
|
30902
|
+
}
|
|
30903
|
+
function persistCursorHooks(input) {
|
|
30904
|
+
const { targetPath, existing, hooks, postToolUse, skeletonIdx } = input;
|
|
30905
|
+
const next = {
|
|
30906
|
+
...existing,
|
|
30907
|
+
version: existing.version ?? 1,
|
|
30908
|
+
hooks: { ...hooks, postToolUse }
|
|
30909
|
+
};
|
|
30910
|
+
if (deepEqual(existing, next))
|
|
30911
|
+
return { platform: "cursor", action: "skipped" };
|
|
30912
|
+
writeJson(targetPath, next);
|
|
30913
|
+
return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
|
|
30914
|
+
}
|
|
30465
30915
|
function mergeCursorHooks(targetPath, fragment, opts) {
|
|
30466
30916
|
const existing = readJson(targetPath) ?? {};
|
|
30467
30917
|
const hooks = existing.hooks ?? {};
|
|
@@ -30472,79 +30922,103 @@ function mergeCursorHooks(targetPath, fragment, opts) {
|
|
|
30472
30922
|
const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
30473
30923
|
const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" };
|
|
30474
30924
|
if (skeletonIdx >= 0) {
|
|
30475
|
-
const
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
return {
|
|
30479
|
-
platform: "cursor",
|
|
30480
|
-
action: "conflict",
|
|
30481
|
-
message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
|
|
30482
|
-
};
|
|
30483
|
-
}
|
|
30484
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
|
|
30485
|
-
const merged = { ...extras, ...canonical };
|
|
30486
|
-
if (deepEqual(current, merged))
|
|
30487
|
-
return { platform: "cursor", action: "skipped" };
|
|
30488
|
-
postToolUse[skeletonIdx] = merged;
|
|
30925
|
+
const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts });
|
|
30926
|
+
if (conflict)
|
|
30927
|
+
return conflict;
|
|
30489
30928
|
} else {
|
|
30490
30929
|
postToolUse.push(canonical);
|
|
30491
30930
|
}
|
|
30492
|
-
|
|
30493
|
-
...existing,
|
|
30494
|
-
version: existing.version ?? 1,
|
|
30495
|
-
hooks: { ...hooks, postToolUse }
|
|
30496
|
-
};
|
|
30497
|
-
if (deepEqual(existing, next))
|
|
30498
|
-
return { platform: "cursor", action: "skipped" };
|
|
30499
|
-
writeJson(targetPath, next);
|
|
30500
|
-
return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
|
|
30931
|
+
return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx });
|
|
30501
30932
|
}
|
|
30502
|
-
function
|
|
30503
|
-
const
|
|
30504
|
-
const
|
|
30505
|
-
const
|
|
30506
|
-
|
|
30933
|
+
function mergeExistingNestedSkeletonHook(input) {
|
|
30934
|
+
const { hooks, skeletonIdx, incomingHook, opts, platform, eventName, matcher } = input;
|
|
30935
|
+
const current = hooks[skeletonIdx];
|
|
30936
|
+
const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
|
|
30937
|
+
if (userEdited) {
|
|
30938
|
+
return {
|
|
30939
|
+
changed: false,
|
|
30940
|
+
conflict: {
|
|
30941
|
+
platform,
|
|
30942
|
+
action: "conflict",
|
|
30943
|
+
message: identityKey(platform, eventName, matcher)
|
|
30944
|
+
}
|
|
30945
|
+
};
|
|
30946
|
+
}
|
|
30947
|
+
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
|
|
30948
|
+
const merged = { ...extras, ...incomingHook };
|
|
30949
|
+
if (!deepEqual(current, merged)) {
|
|
30950
|
+
hooks[skeletonIdx] = merged;
|
|
30951
|
+
return { changed: true };
|
|
30952
|
+
}
|
|
30953
|
+
return { changed: false };
|
|
30954
|
+
}
|
|
30955
|
+
function mergeIncomingHookGroup(args) {
|
|
30956
|
+
const { group, incomingHook, eventHooks, opts, platform, eventName } = args;
|
|
30957
|
+
const matcher = group.matcher ?? "";
|
|
30958
|
+
const groupIdx = eventHooks.findIndex((g) => g.matcher === matcher);
|
|
30959
|
+
if (groupIdx < 0) {
|
|
30960
|
+
eventHooks.push({ ...group, hooks: [{ ...incomingHook }] });
|
|
30961
|
+
return { changed: true };
|
|
30962
|
+
}
|
|
30963
|
+
const existingGroup = eventHooks[groupIdx];
|
|
30964
|
+
const hooks = [...existingGroup?.hooks ?? []];
|
|
30965
|
+
const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
30966
|
+
if (skeletonIdx >= 0) {
|
|
30967
|
+
const result = mergeExistingNestedSkeletonHook({
|
|
30968
|
+
hooks,
|
|
30969
|
+
skeletonIdx,
|
|
30970
|
+
incomingHook,
|
|
30971
|
+
opts,
|
|
30972
|
+
platform,
|
|
30973
|
+
eventName,
|
|
30974
|
+
matcher
|
|
30975
|
+
});
|
|
30976
|
+
if (result.conflict)
|
|
30977
|
+
return { changed: false, conflict: result.conflict };
|
|
30978
|
+
eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
|
|
30979
|
+
return { changed: result.changed };
|
|
30980
|
+
}
|
|
30981
|
+
hooks.push({ ...incomingHook });
|
|
30982
|
+
eventHooks[groupIdx] = { ...existingGroup, matcher, hooks };
|
|
30983
|
+
return { changed: true };
|
|
30984
|
+
}
|
|
30985
|
+
function mergeIncomingGroups(args) {
|
|
30507
30986
|
let changed = false;
|
|
30508
|
-
for (const incomingGroup of incomingGroups) {
|
|
30509
|
-
const matcher = incomingGroup.matcher ?? "";
|
|
30987
|
+
for (const incomingGroup of args.incomingGroups) {
|
|
30510
30988
|
const incomingHook = incomingGroup.hooks?.[0];
|
|
30511
30989
|
if (!incomingHook)
|
|
30512
30990
|
continue;
|
|
30513
|
-
const
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30517
|
-
|
|
30518
|
-
|
|
30519
|
-
|
|
30520
|
-
|
|
30521
|
-
|
|
30522
|
-
|
|
30523
|
-
|
|
30524
|
-
const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
30525
|
-
if (skeletonIdx >= 0) {
|
|
30526
|
-
const current = hooks[skeletonIdx];
|
|
30527
|
-
const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
|
|
30528
|
-
if (userEdited) {
|
|
30529
|
-
return {
|
|
30530
|
-
platform,
|
|
30531
|
-
action: "conflict",
|
|
30532
|
-
message: identityKey(platform, eventName, matcher)
|
|
30533
|
-
};
|
|
30534
|
-
}
|
|
30535
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
|
|
30536
|
-
const merged = { ...extras, ...incomingHook };
|
|
30537
|
-
if (!deepEqual(current, merged)) {
|
|
30538
|
-
hooks[skeletonIdx] = merged;
|
|
30539
|
-
changed = true;
|
|
30540
|
-
}
|
|
30541
|
-
} else {
|
|
30542
|
-
hooks.push({ ...incomingHook });
|
|
30991
|
+
const result = mergeIncomingHookGroup({
|
|
30992
|
+
group: incomingGroup,
|
|
30993
|
+
incomingHook,
|
|
30994
|
+
eventHooks: args.eventHooks,
|
|
30995
|
+
opts: args.opts,
|
|
30996
|
+
platform: args.platform,
|
|
30997
|
+
eventName: args.eventName
|
|
30998
|
+
});
|
|
30999
|
+
if (result.conflict)
|
|
31000
|
+
return { changed: false, conflict: result.conflict };
|
|
31001
|
+
if (result.changed)
|
|
30543
31002
|
changed = true;
|
|
30544
|
-
}
|
|
30545
|
-
eventHooks[groupIdx] = { ...group, matcher, hooks };
|
|
30546
31003
|
}
|
|
30547
|
-
|
|
31004
|
+
return { changed };
|
|
31005
|
+
}
|
|
31006
|
+
function mergeNestedHooks(args) {
|
|
31007
|
+
const { platform, targetPath, eventName } = args;
|
|
31008
|
+
const existing = readJson(targetPath) ?? {};
|
|
31009
|
+
const rootHooks = existing.hooks ?? {};
|
|
31010
|
+
const eventHooks = [...rootHooks[eventName] ?? []];
|
|
31011
|
+
const incomingGroups = args.fragment.hooks?.[eventName] ?? [];
|
|
31012
|
+
const result = mergeIncomingGroups({
|
|
31013
|
+
incomingGroups,
|
|
31014
|
+
eventHooks,
|
|
31015
|
+
opts: args.opts,
|
|
31016
|
+
platform,
|
|
31017
|
+
eventName
|
|
31018
|
+
});
|
|
31019
|
+
if (result.conflict)
|
|
31020
|
+
return result.conflict;
|
|
31021
|
+
if (!result.changed)
|
|
30548
31022
|
return { platform, action: "skipped" };
|
|
30549
31023
|
const next = {
|
|
30550
31024
|
...existing,
|
|
@@ -30560,11 +31034,23 @@ function mergeHookConfigs(opts) {
|
|
|
30560
31034
|
results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
|
|
30561
31035
|
const claudePath = join18(opts.cwd, ".claude/settings.json");
|
|
30562
31036
|
const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
|
|
30563
|
-
results.push(mergeNestedHooks(
|
|
31037
|
+
results.push(mergeNestedHooks({
|
|
31038
|
+
platform: "claude",
|
|
31039
|
+
targetPath: claudePath,
|
|
31040
|
+
fragment: claudeFragment,
|
|
31041
|
+
eventName: "PostToolUse",
|
|
31042
|
+
opts
|
|
31043
|
+
}));
|
|
30564
31044
|
const codexPath = join18(opts.cwd, ".codex/hooks.json");
|
|
30565
31045
|
if (existsSync21(join18(opts.cwd, ".codex"))) {
|
|
30566
31046
|
const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
|
|
30567
|
-
results.push(mergeNestedHooks(
|
|
31047
|
+
results.push(mergeNestedHooks({
|
|
31048
|
+
platform: "codex",
|
|
31049
|
+
targetPath: codexPath,
|
|
31050
|
+
fragment: codexFragment,
|
|
31051
|
+
eventName: "PostToolUse",
|
|
31052
|
+
opts
|
|
31053
|
+
}));
|
|
30568
31054
|
} else {
|
|
30569
31055
|
results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" });
|
|
30570
31056
|
}
|
|
@@ -30669,26 +31155,36 @@ function runSkillsAdd(args, cwd) {
|
|
|
30669
31155
|
});
|
|
30670
31156
|
return result.status ?? 1;
|
|
30671
31157
|
}
|
|
31158
|
+
function logHookMergeResult(result) {
|
|
31159
|
+
if (result.action === "conflict") {
|
|
31160
|
+
console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
|
|
31161
|
+
return;
|
|
31162
|
+
}
|
|
31163
|
+
if (result.action === "added")
|
|
31164
|
+
console.log(`init: added ${result.platform} customize hook`);
|
|
31165
|
+
if (result.action === "updated")
|
|
31166
|
+
console.log(`init: updated ${result.platform} customize hook`);
|
|
31167
|
+
}
|
|
31168
|
+
function installSkillsIfRequested(options, cwd) {
|
|
31169
|
+
if (!(options.skills && !options.noSkills))
|
|
31170
|
+
return "skipped";
|
|
31171
|
+
const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
|
|
31172
|
+
const run = options.runSkillsCommand ?? runSkillsAdd;
|
|
31173
|
+
const exitCode = run(args, cwd);
|
|
31174
|
+
if (exitCode !== 0)
|
|
31175
|
+
throw new Error(`skills install failed: npx ${args.join(" ")}`);
|
|
31176
|
+
console.log("init: installed /skeleton skill");
|
|
31177
|
+
return "installed";
|
|
31178
|
+
}
|
|
30672
31179
|
function runInit(options = {}) {
|
|
30673
|
-
const cwd = options.cwd ??
|
|
31180
|
+
const cwd = options.cwd ?? process4.cwd();
|
|
30674
31181
|
assertPackageResolvable(cwd);
|
|
30675
31182
|
const scaffold = writeScaffold(cwd);
|
|
30676
31183
|
const hookCommand = resolveHookCommand(cwd);
|
|
30677
|
-
const hooks = mergeHookConfigs({
|
|
30678
|
-
cwd,
|
|
30679
|
-
hookCommand,
|
|
30680
|
-
forceHooks: options.forceHooks
|
|
30681
|
-
});
|
|
31184
|
+
const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
|
|
30682
31185
|
const scripts = mergePackageJsonScripts(cwd);
|
|
30683
|
-
for (const result of hooks)
|
|
30684
|
-
|
|
30685
|
-
console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
|
|
30686
|
-
} else if (result.action === "added") {
|
|
30687
|
-
console.log(`init: added ${result.platform} customize hook`);
|
|
30688
|
-
} else if (result.action === "updated") {
|
|
30689
|
-
console.log(`init: updated ${result.platform} customize hook`);
|
|
30690
|
-
}
|
|
30691
|
-
}
|
|
31186
|
+
for (const result of hooks)
|
|
31187
|
+
logHookMergeResult(result);
|
|
30692
31188
|
if (scaffold === "created") {
|
|
30693
31189
|
console.log("init: wrote .skeleton/config.yaml and registry.md");
|
|
30694
31190
|
} else {
|
|
@@ -30697,17 +31193,7 @@ function runInit(options = {}) {
|
|
|
30697
31193
|
if (scripts === "updated") {
|
|
30698
31194
|
console.log("init: merged validate/audit scripts into package.json");
|
|
30699
31195
|
}
|
|
30700
|
-
|
|
30701
|
-
if (options.skills && !options.noSkills) {
|
|
30702
|
-
const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
|
|
30703
|
-
const run = options.runSkillsCommand ?? runSkillsAdd;
|
|
30704
|
-
const exitCode = run(args, cwd);
|
|
30705
|
-
if (exitCode !== 0) {
|
|
30706
|
-
throw new Error(`skills install failed: npx ${args.join(" ")}`);
|
|
30707
|
-
}
|
|
30708
|
-
skills = "installed";
|
|
30709
|
-
console.log("init: installed /skeleton skill");
|
|
30710
|
-
}
|
|
31196
|
+
const skills = installSkillsIfRequested(options, cwd);
|
|
30711
31197
|
return { scaffold, hooks, scripts, skills };
|
|
30712
31198
|
}
|
|
30713
31199
|
|
|
@@ -30798,7 +31284,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
|
|
|
30798
31284
|
hash.update("\x00");
|
|
30799
31285
|
hash.update(content3);
|
|
30800
31286
|
hash.update("\x00");
|
|
30801
|
-
for (const dep of localImportPaths(abs, content3).sort()) {
|
|
31287
|
+
for (const dep of localImportPaths(abs, content3).sort((a, b) => a.localeCompare(b))) {
|
|
30802
31288
|
walk(dep);
|
|
30803
31289
|
}
|
|
30804
31290
|
}
|
|
@@ -30869,6 +31355,9 @@ async function runBuildPlugin(options = {}) {
|
|
|
30869
31355
|
return { built, checked };
|
|
30870
31356
|
}
|
|
30871
31357
|
|
|
31358
|
+
// src/references/run.ts
|
|
31359
|
+
import process6 from "node:process";
|
|
31360
|
+
|
|
30872
31361
|
// src/references/sync.ts
|
|
30873
31362
|
import {
|
|
30874
31363
|
existsSync as existsSync24,
|
|
@@ -30879,14 +31368,13 @@ import {
|
|
|
30879
31368
|
writeFileSync as writeFileSync4
|
|
30880
31369
|
} from "node:fs";
|
|
30881
31370
|
import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
|
|
31371
|
+
import process5 from "node:process";
|
|
30882
31372
|
function resolveOwnership(root2, override) {
|
|
30883
31373
|
if (override !== undefined)
|
|
30884
31374
|
return override;
|
|
30885
31375
|
try {
|
|
30886
31376
|
return loadConfig(root2).skillOwnership;
|
|
30887
|
-
} catch {
|
|
30888
|
-
return;
|
|
30889
|
-
}
|
|
31377
|
+
} catch {}
|
|
30890
31378
|
}
|
|
30891
31379
|
function walkMarkdownFiles2(dir, root2) {
|
|
30892
31380
|
const files = [];
|
|
@@ -30906,89 +31394,97 @@ function walkMarkdownFiles2(dir, root2) {
|
|
|
30906
31394
|
}
|
|
30907
31395
|
return files;
|
|
30908
31396
|
}
|
|
31397
|
+
function collectGeneratedInDir(input) {
|
|
31398
|
+
const { dir, refsDir, skill, files } = input;
|
|
31399
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
31400
|
+
const fullPath = join20(dir, entry.name);
|
|
31401
|
+
if (entry.isDirectory()) {
|
|
31402
|
+
collectGeneratedInDir({ dir: fullPath, refsDir, skill, files });
|
|
31403
|
+
continue;
|
|
31404
|
+
}
|
|
31405
|
+
if (!entry.name.endsWith(".md"))
|
|
31406
|
+
continue;
|
|
31407
|
+
const content3 = readFileSync19(fullPath, "utf8");
|
|
31408
|
+
if (!isGeneratedReference(content3))
|
|
31409
|
+
continue;
|
|
31410
|
+
const refPath = normalizeRelPath(relative11(refsDir, fullPath));
|
|
31411
|
+
files.push(generatedRefPath(skill, refPath));
|
|
31412
|
+
}
|
|
31413
|
+
}
|
|
30909
31414
|
function listGeneratedReferenceFiles(skillDir, skill) {
|
|
30910
31415
|
const refsDir = join20(skillDir, "references");
|
|
30911
31416
|
if (!existsSync24(refsDir))
|
|
30912
31417
|
return [];
|
|
30913
31418
|
const files = [];
|
|
30914
|
-
|
|
30915
|
-
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
30916
|
-
const fullPath = join20(dir, entry.name);
|
|
30917
|
-
if (entry.isDirectory()) {
|
|
30918
|
-
walk(fullPath);
|
|
30919
|
-
continue;
|
|
30920
|
-
}
|
|
30921
|
-
if (!entry.name.endsWith(".md"))
|
|
30922
|
-
continue;
|
|
30923
|
-
const content3 = readFileSync19(fullPath, "utf8");
|
|
30924
|
-
if (isGeneratedReference(content3)) {
|
|
30925
|
-
const refPath = normalizeRelPath(relative11(refsDir, fullPath));
|
|
30926
|
-
files.push(generatedRefPath(skill, refPath));
|
|
30927
|
-
}
|
|
30928
|
-
}
|
|
30929
|
-
};
|
|
30930
|
-
walk(refsDir);
|
|
31419
|
+
collectGeneratedInDir({ dir: refsDir, refsDir, skill, files });
|
|
30931
31420
|
return files;
|
|
30932
31421
|
}
|
|
31422
|
+
function syncGeneratedCopy(ctx, refPath) {
|
|
31423
|
+
const { root: root2, plan, options, result } = ctx;
|
|
31424
|
+
const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
|
|
31425
|
+
const canonicalPath = join20(root2, sourceRel);
|
|
31426
|
+
if (!existsSync24(canonicalPath)) {
|
|
31427
|
+
throw new Error(`canonical reference missing: ${sourceRel}`);
|
|
31428
|
+
}
|
|
31429
|
+
const targetRel = generatedRefPath(plan.skill, refPath);
|
|
31430
|
+
const targetPath = join20(root2, targetRel);
|
|
31431
|
+
const canonicalContent = readFileSync19(canonicalPath, "utf8");
|
|
31432
|
+
const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
|
|
31433
|
+
if (!options.dryRun)
|
|
31434
|
+
mkdirSync3(dirname12(targetPath), { recursive: true });
|
|
31435
|
+
const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
|
|
31436
|
+
if (existing !== nextContent) {
|
|
31437
|
+
if (!options.dryRun)
|
|
31438
|
+
writeFileSync4(targetPath, nextContent, "utf8");
|
|
31439
|
+
result.written.push(targetRel);
|
|
31440
|
+
} else {
|
|
31441
|
+
result.skipped.push(targetRel);
|
|
31442
|
+
}
|
|
31443
|
+
}
|
|
31444
|
+
function rewritePlanLinks(ctx, skillDir) {
|
|
31445
|
+
const { root: root2, plan, options, result } = ctx;
|
|
31446
|
+
if (options.rewriteLinks === false)
|
|
31447
|
+
return;
|
|
31448
|
+
for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
|
|
31449
|
+
const filePath = join20(root2, relFile);
|
|
31450
|
+
const content3 = readFileSync19(filePath, "utf8");
|
|
31451
|
+
const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
|
|
31452
|
+
if (next === content3)
|
|
31453
|
+
continue;
|
|
31454
|
+
if (!options.dryRun)
|
|
31455
|
+
writeFileSync4(filePath, next, "utf8");
|
|
31456
|
+
result.rewritten.push(relFile);
|
|
31457
|
+
}
|
|
31458
|
+
}
|
|
31459
|
+
function removeStaleGenerated(ctx, skillDir) {
|
|
31460
|
+
const { root: root2, plan, options, result } = ctx;
|
|
31461
|
+
for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
|
|
31462
|
+
const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
|
|
31463
|
+
if (plan.refPaths.has(refPath))
|
|
31464
|
+
continue;
|
|
31465
|
+
if (!options.dryRun)
|
|
31466
|
+
unlinkSync(join20(root2, generatedRel));
|
|
31467
|
+
result.removed.push(generatedRel);
|
|
31468
|
+
}
|
|
31469
|
+
}
|
|
31470
|
+
function syncPlan(ctx) {
|
|
31471
|
+
const skillDir = join20(ctx.root, ctx.plan.skill);
|
|
31472
|
+
for (const refPath of ctx.plan.refPaths) {
|
|
31473
|
+
syncGeneratedCopy(ctx, refPath);
|
|
31474
|
+
}
|
|
31475
|
+
rewritePlanLinks(ctx, skillDir);
|
|
31476
|
+
removeStaleGenerated(ctx, skillDir);
|
|
31477
|
+
}
|
|
30933
31478
|
function syncReferences(options = {}) {
|
|
30934
|
-
const root2 = options.root ??
|
|
31479
|
+
const root2 = options.root ?? process5.cwd();
|
|
30935
31480
|
const canonicalDir = join20(root2, CANONICAL_REFS_DIR);
|
|
30936
31481
|
if (!existsSync24(canonicalDir)) {
|
|
30937
31482
|
throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
|
|
30938
31483
|
}
|
|
30939
|
-
const result = {
|
|
30940
|
-
written: [],
|
|
30941
|
-
rewritten: [],
|
|
30942
|
-
removed: [],
|
|
30943
|
-
skipped: []
|
|
30944
|
-
};
|
|
31484
|
+
const result = { written: [], rewritten: [], removed: [], skipped: [] };
|
|
30945
31485
|
const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
|
|
30946
|
-
for (const plan of plans)
|
|
30947
|
-
|
|
30948
|
-
for (const refPath of plan.refPaths) {
|
|
30949
|
-
const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
|
|
30950
|
-
const canonicalPath = join20(root2, sourceRel);
|
|
30951
|
-
if (!existsSync24(canonicalPath)) {
|
|
30952
|
-
throw new Error(`canonical reference missing: ${sourceRel}`);
|
|
30953
|
-
}
|
|
30954
|
-
const targetRel = generatedRefPath(plan.skill, refPath);
|
|
30955
|
-
const targetPath = join20(root2, targetRel);
|
|
30956
|
-
const canonicalContent = readFileSync19(canonicalPath, "utf8");
|
|
30957
|
-
const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
|
|
30958
|
-
if (!options.dryRun) {
|
|
30959
|
-
mkdirSync3(dirname12(targetPath), { recursive: true });
|
|
30960
|
-
}
|
|
30961
|
-
const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
|
|
30962
|
-
if (existing !== nextContent) {
|
|
30963
|
-
if (!options.dryRun)
|
|
30964
|
-
writeFileSync4(targetPath, nextContent, "utf8");
|
|
30965
|
-
result.written.push(targetRel);
|
|
30966
|
-
} else {
|
|
30967
|
-
result.skipped.push(targetRel);
|
|
30968
|
-
}
|
|
30969
|
-
}
|
|
30970
|
-
if (options.rewriteLinks !== false) {
|
|
30971
|
-
for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
|
|
30972
|
-
const filePath = join20(root2, relFile);
|
|
30973
|
-
const content3 = readFileSync19(filePath, "utf8");
|
|
30974
|
-
const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
|
|
30975
|
-
if (next !== content3) {
|
|
30976
|
-
if (!options.dryRun)
|
|
30977
|
-
writeFileSync4(filePath, next, "utf8");
|
|
30978
|
-
result.rewritten.push(relFile);
|
|
30979
|
-
}
|
|
30980
|
-
}
|
|
30981
|
-
}
|
|
30982
|
-
for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
|
|
30983
|
-
const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
|
|
30984
|
-
if (!plan.refPaths.has(refPath)) {
|
|
30985
|
-
const fullPath = join20(root2, generatedRel);
|
|
30986
|
-
if (!options.dryRun)
|
|
30987
|
-
unlinkSync(fullPath);
|
|
30988
|
-
result.removed.push(generatedRel);
|
|
30989
|
-
}
|
|
30990
|
-
}
|
|
30991
|
-
}
|
|
31486
|
+
for (const plan of plans)
|
|
31487
|
+
syncPlan({ root: root2, plan, options, result });
|
|
30992
31488
|
return result;
|
|
30993
31489
|
}
|
|
30994
31490
|
|
|
@@ -30997,7 +31493,7 @@ function runReferencesSync(options = {}) {
|
|
|
30997
31493
|
return syncReferences(options);
|
|
30998
31494
|
}
|
|
30999
31495
|
function runReferencesCheck(options = {}) {
|
|
31000
|
-
const root2 = options.root ??
|
|
31496
|
+
const root2 = options.root ?? process6.cwd();
|
|
31001
31497
|
let ownership;
|
|
31002
31498
|
try {
|
|
31003
31499
|
ownership = loadConfig(root2).skillOwnership;
|
|
@@ -31011,22 +31507,17 @@ function runReferencesCheck(options = {}) {
|
|
|
31011
31507
|
label: "References check"
|
|
31012
31508
|
});
|
|
31013
31509
|
}
|
|
31510
|
+
function printSyncSection(label, files, prefix) {
|
|
31511
|
+
if (files.length === 0)
|
|
31512
|
+
return;
|
|
31513
|
+
console.log(label);
|
|
31514
|
+
for (const file of files)
|
|
31515
|
+
console.log(` ${prefix} ${file}`);
|
|
31516
|
+
}
|
|
31014
31517
|
function printSyncResult(result) {
|
|
31015
|
-
|
|
31016
|
-
|
|
31017
|
-
|
|
31018
|
-
console.log(` + ${file}`);
|
|
31019
|
-
}
|
|
31020
|
-
if (result.rewritten.length > 0) {
|
|
31021
|
-
console.log(`references sync: rewrote links in ${result.rewritten.length} file(s)`);
|
|
31022
|
-
for (const file of result.rewritten)
|
|
31023
|
-
console.log(` ~ ${file}`);
|
|
31024
|
-
}
|
|
31025
|
-
if (result.removed.length > 0) {
|
|
31026
|
-
console.log(`references sync: removed ${result.removed.length} stale file(s)`);
|
|
31027
|
-
for (const file of result.removed)
|
|
31028
|
-
console.log(` - ${file}`);
|
|
31029
|
-
}
|
|
31518
|
+
printSyncSection(`references sync: wrote ${result.written.length} file(s)`, result.written, "+");
|
|
31519
|
+
printSyncSection(`references sync: rewrote links in ${result.rewritten.length} file(s)`, result.rewritten, "~");
|
|
31520
|
+
printSyncSection(`references sync: removed ${result.removed.length} stale file(s)`, result.removed, "-");
|
|
31030
31521
|
if (result.written.length === 0 && result.rewritten.length === 0 && result.removed.length === 0) {
|
|
31031
31522
|
console.log(`references sync: up to date (${result.skipped.length} file(s) checked)`);
|
|
31032
31523
|
}
|
|
@@ -31060,7 +31551,7 @@ function parseRegistryRows(content3) {
|
|
|
31060
31551
|
for (const line of content3.split(`
|
|
31061
31552
|
`)) {
|
|
31062
31553
|
const match = REGISTRY_TABLE_ROW_RE2.exec(line);
|
|
31063
|
-
if (!match?.[1]
|
|
31554
|
+
if (!(match?.[1] && match[2]))
|
|
31064
31555
|
continue;
|
|
31065
31556
|
rows.push({ topic: match[1].trim(), link: match[2].trim(), line });
|
|
31066
31557
|
}
|
|
@@ -31091,7 +31582,8 @@ ${REGISTRY_TABLE_HEADER}
|
|
|
31091
31582
|
|
|
31092
31583
|
`;
|
|
31093
31584
|
}
|
|
31094
|
-
function upsertRow(
|
|
31585
|
+
function upsertRow(opts) {
|
|
31586
|
+
const { content: content3, topic, link: link2, section, root: root2 } = opts;
|
|
31095
31587
|
const rows = parseRegistryRows(content3);
|
|
31096
31588
|
const targetPath = link2;
|
|
31097
31589
|
const existingByLink = rows.find((row) => row.link === targetPath);
|
|
@@ -31131,13 +31623,19 @@ ${newLine}
|
|
|
31131
31623
|
`;
|
|
31132
31624
|
return { content: appended, action: "added" };
|
|
31133
31625
|
}
|
|
31134
|
-
function
|
|
31135
|
-
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
31139
|
-
|
|
31626
|
+
function printRegisterResult(options, result, relPath2) {
|
|
31627
|
+
if (options.json) {
|
|
31628
|
+
console.log(JSON.stringify(result, null, 2));
|
|
31629
|
+
} else if (options.dryRun) {
|
|
31630
|
+
console.log(`dry-run: would ${result.action} registry row for ${relPath2} → ${result.topic}`);
|
|
31631
|
+
} else if (result.action === "noop") {
|
|
31632
|
+
console.log(`register: ${relPath2} already registered (${result.topic})`);
|
|
31633
|
+
} else {
|
|
31634
|
+
console.log(`register: ${result.action} ${relPath2} → ${result.topic}`);
|
|
31140
31635
|
}
|
|
31636
|
+
}
|
|
31637
|
+
function resolveRegistrationTopic(input) {
|
|
31638
|
+
const { options, root: root2, relPath: relPath2, absPath } = input;
|
|
31141
31639
|
const content3 = readFileSync20(absPath, "utf8");
|
|
31142
31640
|
let topic = options.topic ?? extractTopic(content3);
|
|
31143
31641
|
if (!topic) {
|
|
@@ -31145,13 +31643,36 @@ function registerPath(options) {
|
|
|
31145
31643
|
}
|
|
31146
31644
|
const registryLink = toRegistryLink(root2, absPath);
|
|
31147
31645
|
topic = ensureCustomizeTopic(topic, registryLink);
|
|
31148
|
-
|
|
31646
|
+
return { topic, registryLink, section: inferSection(registryLink) };
|
|
31647
|
+
}
|
|
31648
|
+
function loadRegistryContent(root2) {
|
|
31149
31649
|
const registryAbs = join21(root2, REGISTRY_REL_PATH);
|
|
31150
|
-
|
|
31151
|
-
if (!existsSync25(registryAbs) && !existsSync25(join21(root2, ".skeleton/config.yaml"))) {
|
|
31650
|
+
if (!(existsSync25(registryAbs) || existsSync25(join21(root2, ".skeleton/config.yaml")))) {
|
|
31152
31651
|
throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
|
|
31153
31652
|
}
|
|
31154
|
-
|
|
31653
|
+
return existsSync25(registryAbs) ? readFileSync20(registryAbs, "utf8") : defaultRegistryContent();
|
|
31654
|
+
}
|
|
31655
|
+
function registerPath(options) {
|
|
31656
|
+
const root2 = options.root ?? findRepoRoot();
|
|
31657
|
+
const relPath2 = normalizeRelPath(options.path);
|
|
31658
|
+
const absPath = join21(root2, relPath2);
|
|
31659
|
+
if (!existsSync25(absPath)) {
|
|
31660
|
+
throw new Error(`File not found: ${relPath2}`);
|
|
31661
|
+
}
|
|
31662
|
+
const { topic, registryLink, section } = resolveRegistrationTopic({
|
|
31663
|
+
options,
|
|
31664
|
+
root: root2,
|
|
31665
|
+
relPath: relPath2,
|
|
31666
|
+
absPath
|
|
31667
|
+
});
|
|
31668
|
+
let registryContent = loadRegistryContent(root2);
|
|
31669
|
+
const { content: updated, action } = upsertRow({
|
|
31670
|
+
content: registryContent,
|
|
31671
|
+
topic,
|
|
31672
|
+
link: registryLink,
|
|
31673
|
+
section,
|
|
31674
|
+
root: root2
|
|
31675
|
+
});
|
|
31155
31676
|
registryContent = updated;
|
|
31156
31677
|
const result = {
|
|
31157
31678
|
topic,
|
|
@@ -31160,6 +31681,7 @@ function registerPath(options) {
|
|
|
31160
31681
|
action,
|
|
31161
31682
|
warnOutsideScan: isOutsideScan(root2, relPath2)
|
|
31162
31683
|
};
|
|
31684
|
+
const registryAbs = join21(root2, REGISTRY_REL_PATH);
|
|
31163
31685
|
if (!options.dryRun && action !== "noop") {
|
|
31164
31686
|
const dir = dirname13(registryAbs);
|
|
31165
31687
|
if (!existsSync25(dir)) {
|
|
@@ -31170,15 +31692,7 @@ function registerPath(options) {
|
|
|
31170
31692
|
if (result.warnOutsideScan) {
|
|
31171
31693
|
console.error(`warning: ${relPath2} is outside scan.include — register succeeded but audit will not scan it`);
|
|
31172
31694
|
}
|
|
31173
|
-
|
|
31174
|
-
console.log(JSON.stringify(result, null, 2));
|
|
31175
|
-
} else if (options.dryRun) {
|
|
31176
|
-
console.log(`dry-run: would ${action} registry row for ${relPath2} → ${topic}`);
|
|
31177
|
-
} else if (action === "noop") {
|
|
31178
|
-
console.log(`register: ${relPath2} already registered (${topic})`);
|
|
31179
|
-
} else {
|
|
31180
|
-
console.log(`register: ${action} ${relPath2} → ${topic}`);
|
|
31181
|
-
}
|
|
31695
|
+
printRegisterResult(options, result, relPath2);
|
|
31182
31696
|
return result;
|
|
31183
31697
|
}
|
|
31184
31698
|
|
|
@@ -31224,41 +31738,49 @@ function isSkeletonYamlCandidate(normalized, ext) {
|
|
|
31224
31738
|
return false;
|
|
31225
31739
|
return true;
|
|
31226
31740
|
}
|
|
31227
|
-
function
|
|
31741
|
+
function bucketForSkillPath(normalized, skillIndex) {
|
|
31742
|
+
if (!isSkillPath(normalized, skillIndex))
|
|
31743
|
+
return "skip";
|
|
31744
|
+
if (isForeignSkillPath(normalized, skillIndex))
|
|
31745
|
+
return "foreign-skill";
|
|
31746
|
+
return "skills";
|
|
31747
|
+
}
|
|
31748
|
+
function bucketForDocPath(normalized, ctx) {
|
|
31749
|
+
const ext = extname2(normalized).toLowerCase();
|
|
31750
|
+
if (!DOC_EXTENSIONS.has(ext))
|
|
31751
|
+
return "skip";
|
|
31752
|
+
const config = loadConfig(ctx.root);
|
|
31753
|
+
if (isInScanPerimeter(normalized, { config, root: ctx.root, skillIndex: ctx.skillIndex })) {
|
|
31754
|
+
return "docs";
|
|
31755
|
+
}
|
|
31756
|
+
return "skip";
|
|
31757
|
+
}
|
|
31758
|
+
function bucketFor(relPath2, ctx) {
|
|
31228
31759
|
const normalized = normalizeRelPath(relPath2);
|
|
31229
31760
|
const ext = extname2(normalized).toLowerCase();
|
|
31230
31761
|
const name = basename5(normalized);
|
|
31231
|
-
if (SKIP_EXTENSIONS.has(ext))
|
|
31232
|
-
return "skip";
|
|
31233
|
-
if (COMMAND_CONFIG_NAMES.has(name))
|
|
31762
|
+
if (SKIP_EXTENSIONS.has(ext) || COMMAND_CONFIG_NAMES.has(name))
|
|
31234
31763
|
return "skip";
|
|
31235
31764
|
if (isSkeletonYamlCandidate(normalized, ext)) {
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
|
|
31240
|
-
|
|
31241
|
-
|
|
31242
|
-
|
|
31243
|
-
return
|
|
31244
|
-
}
|
|
31245
|
-
if (DOC_EXTENSIONS.has(ext)) {
|
|
31246
|
-
const config = loadConfig(root2);
|
|
31247
|
-
if (isInScanPerimeter(normalized, config, root2, skillIndex))
|
|
31248
|
-
return "docs";
|
|
31249
|
-
return "skip";
|
|
31250
|
-
}
|
|
31765
|
+
return ctx.wiredPolicies.has(normalized) ? "policy" : "skip";
|
|
31766
|
+
}
|
|
31767
|
+
const skillBucket = bucketForSkillPath(normalized, ctx.skillIndex);
|
|
31768
|
+
if (skillBucket !== "skip")
|
|
31769
|
+
return skillBucket;
|
|
31770
|
+
const docBucket = bucketForDocPath(normalized, ctx);
|
|
31771
|
+
if (docBucket !== "skip")
|
|
31772
|
+
return docBucket;
|
|
31251
31773
|
if (SHELL_EXTENSIONS.has(ext))
|
|
31252
31774
|
return "shell";
|
|
31253
31775
|
if (ext === ".json")
|
|
31254
31776
|
return "json";
|
|
31255
31777
|
return "skip";
|
|
31256
31778
|
}
|
|
31257
|
-
function isInScanPerimeter(relPath2,
|
|
31258
|
-
const scanned = new Set(collectScanFiles(config,
|
|
31779
|
+
function isInScanPerimeter(relPath2, ctx) {
|
|
31780
|
+
const scanned = new Set(collectScanFiles(ctx.config, ctx.root, ctx.skillIndex).map((abs) => relPath(abs, ctx.root)));
|
|
31259
31781
|
if (scanned.has(relPath2))
|
|
31260
31782
|
return true;
|
|
31261
|
-
return config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
|
|
31783
|
+
return ctx.config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
|
|
31262
31784
|
}
|
|
31263
31785
|
function parseJsonContent(content3) {
|
|
31264
31786
|
try {
|
|
@@ -31310,27 +31832,30 @@ function resolvePaths(options) {
|
|
|
31310
31832
|
root: options.root
|
|
31311
31833
|
});
|
|
31312
31834
|
}
|
|
31313
|
-
function
|
|
31314
|
-
let pm2 = null;
|
|
31835
|
+
function packageManagerFromPackageJson(root2) {
|
|
31315
31836
|
const pkgPath = join22(root2, "package.json");
|
|
31316
|
-
if (existsSync26(pkgPath))
|
|
31317
|
-
|
|
31318
|
-
|
|
31319
|
-
|
|
31320
|
-
|
|
31321
|
-
|
|
31322
|
-
|
|
31323
|
-
}
|
|
31324
|
-
|
|
31325
|
-
|
|
31326
|
-
|
|
31327
|
-
|
|
31328
|
-
|
|
31329
|
-
|
|
31330
|
-
|
|
31331
|
-
|
|
31332
|
-
|
|
31333
|
-
|
|
31837
|
+
if (!existsSync26(pkgPath))
|
|
31838
|
+
return null;
|
|
31839
|
+
try {
|
|
31840
|
+
const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
|
|
31841
|
+
const raw = pkg.packageManager?.split("@")[0];
|
|
31842
|
+
if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
|
|
31843
|
+
return raw;
|
|
31844
|
+
} catch {}
|
|
31845
|
+
return null;
|
|
31846
|
+
}
|
|
31847
|
+
function packageManagerFromLockfiles(root2) {
|
|
31848
|
+
if (existsSync26(join22(root2, "bun.lock")) || existsSync26(join22(root2, "bun.lockb")))
|
|
31849
|
+
return "bun";
|
|
31850
|
+
if (existsSync26(join22(root2, "pnpm-lock.yaml")))
|
|
31851
|
+
return "pnpm";
|
|
31852
|
+
if (existsSync26(join22(root2, "yarn.lock")))
|
|
31853
|
+
return "yarn";
|
|
31854
|
+
if (existsSync26(join22(root2, "package-lock.json")))
|
|
31855
|
+
return "npm";
|
|
31856
|
+
return null;
|
|
31857
|
+
}
|
|
31858
|
+
function hintForPackageManager(pm2) {
|
|
31334
31859
|
switch (pm2) {
|
|
31335
31860
|
case "bun":
|
|
31336
31861
|
return " Run: bun test && bun run typecheck && bun run build";
|
|
@@ -31340,126 +31865,113 @@ function codeValidationHint(root2) {
|
|
|
31340
31865
|
return " Run: pnpm test && pnpm run typecheck";
|
|
31341
31866
|
case "yarn":
|
|
31342
31867
|
return " Run: yarn test && yarn typecheck";
|
|
31343
|
-
default:
|
|
31344
|
-
return " Run your local code validation gates (test + typecheck + build).";
|
|
31345
31868
|
}
|
|
31346
31869
|
}
|
|
31347
|
-
|
|
31348
|
-
const
|
|
31349
|
-
|
|
31350
|
-
|
|
31351
|
-
|
|
31352
|
-
|
|
31353
|
-
|
|
31354
|
-
|
|
31355
|
-
const
|
|
31356
|
-
|
|
31357
|
-
|
|
31358
|
-
|
|
31359
|
-
|
|
31360
|
-
console.error(`validate changed:
|
|
31361
|
-
return
|
|
31362
|
-
}
|
|
31363
|
-
const buckets = {
|
|
31364
|
-
docs: [],
|
|
31365
|
-
skills: [],
|
|
31366
|
-
shell: [],
|
|
31367
|
-
json: [],
|
|
31368
|
-
policy: []
|
|
31369
|
-
};
|
|
31370
|
-
let missing = 0;
|
|
31371
|
-
let skipped = 0;
|
|
31372
|
-
let foreignSkipped = 0;
|
|
31373
|
-
const orphans = [];
|
|
31374
|
-
for (const relPath2 of relPaths) {
|
|
31375
|
-
const normalized = normalizeRelPath(relPath2);
|
|
31376
|
-
const abs = join22(root2, normalized);
|
|
31377
|
-
if (!existsSync26(abs)) {
|
|
31378
|
-
missing++;
|
|
31379
|
-
console.error(`validate changed: path not found: ${relPath2}`);
|
|
31380
|
-
continue;
|
|
31381
|
-
}
|
|
31382
|
-
const ext = extname2(normalized).toLowerCase();
|
|
31383
|
-
if (isSkeletonYamlCandidate(normalized, ext) && !wiredPolicies.has(normalized)) {
|
|
31384
|
-
orphans.push(normalized);
|
|
31385
|
-
continue;
|
|
31386
|
-
}
|
|
31387
|
-
const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
|
|
31388
|
-
if (bucket === "skip") {
|
|
31389
|
-
skipped++;
|
|
31390
|
-
continue;
|
|
31391
|
-
}
|
|
31392
|
-
if (bucket === "foreign-skill") {
|
|
31393
|
-
foreignSkipped++;
|
|
31394
|
-
console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
|
|
31395
|
-
continue;
|
|
31396
|
-
}
|
|
31397
|
-
buckets[bucket].push(normalized);
|
|
31870
|
+
function codeValidationHint(root2) {
|
|
31871
|
+
const pm2 = packageManagerFromPackageJson(root2) ?? packageManagerFromLockfiles(root2);
|
|
31872
|
+
return pm2 ? hintForPackageManager(pm2) : " Run your local code validation gates (test + typecheck + build).";
|
|
31873
|
+
}
|
|
31874
|
+
function emptyBuckets() {
|
|
31875
|
+
return { docs: [], skills: [], shell: [], json: [], policy: [] };
|
|
31876
|
+
}
|
|
31877
|
+
function classifySinglePath(input) {
|
|
31878
|
+
const { relPath: relPath2, ctx, state, bucketCtx } = input;
|
|
31879
|
+
const normalized = normalizeRelPath(relPath2);
|
|
31880
|
+
const abs = join22(ctx.root, normalized);
|
|
31881
|
+
if (!existsSync26(abs)) {
|
|
31882
|
+
state.missing++;
|
|
31883
|
+
console.error(`validate changed: path not found: ${relPath2}`);
|
|
31884
|
+
return;
|
|
31398
31885
|
}
|
|
31399
|
-
|
|
31400
|
-
|
|
31401
|
-
|
|
31402
|
-
|
|
31403
|
-
}
|
|
31404
|
-
return 1;
|
|
31886
|
+
const ext = extname2(normalized).toLowerCase();
|
|
31887
|
+
if (isSkeletonYamlCandidate(normalized, ext) && !ctx.wiredPolicies.has(normalized)) {
|
|
31888
|
+
state.orphans.push(normalized);
|
|
31889
|
+
return;
|
|
31405
31890
|
}
|
|
31406
|
-
const
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
return 1;
|
|
31891
|
+
const bucket = bucketFor(normalized, bucketCtx);
|
|
31892
|
+
if (bucket === "skip") {
|
|
31893
|
+
state.skipped++;
|
|
31894
|
+
return;
|
|
31411
31895
|
}
|
|
31412
|
-
if (
|
|
31413
|
-
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
json: false,
|
|
31417
|
-
paths: [],
|
|
31418
|
-
only: null,
|
|
31419
|
-
root: root2,
|
|
31420
|
-
globalOnly: true
|
|
31421
|
-
});
|
|
31422
|
-
if (globalExit !== 0)
|
|
31423
|
-
exitCode = 1;
|
|
31896
|
+
if (bucket === "foreign-skill") {
|
|
31897
|
+
state.foreignSkipped++;
|
|
31898
|
+
console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
|
|
31899
|
+
return;
|
|
31424
31900
|
}
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
31428
|
-
|
|
31901
|
+
state.buckets[bucket].push(normalized);
|
|
31902
|
+
}
|
|
31903
|
+
function classifyPaths(ctx) {
|
|
31904
|
+
const state = {
|
|
31905
|
+
buckets: emptyBuckets(),
|
|
31906
|
+
missing: 0,
|
|
31907
|
+
skipped: 0,
|
|
31908
|
+
foreignSkipped: 0,
|
|
31909
|
+
orphans: []
|
|
31910
|
+
};
|
|
31911
|
+
const bucketCtx = {
|
|
31912
|
+
root: ctx.root,
|
|
31913
|
+
wiredPolicies: ctx.wiredPolicies,
|
|
31914
|
+
skillIndex: ctx.skillIndex
|
|
31915
|
+
};
|
|
31916
|
+
for (const relPath2 of ctx.relPaths) {
|
|
31917
|
+
classifySinglePath({ relPath: relPath2, ctx, state, bucketCtx });
|
|
31429
31918
|
}
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31434
|
-
|
|
31435
|
-
|
|
31436
|
-
only: null,
|
|
31437
|
-
root: root2,
|
|
31438
|
-
pathScopedOnly: true
|
|
31439
|
-
});
|
|
31440
|
-
if (docExit !== 0)
|
|
31441
|
-
exitCode = 1;
|
|
31919
|
+
return state;
|
|
31920
|
+
}
|
|
31921
|
+
function reportOrphans(orphans) {
|
|
31922
|
+
for (const orphan of orphans) {
|
|
31923
|
+
console.error(`validate changed: ${orphan} is under .skeleton/ but not referenced by any plugin policies glob.
|
|
31924
|
+
` + " Export it from a plugin `policies` array (see docs/developer/plugins.md), or remove the file.");
|
|
31442
31925
|
}
|
|
31443
|
-
|
|
31444
|
-
|
|
31445
|
-
|
|
31926
|
+
return 1;
|
|
31927
|
+
}
|
|
31928
|
+
async function runGlobalAuditIfBase(base, root2) {
|
|
31929
|
+
if (!base)
|
|
31930
|
+
return 0;
|
|
31931
|
+
return runAudit({
|
|
31932
|
+
suite: "self",
|
|
31933
|
+
strict: false,
|
|
31934
|
+
json: false,
|
|
31935
|
+
paths: [],
|
|
31936
|
+
only: null,
|
|
31937
|
+
root: root2,
|
|
31938
|
+
globalOnly: true
|
|
31939
|
+
});
|
|
31940
|
+
}
|
|
31941
|
+
async function auditDocsBucket(paths, root2) {
|
|
31942
|
+
if (paths.length === 0)
|
|
31943
|
+
return 0;
|
|
31944
|
+
return runAudit({
|
|
31945
|
+
suite: "docs",
|
|
31946
|
+
strict: false,
|
|
31947
|
+
json: false,
|
|
31948
|
+
paths,
|
|
31949
|
+
only: null,
|
|
31950
|
+
root: root2,
|
|
31951
|
+
pathScopedOnly: true
|
|
31952
|
+
});
|
|
31953
|
+
}
|
|
31954
|
+
async function auditSkillsBucket(paths, root2, base) {
|
|
31955
|
+
if (paths.length === 0)
|
|
31956
|
+
return 0;
|
|
31957
|
+
if (!base) {
|
|
31958
|
+
console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
|
|
31446
31959
|
` + ` Run: skeleton audit skills
|
|
31447
31960
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31448
|
-
|
|
31449
|
-
} else {
|
|
31450
|
-
const skillExit = await runAudit({
|
|
31451
|
-
suite: "skills",
|
|
31452
|
-
strict: false,
|
|
31453
|
-
json: false,
|
|
31454
|
-
paths: buckets.skills,
|
|
31455
|
-
only: null,
|
|
31456
|
-
root: root2,
|
|
31457
|
-
pathScopedOnly: true
|
|
31458
|
-
});
|
|
31459
|
-
if (skillExit !== 0)
|
|
31460
|
-
exitCode = 1;
|
|
31461
|
-
}
|
|
31961
|
+
return 1;
|
|
31462
31962
|
}
|
|
31963
|
+
return runAudit({
|
|
31964
|
+
suite: "skills",
|
|
31965
|
+
strict: false,
|
|
31966
|
+
json: false,
|
|
31967
|
+
paths,
|
|
31968
|
+
only: null,
|
|
31969
|
+
root: root2,
|
|
31970
|
+
pathScopedOnly: true
|
|
31971
|
+
});
|
|
31972
|
+
}
|
|
31973
|
+
function validateLocalBuckets(buckets, root2) {
|
|
31974
|
+
let exitCode = 0;
|
|
31463
31975
|
for (const relPath2 of buckets.shell) {
|
|
31464
31976
|
if (validateShell(relPath2, root2) !== 0)
|
|
31465
31977
|
exitCode = 1;
|
|
@@ -31472,49 +31984,124 @@ async function runValidateChanged(options = {}) {
|
|
|
31472
31984
|
if (validatePolicy(relPath2, root2) !== 0)
|
|
31473
31985
|
exitCode = 1;
|
|
31474
31986
|
}
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
|
|
31496
|
-
|
|
31497
|
-
|
|
31498
|
-
|
|
31499
|
-
|
|
31500
|
-
|
|
31501
|
-
|
|
31502
|
-
|
|
31987
|
+
return exitCode;
|
|
31988
|
+
}
|
|
31989
|
+
async function provePolicyProse(root2, skillIndex) {
|
|
31990
|
+
const proseExit = await runAudit({
|
|
31991
|
+
suite: "docs",
|
|
31992
|
+
strict: false,
|
|
31993
|
+
json: false,
|
|
31994
|
+
paths: [],
|
|
31995
|
+
only: null,
|
|
31996
|
+
root: root2
|
|
31997
|
+
});
|
|
31998
|
+
if (proseExit !== 0)
|
|
31999
|
+
return 1;
|
|
32000
|
+
const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
|
|
32001
|
+
if (skillPaths.length === 0)
|
|
32002
|
+
return 0;
|
|
32003
|
+
const skillProseExit = await runAudit({
|
|
32004
|
+
suite: "skills",
|
|
32005
|
+
strict: false,
|
|
32006
|
+
json: false,
|
|
32007
|
+
paths: skillPaths,
|
|
32008
|
+
only: null,
|
|
32009
|
+
root: root2,
|
|
32010
|
+
pathScopedOnly: true
|
|
32011
|
+
});
|
|
32012
|
+
return skillProseExit !== 0 ? 1 : 0;
|
|
32013
|
+
}
|
|
32014
|
+
async function auditPolicyBucket(ctx) {
|
|
32015
|
+
if (ctx.policyPaths.length === 0)
|
|
32016
|
+
return 0;
|
|
32017
|
+
if (!ctx.base) {
|
|
32018
|
+
console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
|
|
31503
32019
|
` + ` Run: skeleton audit docs
|
|
31504
32020
|
` + ` And: skeleton audit skills
|
|
31505
32021
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31506
|
-
|
|
31507
|
-
}
|
|
32022
|
+
return 1;
|
|
31508
32023
|
}
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
|
|
31514
|
-
|
|
31515
|
-
|
|
31516
|
-
|
|
32024
|
+
return provePolicyProse(ctx.root, ctx.skillIndex);
|
|
32025
|
+
}
|
|
32026
|
+
function printSuccess2(skipped, foreignSkipped) {
|
|
32027
|
+
const parts = [];
|
|
32028
|
+
if (skipped > 0)
|
|
32029
|
+
parts.push(`${skipped} path(s) skipped`);
|
|
32030
|
+
if (foreignSkipped > 0)
|
|
32031
|
+
parts.push(`${foreignSkipped} foreign skill(s) ignored`);
|
|
32032
|
+
const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
32033
|
+
console.log(`validate changed passed${note}.`);
|
|
32034
|
+
}
|
|
32035
|
+
function auditedPathCount(buckets) {
|
|
32036
|
+
return buckets.docs.length + buckets.skills.length + buckets.shell.length + buckets.json.length + buckets.policy.length;
|
|
32037
|
+
}
|
|
32038
|
+
function earlyExitForClassification(classification, root2, base) {
|
|
32039
|
+
const { buckets, missing, skipped, orphans } = classification;
|
|
32040
|
+
if (orphans.length > 0)
|
|
32041
|
+
return reportOrphans(orphans);
|
|
32042
|
+
const audited = auditedPathCount(buckets);
|
|
32043
|
+
if (missing > 0 && audited === 0 && skipped === 0) {
|
|
32044
|
+
console.error("validate changed: no paths existed on disk. Pass real paths or use --staged / --base.");
|
|
32045
|
+
return 1;
|
|
31517
32046
|
}
|
|
32047
|
+
if (skipped > 0 && audited === 0 && !base) {
|
|
32048
|
+
console.error(`validate changed: all paths were skipped (code/config). This does not verify TypeScript or app code.
|
|
32049
|
+
` + codeValidationHint(root2));
|
|
32050
|
+
return 1;
|
|
32051
|
+
}
|
|
32052
|
+
return null;
|
|
32053
|
+
}
|
|
32054
|
+
async function runBucketAudits(ctx) {
|
|
32055
|
+
let exitCode = 0;
|
|
32056
|
+
const mergeExit = (code3) => {
|
|
32057
|
+
if (code3 !== 0)
|
|
32058
|
+
exitCode = 1;
|
|
32059
|
+
};
|
|
32060
|
+
mergeExit(await runGlobalAuditIfBase(ctx.base, ctx.root));
|
|
32061
|
+
mergeExit(await auditDocsBucket(ctx.buckets.docs, ctx.root));
|
|
32062
|
+
mergeExit(await auditSkillsBucket(ctx.buckets.skills, ctx.root, ctx.base));
|
|
32063
|
+
mergeExit(validateLocalBuckets(ctx.buckets, ctx.root));
|
|
32064
|
+
mergeExit(await auditPolicyBucket({
|
|
32065
|
+
policyPaths: ctx.buckets.policy,
|
|
32066
|
+
root: ctx.root,
|
|
32067
|
+
skillIndex: ctx.skillIndex,
|
|
32068
|
+
base: ctx.base
|
|
32069
|
+
}));
|
|
32070
|
+
return exitCode;
|
|
32071
|
+
}
|
|
32072
|
+
async function runValidateChanged(options = {}) {
|
|
32073
|
+
const root2 = options.root ?? findRepoRoot();
|
|
32074
|
+
const relPaths = resolvePaths(options);
|
|
32075
|
+
if (relPaths.length === 0) {
|
|
32076
|
+
console.log("validate changed: no changed files.");
|
|
32077
|
+
return 0;
|
|
32078
|
+
}
|
|
32079
|
+
const config = loadConfig(root2);
|
|
32080
|
+
const skillIndex = buildSkillIndex(root2, config.skillOwnership);
|
|
32081
|
+
let wiredPolicies;
|
|
32082
|
+
try {
|
|
32083
|
+
wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
|
|
32084
|
+
} catch (error) {
|
|
32085
|
+
console.error(`validate changed: ${error instanceof Error ? error.message : error}`);
|
|
32086
|
+
return 1;
|
|
32087
|
+
}
|
|
32088
|
+
const classification = classifyPaths({
|
|
32089
|
+
relPaths,
|
|
32090
|
+
root: root2,
|
|
32091
|
+
wiredPolicies,
|
|
32092
|
+
skillIndex
|
|
32093
|
+
});
|
|
32094
|
+
const earlyExit = earlyExitForClassification(classification, root2, options.base);
|
|
32095
|
+
if (earlyExit !== null)
|
|
32096
|
+
return earlyExit;
|
|
32097
|
+
const exitCode = await runBucketAudits({
|
|
32098
|
+
buckets: classification.buckets,
|
|
32099
|
+
root: root2,
|
|
32100
|
+
skillIndex,
|
|
32101
|
+
base: options.base
|
|
32102
|
+
});
|
|
32103
|
+
if (exitCode === 0)
|
|
32104
|
+
printSuccess2(classification.skipped, classification.foreignSkipped);
|
|
31518
32105
|
return exitCode;
|
|
31519
32106
|
}
|
|
31520
32107
|
|
|
@@ -31546,123 +32133,153 @@ function parseRegisterArgs(argv) {
|
|
|
31546
32133
|
json = true;
|
|
31547
32134
|
else if (arg.startsWith("--topic="))
|
|
31548
32135
|
topic = arg.slice("--topic=".length);
|
|
31549
|
-
else if (!arg.startsWith("-")
|
|
32136
|
+
else if (!(arg.startsWith("-") || path2))
|
|
31550
32137
|
path2 = arg;
|
|
31551
32138
|
}
|
|
31552
32139
|
return { path: path2, topic, dryRun, json };
|
|
31553
32140
|
}
|
|
32141
|
+
function parseValidateChangedArgs(rest) {
|
|
32142
|
+
const paths = [];
|
|
32143
|
+
let staged = false;
|
|
32144
|
+
let base;
|
|
32145
|
+
for (let i = 0;i < rest.length; i++) {
|
|
32146
|
+
const arg = rest[i];
|
|
32147
|
+
if (arg === "--staged")
|
|
32148
|
+
staged = true;
|
|
32149
|
+
else if (arg === "--base")
|
|
32150
|
+
base = rest[++i];
|
|
32151
|
+
else if (arg?.startsWith("--base="))
|
|
32152
|
+
base = arg.slice("--base=".length);
|
|
32153
|
+
else if (arg && !arg.startsWith("-"))
|
|
32154
|
+
paths.push(arg);
|
|
32155
|
+
}
|
|
32156
|
+
return { paths, staged, base };
|
|
32157
|
+
}
|
|
32158
|
+
async function handleAudit(argv) {
|
|
32159
|
+
const sub = argv[0];
|
|
32160
|
+
if (sub !== "docs" && sub !== "self" && sub !== "skills") {
|
|
32161
|
+
usage();
|
|
32162
|
+
return 1;
|
|
32163
|
+
}
|
|
32164
|
+
const options = parseAuditArgs(argv.slice(1));
|
|
32165
|
+
options.suite = sub;
|
|
32166
|
+
return runAudit(options);
|
|
32167
|
+
}
|
|
32168
|
+
async function handleBuildPlugin(argv) {
|
|
32169
|
+
const { entry, check } = parseBuildPluginArgs(argv);
|
|
32170
|
+
const root2 = findRepoRoot();
|
|
32171
|
+
const result = await runBuildPlugin({ root: root2, entry, check });
|
|
32172
|
+
if (check) {
|
|
32173
|
+
console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
|
|
32174
|
+
} else {
|
|
32175
|
+
console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
|
|
32176
|
+
}
|
|
32177
|
+
return 0;
|
|
32178
|
+
}
|
|
32179
|
+
async function handleValidateChanged(argv) {
|
|
32180
|
+
const { paths, staged, base } = parseValidateChangedArgs(argv);
|
|
32181
|
+
return runValidateChanged({ paths, staged, base });
|
|
32182
|
+
}
|
|
32183
|
+
function handleRegister(argv) {
|
|
32184
|
+
const opts = parseRegisterArgs(argv);
|
|
32185
|
+
if (!opts.path) {
|
|
32186
|
+
console.error("register: path required");
|
|
32187
|
+
return 1;
|
|
32188
|
+
}
|
|
32189
|
+
registerPath({
|
|
32190
|
+
path: opts.path,
|
|
32191
|
+
topic: opts.topic,
|
|
32192
|
+
dryRun: opts.dryRun,
|
|
32193
|
+
json: opts.json
|
|
32194
|
+
});
|
|
32195
|
+
return 0;
|
|
32196
|
+
}
|
|
32197
|
+
function handleCustomizeResolve(argv) {
|
|
32198
|
+
const slug2 = argv[0];
|
|
32199
|
+
const json = argv.includes("--json");
|
|
32200
|
+
if (!slug2) {
|
|
32201
|
+
console.error("customize resolve: slug required");
|
|
32202
|
+
return 1;
|
|
32203
|
+
}
|
|
32204
|
+
const result = resolveCustomizeFromRoot(slug2);
|
|
32205
|
+
if (json) {
|
|
32206
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32207
|
+
} else if (result.content) {
|
|
32208
|
+
process7.stdout.write(result.content);
|
|
32209
|
+
}
|
|
32210
|
+
return 0;
|
|
32211
|
+
}
|
|
32212
|
+
function handleHook(argv) {
|
|
32213
|
+
if (argv[0] !== "customize") {
|
|
32214
|
+
usage();
|
|
32215
|
+
return 1;
|
|
32216
|
+
}
|
|
32217
|
+
process7.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
|
|
32218
|
+
return 0;
|
|
32219
|
+
}
|
|
32220
|
+
function handleInit(argv) {
|
|
32221
|
+
runInit(parseInitArgs(argv));
|
|
32222
|
+
return 0;
|
|
32223
|
+
}
|
|
32224
|
+
function handleReferences(argv) {
|
|
32225
|
+
const sub = argv[0];
|
|
32226
|
+
if (sub === "sync") {
|
|
32227
|
+
const dryRun = argv.includes("--dry-run");
|
|
32228
|
+
const rewriteLinks = !argv.includes("--no-rewrite-links");
|
|
32229
|
+
const result = runReferencesSync({ dryRun, rewriteLinks });
|
|
32230
|
+
printSyncResult(result);
|
|
32231
|
+
return 0;
|
|
32232
|
+
}
|
|
32233
|
+
if (sub === "check") {
|
|
32234
|
+
return runReferencesCheck({
|
|
32235
|
+
json: argv.includes("--json"),
|
|
32236
|
+
strict: argv.includes("--strict")
|
|
32237
|
+
});
|
|
32238
|
+
}
|
|
32239
|
+
usage();
|
|
32240
|
+
return 1;
|
|
32241
|
+
}
|
|
32242
|
+
async function dispatchCommand(argv) {
|
|
32243
|
+
const command = argv[0];
|
|
32244
|
+
const rest = argv.slice(1);
|
|
32245
|
+
switch (command) {
|
|
32246
|
+
case "audit":
|
|
32247
|
+
return handleAudit(rest);
|
|
32248
|
+
case "build-plugin":
|
|
32249
|
+
return handleBuildPlugin(rest);
|
|
32250
|
+
case "validate":
|
|
32251
|
+
return rest[0] === "changed" ? handleValidateChanged(rest.slice(1)) : null;
|
|
32252
|
+
case "register":
|
|
32253
|
+
return handleRegister(rest);
|
|
32254
|
+
case "customize":
|
|
32255
|
+
return rest[0] === "resolve" ? handleCustomizeResolve(rest.slice(1)) : null;
|
|
32256
|
+
case "hook":
|
|
32257
|
+
return handleHook(rest);
|
|
32258
|
+
case "init":
|
|
32259
|
+
return handleInit(rest);
|
|
32260
|
+
case "references":
|
|
32261
|
+
return handleReferences(rest);
|
|
32262
|
+
default:
|
|
32263
|
+
return null;
|
|
32264
|
+
}
|
|
32265
|
+
}
|
|
31554
32266
|
async function main() {
|
|
31555
|
-
const argv =
|
|
32267
|
+
const argv = process7.argv.slice(2);
|
|
31556
32268
|
const command = argv[0];
|
|
31557
32269
|
if (!command || command === "--help" || command === "-h") {
|
|
31558
32270
|
usage();
|
|
31559
|
-
|
|
32271
|
+
process7.exit(command ? 0 : 1);
|
|
31560
32272
|
}
|
|
31561
32273
|
try {
|
|
31562
|
-
|
|
31563
|
-
|
|
31564
|
-
if (sub !== "docs" && sub !== "self" && sub !== "skills") {
|
|
31565
|
-
usage();
|
|
31566
|
-
process.exit(1);
|
|
31567
|
-
}
|
|
31568
|
-
const options = parseAuditArgs(argv.slice(2));
|
|
31569
|
-
options.suite = sub;
|
|
31570
|
-
process.exit(await runAudit(options));
|
|
31571
|
-
}
|
|
31572
|
-
if (command === "build-plugin") {
|
|
31573
|
-
const { entry, check } = parseBuildPluginArgs(argv.slice(1));
|
|
31574
|
-
const root2 = findRepoRoot();
|
|
31575
|
-
const result = await runBuildPlugin({ root: root2, entry, check });
|
|
31576
|
-
if (check) {
|
|
31577
|
-
console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
|
|
31578
|
-
} else {
|
|
31579
|
-
console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
|
|
31580
|
-
}
|
|
31581
|
-
process.exit(0);
|
|
31582
|
-
}
|
|
31583
|
-
if (command === "validate" && argv[1] === "changed") {
|
|
31584
|
-
const rest = argv.slice(2);
|
|
31585
|
-
const paths = [];
|
|
31586
|
-
let staged = false;
|
|
31587
|
-
let base;
|
|
31588
|
-
for (let i = 0;i < rest.length; i++) {
|
|
31589
|
-
const arg = rest[i];
|
|
31590
|
-
if (arg === "--staged")
|
|
31591
|
-
staged = true;
|
|
31592
|
-
else if (arg === "--base")
|
|
31593
|
-
base = rest[++i];
|
|
31594
|
-
else if (arg?.startsWith("--base="))
|
|
31595
|
-
base = arg.slice("--base=".length);
|
|
31596
|
-
else if (arg && !arg.startsWith("-"))
|
|
31597
|
-
paths.push(arg);
|
|
31598
|
-
}
|
|
31599
|
-
process.exit(await runValidateChanged({ paths, staged, base }));
|
|
31600
|
-
}
|
|
31601
|
-
if (command === "register") {
|
|
31602
|
-
const opts = parseRegisterArgs(argv.slice(1));
|
|
31603
|
-
if (!opts.path) {
|
|
31604
|
-
console.error("register: path required");
|
|
31605
|
-
process.exit(1);
|
|
31606
|
-
}
|
|
31607
|
-
registerPath({
|
|
31608
|
-
path: opts.path,
|
|
31609
|
-
topic: opts.topic,
|
|
31610
|
-
dryRun: opts.dryRun,
|
|
31611
|
-
json: opts.json
|
|
31612
|
-
});
|
|
31613
|
-
process.exit(0);
|
|
31614
|
-
}
|
|
31615
|
-
if (command === "customize" && argv[1] === "resolve") {
|
|
31616
|
-
const slug2 = argv[2];
|
|
31617
|
-
const json = argv.includes("--json");
|
|
31618
|
-
if (!slug2) {
|
|
31619
|
-
console.error("customize resolve: slug required");
|
|
31620
|
-
process.exit(1);
|
|
31621
|
-
}
|
|
31622
|
-
const result = resolveCustomizeFromRoot(slug2);
|
|
31623
|
-
if (json) {
|
|
31624
|
-
console.log(JSON.stringify(result, null, 2));
|
|
31625
|
-
} else if (result.content) {
|
|
31626
|
-
process.stdout.write(result.content);
|
|
31627
|
-
}
|
|
31628
|
-
process.exit(0);
|
|
31629
|
-
}
|
|
31630
|
-
if (command === "hook") {
|
|
31631
|
-
if (argv[1] !== "customize") {
|
|
31632
|
-
usage();
|
|
31633
|
-
process.exit(1);
|
|
31634
|
-
}
|
|
31635
|
-
process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
|
|
31636
|
-
process.exit(0);
|
|
31637
|
-
}
|
|
31638
|
-
if (command === "init") {
|
|
31639
|
-
const parsed = parseInitArgs(argv.slice(1));
|
|
31640
|
-
runInit(parsed);
|
|
31641
|
-
process.exit(0);
|
|
31642
|
-
}
|
|
31643
|
-
if (command === "references") {
|
|
31644
|
-
const sub = argv[1];
|
|
31645
|
-
if (sub === "sync") {
|
|
31646
|
-
const dryRun = argv.includes("--dry-run");
|
|
31647
|
-
const rewriteLinks = !argv.includes("--no-rewrite-links");
|
|
31648
|
-
const result = runReferencesSync({ dryRun, rewriteLinks });
|
|
31649
|
-
printSyncResult(result);
|
|
31650
|
-
process.exit(0);
|
|
31651
|
-
}
|
|
31652
|
-
if (sub === "check") {
|
|
31653
|
-
process.exit(runReferencesCheck({
|
|
31654
|
-
json: argv.includes("--json"),
|
|
31655
|
-
strict: argv.includes("--strict")
|
|
31656
|
-
}));
|
|
31657
|
-
}
|
|
32274
|
+
const exitCode = await dispatchCommand(argv);
|
|
32275
|
+
if (exitCode === null) {
|
|
31658
32276
|
usage();
|
|
31659
|
-
|
|
32277
|
+
process7.exit(1);
|
|
31660
32278
|
}
|
|
31661
|
-
|
|
31662
|
-
process.exit(1);
|
|
32279
|
+
process7.exit(exitCode);
|
|
31663
32280
|
} catch (error) {
|
|
31664
32281
|
console.error(String(error));
|
|
31665
|
-
|
|
32282
|
+
process7.exit(1);
|
|
31666
32283
|
}
|
|
31667
32284
|
}
|
|
31668
32285
|
main();
|