@csark0812/skeleton 1.5.5 → 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 +1915 -1286
- 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) {
|
|
@@ -17313,23 +17500,10 @@ function expandPatterns(root, patterns, exclude) {
|
|
|
17313
17500
|
cwd: root,
|
|
17314
17501
|
absolute: true,
|
|
17315
17502
|
onlyFiles: true,
|
|
17316
|
-
dot: true
|
|
17503
|
+
dot: true,
|
|
17504
|
+
ignore: exclude
|
|
17317
17505
|
})) {
|
|
17318
|
-
|
|
17319
|
-
continue;
|
|
17320
|
-
const rel = normalizeRelPath(relative4(root, abs));
|
|
17321
|
-
if (shouldExclude(rel, exclude))
|
|
17322
|
-
continue;
|
|
17323
|
-
let real;
|
|
17324
|
-
try {
|
|
17325
|
-
real = realpathSync4(abs);
|
|
17326
|
-
} catch {
|
|
17327
|
-
real = abs;
|
|
17328
|
-
}
|
|
17329
|
-
const existing = byReal.get(real);
|
|
17330
|
-
if (existing === undefined || abs === real && existing !== real) {
|
|
17331
|
-
byReal.set(real, abs);
|
|
17332
|
-
}
|
|
17506
|
+
rememberMarkdownFile({ byReal, root, abs, exclude });
|
|
17333
17507
|
}
|
|
17334
17508
|
}
|
|
17335
17509
|
return [...byReal.values()];
|
|
@@ -17358,7 +17532,8 @@ function collectBannedFiles(config, root) {
|
|
|
17358
17532
|
cwd: root,
|
|
17359
17533
|
absolute: true,
|
|
17360
17534
|
onlyFiles: true,
|
|
17361
|
-
dot: false
|
|
17535
|
+
dot: false,
|
|
17536
|
+
ignore: exclude
|
|
17362
17537
|
})) {
|
|
17363
17538
|
const rel = normalizeRelPath(relative4(root, abs));
|
|
17364
17539
|
if (shouldExclude(rel, exclude))
|
|
@@ -17375,7 +17550,8 @@ function collectCoverageCandidateFiles(root, exclude) {
|
|
|
17375
17550
|
cwd: root,
|
|
17376
17551
|
absolute: true,
|
|
17377
17552
|
onlyFiles: true,
|
|
17378
|
-
dot: false
|
|
17553
|
+
dot: false,
|
|
17554
|
+
ignore: exclude
|
|
17379
17555
|
})) {
|
|
17380
17556
|
const rel = normalizeRelPath(relative4(root, abs));
|
|
17381
17557
|
if (shouldExclude(rel, exclude))
|
|
@@ -17390,31 +17566,41 @@ function excludeForeignSkillDocMetaPaths(docMetaPaths, skillIndex) {
|
|
|
17390
17566
|
return docMetaPaths;
|
|
17391
17567
|
return docMetaPaths.filter((rel) => !isForeignSkillPath(rel, skillIndex));
|
|
17392
17568
|
}
|
|
17393
|
-
function
|
|
17569
|
+
function collectRegistryDocMeta(ctx) {
|
|
17394
17570
|
const paths = [];
|
|
17395
|
-
for (const
|
|
17396
|
-
|
|
17397
|
-
}
|
|
17398
|
-
const extras = ["docs/README.md", ".skeleton/registry.md"];
|
|
17399
|
-
for (const file of extras) {
|
|
17400
|
-
const abs = join6(root, file);
|
|
17401
|
-
if (existsSync7(abs))
|
|
17402
|
-
paths.push(normalizeRelPath(file));
|
|
17403
|
-
}
|
|
17404
|
-
for (const rel of registryPaths) {
|
|
17405
|
-
if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
|
|
17571
|
+
for (const rel of ctx.registryPaths) {
|
|
17572
|
+
if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
|
|
17406
17573
|
continue;
|
|
17407
|
-
const abs = join6(root, rel);
|
|
17574
|
+
const abs = join6(ctx.root, rel);
|
|
17408
17575
|
if (existsSync7(abs))
|
|
17409
17576
|
paths.push(normalizeRelPath(rel));
|
|
17410
17577
|
}
|
|
17411
|
-
|
|
17578
|
+
return paths;
|
|
17579
|
+
}
|
|
17580
|
+
function collectScannedDocMeta(ctx) {
|
|
17581
|
+
const paths = [];
|
|
17582
|
+
for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
|
|
17412
17583
|
const content = readFileSync5(abs, "utf8");
|
|
17413
17584
|
if (/<!--\s*doc-meta:/.test(content)) {
|
|
17414
|
-
paths.push(normalizeRelPath(relative4(root, abs)));
|
|
17585
|
+
paths.push(normalizeRelPath(relative4(ctx.root, abs)));
|
|
17415
17586
|
}
|
|
17416
17587
|
}
|
|
17417
|
-
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);
|
|
17418
17604
|
}
|
|
17419
17605
|
function validateScanRoots(config, root) {
|
|
17420
17606
|
const missing = [];
|
|
@@ -17437,31 +17623,34 @@ function filterToPaths(files, paths, root) {
|
|
|
17437
17623
|
return normalizedPaths.some((path) => rel === path || rel.startsWith(`${path}/`));
|
|
17438
17624
|
});
|
|
17439
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
|
+
}
|
|
17440
17650
|
function includeExplicitMarkdownPaths(files, paths, root) {
|
|
17441
17651
|
const out = new Set(files);
|
|
17442
17652
|
for (const raw of paths) {
|
|
17443
|
-
|
|
17444
|
-
const abs = join6(root, rel);
|
|
17445
|
-
if (!existsSync7(abs))
|
|
17446
|
-
continue;
|
|
17447
|
-
if (isMarkdownFile(rel)) {
|
|
17448
|
-
out.add(abs);
|
|
17449
|
-
continue;
|
|
17450
|
-
}
|
|
17451
|
-
try {
|
|
17452
|
-
if (!statSync2(abs).isDirectory())
|
|
17453
|
-
continue;
|
|
17454
|
-
} catch {
|
|
17455
|
-
continue;
|
|
17456
|
-
}
|
|
17457
|
-
for (const md of globSync("**/*.{md,mdc}", {
|
|
17458
|
-
cwd: abs,
|
|
17459
|
-
absolute: true,
|
|
17460
|
-
onlyFiles: true,
|
|
17461
|
-
dot: true
|
|
17462
|
-
})) {
|
|
17463
|
-
out.add(md);
|
|
17464
|
-
}
|
|
17653
|
+
addExplicitPath(out, root, raw);
|
|
17465
17654
|
}
|
|
17466
17655
|
return [...out];
|
|
17467
17656
|
}
|
|
@@ -17498,9 +17687,6 @@ function parseRegistry(root) {
|
|
|
17498
17687
|
}
|
|
17499
17688
|
return { paths: [...new Set(paths)], hasTableHeader };
|
|
17500
17689
|
}
|
|
17501
|
-
function parseRegistryPaths(root) {
|
|
17502
|
-
return parseRegistry(root).paths;
|
|
17503
|
-
}
|
|
17504
17690
|
|
|
17505
17691
|
// src/audit/core/context.ts
|
|
17506
17692
|
function createContext(options = {}) {
|
|
@@ -17516,7 +17702,12 @@ function createContext(options = {}) {
|
|
|
17516
17702
|
files = filterToPaths(files, options.paths, root);
|
|
17517
17703
|
}
|
|
17518
17704
|
const registry = parseRegistry(root);
|
|
17519
|
-
const allDocMetaPaths = collectDocMetaPaths(
|
|
17705
|
+
const allDocMetaPaths = collectDocMetaPaths({
|
|
17706
|
+
config,
|
|
17707
|
+
root,
|
|
17708
|
+
registryPaths: registry.paths,
|
|
17709
|
+
skillIndex
|
|
17710
|
+
});
|
|
17520
17711
|
return {
|
|
17521
17712
|
root,
|
|
17522
17713
|
config,
|
|
@@ -21788,9 +21979,9 @@ function factoryTitle(effects, ok2, nok, type, markerType, stringType) {
|
|
|
21788
21979
|
return atBreak(code);
|
|
21789
21980
|
}
|
|
21790
21981
|
effects.consume(code);
|
|
21791
|
-
return code === codes.backslash ?
|
|
21982
|
+
return code === codes.backslash ? escape : inside;
|
|
21792
21983
|
}
|
|
21793
|
-
function
|
|
21984
|
+
function escape(code) {
|
|
21794
21985
|
if (code === marker || code === codes.backslash) {
|
|
21795
21986
|
effects.consume(code);
|
|
21796
21987
|
return inside;
|
|
@@ -28817,174 +29008,213 @@ function stripYamlFrontmatter(content3) {
|
|
|
28817
29008
|
return "";
|
|
28818
29009
|
return content3;
|
|
28819
29010
|
}
|
|
28820
|
-
function
|
|
28821
|
-
let i =
|
|
28822
|
-
|
|
28823
|
-
|
|
28824
|
-
|
|
28825
|
-
|
|
28826
|
-
}
|
|
28827
|
-
if (!/\s/.test(slice[i]))
|
|
28828
|
-
return false;
|
|
28829
|
-
while (i < slice.length && /\s/.test(slice[i]))
|
|
29011
|
+
function skipWhitespace(slice, start) {
|
|
29012
|
+
let i = start;
|
|
29013
|
+
while (i < slice.length) {
|
|
29014
|
+
const ch = slice[i];
|
|
29015
|
+
if (ch === undefined || !/\s/.test(ch))
|
|
29016
|
+
break;
|
|
28830
29017
|
i++;
|
|
28831
|
-
if (i >= slice.length)
|
|
28832
|
-
return false;
|
|
28833
|
-
if (slice[i] === ")") {
|
|
28834
|
-
return i === slice.length - 1;
|
|
28835
29018
|
}
|
|
28836
|
-
|
|
29019
|
+
return i;
|
|
29020
|
+
}
|
|
29021
|
+
function skipTitle(slice, start) {
|
|
29022
|
+
if (start >= slice.length)
|
|
29023
|
+
return start;
|
|
29024
|
+
const open = slice[start];
|
|
28837
29025
|
if (open !== '"' && open !== "'" && open !== "(")
|
|
28838
|
-
return
|
|
29026
|
+
return start;
|
|
28839
29027
|
const close = open === "(" ? ")" : open;
|
|
28840
|
-
i
|
|
29028
|
+
let i = start + 1;
|
|
28841
29029
|
while (i < slice.length && slice[i] !== close)
|
|
28842
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);
|
|
28843
29042
|
if (i >= slice.length)
|
|
28844
29043
|
return false;
|
|
28845
|
-
i
|
|
28846
|
-
|
|
28847
|
-
|
|
29044
|
+
if (slice[i] === ")")
|
|
29045
|
+
return i === slice.length - 1;
|
|
29046
|
+
i = skipTitle(slice, i);
|
|
29047
|
+
i = skipWhitespace(slice, i);
|
|
28848
29048
|
return i === slice.length - 1 && slice[i] === ")";
|
|
28849
29049
|
}
|
|
28850
|
-
function
|
|
28851
|
-
|
|
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);
|
|
28852
29076
|
let searchFrom = 0;
|
|
28853
|
-
while (searchFrom <
|
|
28854
|
-
const openParen =
|
|
29077
|
+
while (searchFrom < nodeSlice.length) {
|
|
29078
|
+
const openParen = nodeSlice.indexOf("](", searchFrom);
|
|
28855
29079
|
if (openParen === -1)
|
|
28856
29080
|
break;
|
|
28857
|
-
const
|
|
28858
|
-
if (
|
|
28859
|
-
|
|
28860
|
-
if (destinationConsumesToSliceEnd(slice, afterDest)) {
|
|
28861
|
-
const urlStart = nodeStart + after + 1;
|
|
28862
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
28863
|
-
}
|
|
28864
|
-
} else if (slice.startsWith(url, after)) {
|
|
28865
|
-
const next = slice[after + url.length];
|
|
28866
|
-
if (next === ")" || next !== undefined && /\s/.test(next)) {
|
|
28867
|
-
if (destinationConsumesToSliceEnd(slice, after + url.length)) {
|
|
28868
|
-
const urlStart = nodeStart + after;
|
|
28869
|
-
return { urlStart, urlEnd: urlStart + url.length };
|
|
28870
|
-
}
|
|
28871
|
-
}
|
|
28872
|
-
}
|
|
29081
|
+
const span = tryLinkDestination({ content: nodeSlice, nodeStart: slice.nodeStart, nodeEnd: slice.nodeEnd, url: slice.url }, openParen);
|
|
29082
|
+
if (span)
|
|
29083
|
+
return span;
|
|
28873
29084
|
searchFrom = openParen + 1;
|
|
28874
29085
|
}
|
|
28875
|
-
|
|
28876
|
-
|
|
28877
|
-
|
|
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 };
|
|
28878
29092
|
}
|
|
28879
|
-
if (
|
|
28880
|
-
return { urlStart: nodeStart, urlEnd: nodeEnd };
|
|
29093
|
+
if (nodeSlice === slice.url) {
|
|
29094
|
+
return { urlStart: slice.nodeStart, urlEnd: slice.nodeEnd };
|
|
28881
29095
|
}
|
|
28882
|
-
const trimmed =
|
|
29096
|
+
const trimmed = nodeSlice.trim();
|
|
28883
29097
|
if (trimmed === auto) {
|
|
28884
|
-
const lead =
|
|
29098
|
+
const lead = nodeSlice.indexOf(auto);
|
|
28885
29099
|
if (lead !== -1) {
|
|
28886
|
-
return {
|
|
29100
|
+
return {
|
|
29101
|
+
urlStart: slice.nodeStart + lead + 1,
|
|
29102
|
+
urlEnd: slice.nodeStart + lead + 1 + slice.url.length
|
|
29103
|
+
};
|
|
28887
29104
|
}
|
|
28888
29105
|
}
|
|
28889
|
-
if (trimmed === url) {
|
|
28890
|
-
const lead =
|
|
29106
|
+
if (trimmed === slice.url) {
|
|
29107
|
+
const lead = nodeSlice.indexOf(slice.url);
|
|
28891
29108
|
if (lead !== -1) {
|
|
28892
|
-
return {
|
|
29109
|
+
return {
|
|
29110
|
+
urlStart: slice.nodeStart + lead,
|
|
29111
|
+
urlEnd: slice.nodeStart + lead + slice.url.length
|
|
29112
|
+
};
|
|
28893
29113
|
}
|
|
28894
29114
|
}
|
|
28895
|
-
return;
|
|
28896
29115
|
}
|
|
28897
|
-
function findUrlInDefinitionSlice(
|
|
28898
|
-
const
|
|
28899
|
-
const labelEnd2 =
|
|
29116
|
+
function findUrlInDefinitionSlice(slice) {
|
|
29117
|
+
const nodeSlice = slice.content.slice(slice.nodeStart, slice.nodeEnd);
|
|
29118
|
+
const labelEnd2 = nodeSlice.indexOf("]:");
|
|
28900
29119
|
if (labelEnd2 === -1)
|
|
28901
29120
|
return;
|
|
28902
29121
|
let i = labelEnd2 + 2;
|
|
28903
|
-
while (i <
|
|
29122
|
+
while (i < nodeSlice.length && /\s/.test(nodeSlice[i] ?? ""))
|
|
28904
29123
|
i++;
|
|
28905
|
-
if (
|
|
28906
|
-
const urlStart = nodeStart + i + 1;
|
|
28907
|
-
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 };
|
|
28908
29127
|
}
|
|
28909
|
-
if (
|
|
28910
|
-
const urlStart = nodeStart + i;
|
|
28911
|
-
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 };
|
|
28912
29131
|
}
|
|
28913
|
-
return;
|
|
28914
29132
|
}
|
|
28915
|
-
function
|
|
28916
|
-
|
|
28917
|
-
|
|
28918
|
-
|
|
28919
|
-
|
|
28920
|
-
|
|
28921
|
-
|
|
28922
|
-
|
|
28923
|
-
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
|
|
28931
|
-
|
|
28932
|
-
|
|
28933
|
-
|
|
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
|
+
});
|
|
29155
|
+
}
|
|
29156
|
+
function collectReferenceDefinitions(content3, tree) {
|
|
29157
|
+
const defs = new Map;
|
|
29158
|
+
visit2(tree, (node2) => {
|
|
29159
|
+
if (node2.type !== "definition")
|
|
28934
29160
|
return;
|
|
28935
|
-
|
|
28936
|
-
url,
|
|
28937
|
-
urlStart: span.urlStart,
|
|
28938
|
-
urlEnd: span.urlEnd,
|
|
28939
|
-
line: lineFromOffset(content3, start) ?? 1
|
|
28940
|
-
});
|
|
29161
|
+
definitionFromNode(content3, node2, defs);
|
|
28941
29162
|
});
|
|
28942
29163
|
return defs;
|
|
28943
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
|
+
}
|
|
28944
29188
|
function extractLinksFromMarkdown(content3, _filePath) {
|
|
28945
29189
|
const tree = processor.parse(content3);
|
|
28946
29190
|
const refDefs = collectReferenceDefinitions(content3, tree);
|
|
28947
29191
|
const links = [];
|
|
28948
29192
|
visit2(tree, (node2) => {
|
|
28949
29193
|
if (node2.type === "link" && "url" in node2 && typeof node2.url === "string") {
|
|
28950
|
-
|
|
28951
|
-
const start = node2.position?.start.offset;
|
|
28952
|
-
const end = node2.position?.end.offset;
|
|
28953
|
-
const span = start !== undefined && end !== undefined ? findUrlSpanInSlice(content3, start, end, target) : undefined;
|
|
28954
|
-
links.push({
|
|
28955
|
-
target,
|
|
28956
|
-
line: lineFromOffset(content3, node2.position?.start.offset),
|
|
28957
|
-
urlStart: span?.urlStart,
|
|
28958
|
-
urlEnd: span?.urlEnd
|
|
28959
|
-
});
|
|
29194
|
+
links.push(linkFromDirectNode(content3, node2));
|
|
28960
29195
|
}
|
|
28961
29196
|
if (node2.type === "linkReference" && "identifier" in node2) {
|
|
28962
|
-
const
|
|
28963
|
-
|
|
28964
|
-
|
|
28965
|
-
links.push({
|
|
28966
|
-
target: def.url.trim(),
|
|
28967
|
-
line: def.line,
|
|
28968
|
-
urlStart: def.urlStart,
|
|
28969
|
-
urlEnd: def.urlEnd
|
|
28970
|
-
});
|
|
28971
|
-
}
|
|
29197
|
+
const refLink = linkFromReference(String(node2.identifier).toLowerCase(), refDefs);
|
|
29198
|
+
if (refLink)
|
|
29199
|
+
links.push(refLink);
|
|
28972
29200
|
}
|
|
28973
29201
|
});
|
|
28974
29202
|
return links;
|
|
28975
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
|
+
}
|
|
28976
29210
|
function phrasingText(nodes) {
|
|
28977
29211
|
if (!nodes?.length)
|
|
28978
29212
|
return "";
|
|
28979
29213
|
let out = "";
|
|
28980
29214
|
for (const node2 of nodes) {
|
|
28981
|
-
|
|
28982
|
-
|
|
28983
|
-
continue;
|
|
28984
|
-
}
|
|
28985
|
-
if (node2.children?.length) {
|
|
29215
|
+
out += textFromPhrasingNode(node2);
|
|
29216
|
+
if (node2.children?.length)
|
|
28986
29217
|
out += phrasingText(node2.children);
|
|
28987
|
-
}
|
|
28988
29218
|
}
|
|
28989
29219
|
return out;
|
|
28990
29220
|
}
|
|
@@ -29074,60 +29304,86 @@ function replaceAnchorInTarget(target, oldAnchor, newAnchor) {
|
|
|
29074
29304
|
return target;
|
|
29075
29305
|
return `${pathPart}#${newAnchor}${queryPart}`;
|
|
29076
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
|
+
}
|
|
29077
29381
|
function collectAnchorFixes(ctx) {
|
|
29078
29382
|
const editsByFile = new Map;
|
|
29079
29383
|
for (const filePath of ctx.files) {
|
|
29080
|
-
const
|
|
29081
|
-
|
|
29082
|
-
|
|
29083
|
-
const relFile = relPath(filePath, ctx.root);
|
|
29084
|
-
for (const { target, line, urlStart, urlEnd } of links) {
|
|
29085
|
-
if (isExternalLink(target) && !target.startsWith("#"))
|
|
29086
|
-
continue;
|
|
29087
|
-
if (isPlaceholderLink(target))
|
|
29088
|
-
continue;
|
|
29089
|
-
const anchor = target.includes("#") ? target.split("#")[1]?.split("?")[0] ?? "" : "";
|
|
29090
|
-
if (!anchor)
|
|
29091
|
-
continue;
|
|
29092
|
-
const resolved = resolveLink(filePath, target);
|
|
29093
|
-
if (!existsSync9(resolved))
|
|
29094
|
-
continue;
|
|
29095
|
-
const targetContent = readFileSync7(resolved, "utf8");
|
|
29096
|
-
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
29097
|
-
const anchorSlug = slugifyAnchor(anchor);
|
|
29098
|
-
if (slugs.has(anchorSlug))
|
|
29099
|
-
continue;
|
|
29100
|
-
const match = findBestAnchorMatch(anchorSlug, slugs);
|
|
29101
|
-
if (!match)
|
|
29102
|
-
continue;
|
|
29103
|
-
const nextTarget = replaceAnchorInTarget(target, anchor, match.slug);
|
|
29104
|
-
if (nextTarget === target)
|
|
29105
|
-
continue;
|
|
29106
|
-
const lineLabel = line ? `${relFile}:${line}` : relFile;
|
|
29107
|
-
const description = `${lineLabel} #${anchor} → #${match.slug} (score ${match.score.toFixed(2)})`;
|
|
29108
|
-
if (urlStart !== undefined && urlEnd !== undefined && content3.slice(urlStart, urlEnd) === target) {
|
|
29109
|
-
pending.push({ urlStart, urlEnd, from: target, to: nextTarget, description });
|
|
29110
|
-
}
|
|
29111
|
-
}
|
|
29112
|
-
if (pending.length === 0)
|
|
29113
|
-
continue;
|
|
29114
|
-
const uniqueBySpan = new Map;
|
|
29115
|
-
for (const edit of pending) {
|
|
29116
|
-
uniqueBySpan.set(`${edit.urlStart}:${edit.urlEnd}:${edit.from}`, edit);
|
|
29117
|
-
}
|
|
29118
|
-
const uniquePending = [...uniqueBySpan.values()];
|
|
29119
|
-
uniquePending.sort((a, b) => b.urlStart - a.urlStart);
|
|
29120
|
-
let updated = content3;
|
|
29121
|
-
const descriptions = [];
|
|
29122
|
-
for (const edit of uniquePending) {
|
|
29123
|
-
if (updated.slice(edit.urlStart, edit.urlEnd) !== edit.from)
|
|
29124
|
-
continue;
|
|
29125
|
-
updated = updated.slice(0, edit.urlStart) + edit.to + updated.slice(edit.urlEnd);
|
|
29126
|
-
descriptions.push(edit.description);
|
|
29127
|
-
}
|
|
29128
|
-
if (updated === content3 || descriptions.length === 0)
|
|
29129
|
-
continue;
|
|
29130
|
-
editsByFile.set(filePath, { content: updated, descriptions });
|
|
29384
|
+
const result = collectFileAnchorFixes(ctx, filePath);
|
|
29385
|
+
if (result)
|
|
29386
|
+
editsByFile.set(filePath, result);
|
|
29131
29387
|
}
|
|
29132
29388
|
const edits = [];
|
|
29133
29389
|
for (const [absPath, { content: content3, descriptions }] of editsByFile) {
|
|
@@ -29170,32 +29426,37 @@ function bumpDocMetaLastReviewed(content3, gitDate) {
|
|
|
29170
29426
|
return null;
|
|
29171
29427
|
return replaceDocMetaLastReviewed(content3, gitDate);
|
|
29172
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
|
+
}
|
|
29173
29454
|
function collectDocMetaFixes(ctx) {
|
|
29174
29455
|
const edits = [];
|
|
29175
29456
|
for (const relPath2 of ctx.docMetaPaths) {
|
|
29176
|
-
const
|
|
29177
|
-
if (
|
|
29178
|
-
|
|
29179
|
-
const content3 = readFileSync8(abs, "utf8");
|
|
29180
|
-
if (!DOC_META_RE.test(content3))
|
|
29181
|
-
continue;
|
|
29182
|
-
const reviewedStr = docMetaLastReviewed(content3);
|
|
29183
|
-
if (!reviewedStr)
|
|
29184
|
-
continue;
|
|
29185
|
-
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29186
|
-
if (Number.isNaN(reviewed.getTime()))
|
|
29187
|
-
continue;
|
|
29188
|
-
const gitDate = lastGitCommitDate(relPath2, ctx.root);
|
|
29189
|
-
if (!gitDate)
|
|
29190
|
-
continue;
|
|
29191
|
-
const updated = bumpDocMetaLastReviewed(content3, gitDate);
|
|
29192
|
-
if (!updated)
|
|
29193
|
-
continue;
|
|
29194
|
-
edits.push({
|
|
29195
|
-
file: relPath2,
|
|
29196
|
-
description: `last-reviewed ${reviewedStr} → ${gitDate}`,
|
|
29197
|
-
content: updated
|
|
29198
|
-
});
|
|
29457
|
+
const fix = docMetaFixForPath(ctx, relPath2);
|
|
29458
|
+
if (fix)
|
|
29459
|
+
edits.push(fix);
|
|
29199
29460
|
}
|
|
29200
29461
|
return edits;
|
|
29201
29462
|
}
|
|
@@ -29237,28 +29498,31 @@ function overlayLastReviewed(targetContent, metaContent) {
|
|
|
29237
29498
|
function underRoot(rootAbs, candidateAbs) {
|
|
29238
29499
|
return candidateAbs === rootAbs || candidateAbs.startsWith(rootAbs + sep3);
|
|
29239
29500
|
}
|
|
29240
|
-
function
|
|
29241
|
-
|
|
29242
|
-
|
|
29243
|
-
|
|
29244
|
-
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29245
|
-
}
|
|
29501
|
+
function shouldStopPathWalk(rootResolved, parent, cursor) {
|
|
29502
|
+
return parent === cursor || !underRoot(rootResolved, parent) && parent !== rootResolved;
|
|
29503
|
+
}
|
|
29504
|
+
function resolveExistingRealPath(rootResolved, abs, relFile) {
|
|
29246
29505
|
const rootReal = existsSync11(rootResolved) ? realpathSync5(rootResolved) : rootResolved;
|
|
29247
29506
|
let cursor = abs;
|
|
29248
|
-
while (
|
|
29249
|
-
if (existsSync11(cursor)) {
|
|
29250
|
-
const real = realpathSync5(cursor);
|
|
29251
|
-
if (!underRoot(rootReal, real)) {
|
|
29252
|
-
throw new Error(`Refusing autofix outside repo root: ${relFile}`);
|
|
29253
|
-
}
|
|
29254
|
-
return abs;
|
|
29255
|
-
}
|
|
29507
|
+
while (!existsSync11(cursor)) {
|
|
29256
29508
|
const parent = dirname6(cursor);
|
|
29257
|
-
if (
|
|
29509
|
+
if (shouldStopPathWalk(rootResolved, parent, cursor))
|
|
29258
29510
|
return abs;
|
|
29259
|
-
}
|
|
29260
29511
|
cursor = parent;
|
|
29261
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);
|
|
29262
29526
|
}
|
|
29263
29527
|
function applyFixes(ctx, options) {
|
|
29264
29528
|
const kinds = new Set(options.kinds);
|
|
@@ -29302,13 +29566,14 @@ function fixKindsForOnly(kinds, only) {
|
|
|
29302
29566
|
}
|
|
29303
29567
|
|
|
29304
29568
|
// src/audit/core/report.ts
|
|
29305
|
-
function issue(rule, file,
|
|
29569
|
+
function issue(rule, file, details) {
|
|
29570
|
+
const message = typeof details === "string" ? details : details.message;
|
|
29306
29571
|
return {
|
|
29307
29572
|
rule,
|
|
29308
29573
|
file,
|
|
29309
|
-
link:
|
|
29574
|
+
link: typeof details === "string" ? undefined : details.link,
|
|
29310
29575
|
message,
|
|
29311
|
-
severity:
|
|
29576
|
+
severity: typeof details === "string" ? "error" : details.severity ?? "error"
|
|
29312
29577
|
};
|
|
29313
29578
|
}
|
|
29314
29579
|
function finalizeIssues(issues, strict) {
|
|
@@ -29321,31 +29586,39 @@ function printReport(issues, options) {
|
|
|
29321
29586
|
const errors2 = finalized.filter((i) => i.severity === "error");
|
|
29322
29587
|
const warnings = finalized.filter((i) => i.severity === "warning");
|
|
29323
29588
|
const label = options.label ?? "Audit";
|
|
29324
|
-
|
|
29325
|
-
|
|
29326
|
-
|
|
29327
|
-
|
|
29328
|
-
|
|
29329
|
-
|
|
29330
|
-
|
|
29331
|
-
|
|
29332
|
-
|
|
29333
|
-
|
|
29334
|
-
|
|
29335
|
-
|
|
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:
|
|
29336
29608
|
`);
|
|
29337
|
-
|
|
29338
|
-
|
|
29339
|
-
|
|
29340
|
-
}
|
|
29341
|
-
console.log("");
|
|
29342
|
-
}
|
|
29343
|
-
if (errors2.length === 0) {
|
|
29344
|
-
const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : "";
|
|
29345
|
-
const countNote = options.successSuffix ?? (options.fileCount !== undefined ? ` (${options.fileCount} files scanned${warnNote})` : "");
|
|
29346
|
-
console.log(`${label} passed${countNote}.`);
|
|
29347
|
-
return 0;
|
|
29609
|
+
for (const i of warnings) {
|
|
29610
|
+
const linkPart = i.link ? ` (${i.link})` : "";
|
|
29611
|
+
console.log(`- ${i.file}${linkPart}: ${i.message}`);
|
|
29348
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) {
|
|
29349
29622
|
console.log(`${label} failed:
|
|
29350
29623
|
`);
|
|
29351
29624
|
for (const i of errors2) {
|
|
@@ -29354,6 +29627,13 @@ function printReport(issues, options) {
|
|
|
29354
29627
|
}
|
|
29355
29628
|
return 1;
|
|
29356
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
|
+
}
|
|
29357
29637
|
|
|
29358
29638
|
// src/references/check.ts
|
|
29359
29639
|
import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
@@ -29413,73 +29693,86 @@ function findSharedRefLinks(content3, sourceFile) {
|
|
|
29413
29693
|
}
|
|
29414
29694
|
return links;
|
|
29415
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
|
+
}
|
|
29416
29715
|
function findLocalCanonicalLinks(root2, content3, sourceFile) {
|
|
29417
29716
|
const links = [];
|
|
29418
29717
|
const localRefRe = /\((?:\.\/)?references\/([^)]+)\)/g;
|
|
29419
29718
|
for (const match of content3.matchAll(localRefRe)) {
|
|
29420
29719
|
const refPath = normalizeRelPath(match[1] ?? "");
|
|
29421
|
-
if (!refPath
|
|
29720
|
+
if (!(refPath && canonicalExists(root2, refPath)))
|
|
29422
29721
|
continue;
|
|
29423
29722
|
links.push({ refPath, sourceFile });
|
|
29424
29723
|
}
|
|
29425
|
-
|
|
29426
|
-
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
|
|
29430
|
-
|
|
29431
|
-
|
|
29432
|
-
|
|
29433
|
-
|
|
29434
|
-
|
|
29435
|
-
|
|
29436
|
-
|
|
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))
|
|
29437
29747
|
continue;
|
|
29438
|
-
|
|
29748
|
+
refPaths.add(link2.refPath);
|
|
29749
|
+
links.push(link2);
|
|
29750
|
+
queue.push(link2.refPath);
|
|
29439
29751
|
}
|
|
29440
29752
|
}
|
|
29441
|
-
|
|
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;
|
|
29442
29768
|
}
|
|
29443
29769
|
function discoverSkillReferencePlans(root2, ownership) {
|
|
29444
29770
|
const index2 = buildSkillIndex(root2, ownership);
|
|
29445
29771
|
const plans = [];
|
|
29446
29772
|
for (const slug2 of index2.ownedSlugs) {
|
|
29447
|
-
const
|
|
29448
|
-
if (
|
|
29449
|
-
|
|
29450
|
-
const refPaths = new Set;
|
|
29451
|
-
const links = [];
|
|
29452
|
-
for (const relFile of walkMarkdownFiles(skillDir, root2)) {
|
|
29453
|
-
const content3 = readFileSync9(join10(root2, relFile), "utf8");
|
|
29454
|
-
if (isGeneratedReference(content3))
|
|
29455
|
-
continue;
|
|
29456
|
-
for (const link2 of findSharedRefLinks(content3, relFile)) {
|
|
29457
|
-
refPaths.add(link2.refPath);
|
|
29458
|
-
links.push(link2);
|
|
29459
|
-
}
|
|
29460
|
-
for (const link2 of findLocalCanonicalLinks(root2, content3, relFile)) {
|
|
29461
|
-
refPaths.add(link2.refPath);
|
|
29462
|
-
links.push(link2);
|
|
29463
|
-
}
|
|
29464
|
-
}
|
|
29465
|
-
const queue = [...refPaths];
|
|
29466
|
-
while (queue.length > 0) {
|
|
29467
|
-
const refPath = queue.pop();
|
|
29468
|
-
if (!refPath || !canonicalExists(root2, refPath))
|
|
29469
|
-
continue;
|
|
29470
|
-
const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
|
|
29471
|
-
const syntheticSource = generatedRefPath(slug2, refPath);
|
|
29472
|
-
for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
|
|
29473
|
-
if (refPaths.has(link2.refPath))
|
|
29474
|
-
continue;
|
|
29475
|
-
refPaths.add(link2.refPath);
|
|
29476
|
-
links.push(link2);
|
|
29477
|
-
queue.push(link2.refPath);
|
|
29478
|
-
}
|
|
29479
|
-
}
|
|
29480
|
-
if (refPaths.size > 0) {
|
|
29481
|
-
plans.push({ skill: slug2, refPaths, links });
|
|
29482
|
-
}
|
|
29773
|
+
const plan = planForSkill(root2, slug2);
|
|
29774
|
+
if (plan)
|
|
29775
|
+
plans.push(plan);
|
|
29483
29776
|
}
|
|
29484
29777
|
return plans.sort((a, b) => a.skill.localeCompare(b.skill));
|
|
29485
29778
|
}
|
|
@@ -29510,30 +29803,75 @@ function rewriteSharedRefLinks(content3, sourceFile, skill) {
|
|
|
29510
29803
|
}
|
|
29511
29804
|
|
|
29512
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
|
+
}
|
|
29513
29822
|
function listAllGeneratedFiles(root2) {
|
|
29514
29823
|
const files = [];
|
|
29515
|
-
|
|
29516
|
-
|
|
29517
|
-
|
|
29518
|
-
|
|
29519
|
-
if (entry.name.startsWith("."))
|
|
29520
|
-
continue;
|
|
29521
|
-
const fullPath = join11(dir, entry.name);
|
|
29522
|
-
if (entry.isDirectory()) {
|
|
29523
|
-
walk(fullPath);
|
|
29524
|
-
continue;
|
|
29525
|
-
}
|
|
29526
|
-
if (!entry.name.endsWith(".md"))
|
|
29527
|
-
continue;
|
|
29528
|
-
const content3 = readFileSync10(fullPath, "utf8");
|
|
29529
|
-
if (isGeneratedReference(content3)) {
|
|
29530
|
-
files.push(normalizeRelPath(relative7(root2, fullPath)));
|
|
29531
|
-
}
|
|
29824
|
+
walkMarkdown(root2, (fullPath) => {
|
|
29825
|
+
const content3 = readFileSync10(fullPath, "utf8");
|
|
29826
|
+
if (isGeneratedReference(content3)) {
|
|
29827
|
+
files.push(normalizeRelPath(relative7(root2, fullPath)));
|
|
29532
29828
|
}
|
|
29533
|
-
};
|
|
29534
|
-
walk(root2);
|
|
29829
|
+
});
|
|
29535
29830
|
return files;
|
|
29536
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
|
+
}
|
|
29537
29875
|
function runGeneratedReferencesCheck(root2, ownership) {
|
|
29538
29876
|
const issues = [];
|
|
29539
29877
|
const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
|
|
@@ -29548,58 +29886,13 @@ function runGeneratedReferencesCheck(root2, ownership) {
|
|
|
29548
29886
|
}
|
|
29549
29887
|
}
|
|
29550
29888
|
for (const targetRel of needed) {
|
|
29551
|
-
const
|
|
29552
|
-
if (
|
|
29553
|
-
issues.push(
|
|
29554
|
-
continue;
|
|
29555
|
-
}
|
|
29556
|
-
const generated = readFileSync10(targetPath, "utf8");
|
|
29557
|
-
if (!isGeneratedReference(generated)) {
|
|
29558
|
-
issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
|
|
29559
|
-
continue;
|
|
29560
|
-
}
|
|
29561
|
-
const body = stripGeneratedHeader(generated);
|
|
29562
|
-
const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
|
|
29563
|
-
const canonicalPath = join11(root2, sourceRel);
|
|
29564
|
-
if (!existsSync13(canonicalPath)) {
|
|
29565
|
-
issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
|
|
29566
|
-
continue;
|
|
29567
|
-
}
|
|
29568
|
-
const canonical = readFileSync10(canonicalPath, "utf8");
|
|
29569
|
-
if (body !== canonical) {
|
|
29570
|
-
issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
|
|
29571
|
-
}
|
|
29572
|
-
}
|
|
29573
|
-
for (const generatedRel of listAllGeneratedFiles(root2)) {
|
|
29574
|
-
if (isForeignSkillPath(generatedRel, skillIndex))
|
|
29575
|
-
continue;
|
|
29576
|
-
if (!needed.has(generatedRel)) {
|
|
29577
|
-
issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
|
|
29578
|
-
}
|
|
29889
|
+
const found = checkNeededCopy(root2, targetRel);
|
|
29890
|
+
if (found)
|
|
29891
|
+
issues.push(found);
|
|
29579
29892
|
}
|
|
29893
|
+
issues.push(...checkOrphanedCopies(root2, needed, skillIndex));
|
|
29580
29894
|
for (const plan of plans) {
|
|
29581
|
-
|
|
29582
|
-
if (!existsSync13(skillDir))
|
|
29583
|
-
continue;
|
|
29584
|
-
const walk = (dir) => {
|
|
29585
|
-
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
29586
|
-
if (entry.name.startsWith("."))
|
|
29587
|
-
continue;
|
|
29588
|
-
const fullPath = join11(dir, entry.name);
|
|
29589
|
-
if (entry.isDirectory()) {
|
|
29590
|
-
walk(fullPath);
|
|
29591
|
-
continue;
|
|
29592
|
-
}
|
|
29593
|
-
if (!entry.name.endsWith(".md"))
|
|
29594
|
-
continue;
|
|
29595
|
-
const relFile = normalizeRelPath(relative7(root2, fullPath));
|
|
29596
|
-
const content3 = readFileSync10(fullPath, "utf8");
|
|
29597
|
-
if (content3.match(SHARED_REF_LINK_RE)) {
|
|
29598
|
-
issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
|
|
29599
|
-
}
|
|
29600
|
-
}
|
|
29601
|
-
};
|
|
29602
|
-
walk(skillDir);
|
|
29895
|
+
issues.push(...checkStaleSharedLinks(root2, join11(root2, plan.skill)));
|
|
29603
29896
|
}
|
|
29604
29897
|
return issues;
|
|
29605
29898
|
}
|
|
@@ -29626,6 +29919,51 @@ var bannedRule = { id: "banned", run: runBannedRule };
|
|
|
29626
29919
|
// src/audit/rules/doc-meta.ts
|
|
29627
29920
|
import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
|
|
29628
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
|
+
}
|
|
29629
29967
|
function runDocMetaRule(ctx) {
|
|
29630
29968
|
const issues = [];
|
|
29631
29969
|
const today = new Date;
|
|
@@ -29634,32 +29972,27 @@ function runDocMetaRule(ctx) {
|
|
|
29634
29972
|
if (!existsSync14(abs))
|
|
29635
29973
|
continue;
|
|
29636
29974
|
const content3 = readFileSync11(abs, "utf8");
|
|
29637
|
-
|
|
29638
|
-
|
|
29639
|
-
|
|
29640
|
-
}
|
|
29641
|
-
const reviewedStr = docMetaLastReviewed(content3);
|
|
29642
|
-
if (!reviewedStr)
|
|
29975
|
+
const banner = checkDocMetaBanner(relPath2, content3);
|
|
29976
|
+
if (banner) {
|
|
29977
|
+
issues.push(banner);
|
|
29643
29978
|
continue;
|
|
29644
|
-
const reviewed = new Date(`${reviewedStr}T00:00:00Z`);
|
|
29645
|
-
if (Number.isNaN(reviewed.getTime()))
|
|
29646
|
-
continue;
|
|
29647
|
-
const ageDays = (today.getTime() - reviewed.getTime()) / 86400000;
|
|
29648
|
-
if (ageDays > ctx.config.daysUntilStale) {
|
|
29649
|
-
issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
|
|
29650
|
-
}
|
|
29651
|
-
const slug2 = slugFromPath(relPath2, ctx.root);
|
|
29652
|
-
if (slug2 !== null && ctx.lockedSkillSlugs.has(slug2))
|
|
29653
|
-
continue;
|
|
29654
|
-
const gitDate = lastGitCommitDate(relPath2, ctx.root);
|
|
29655
|
-
if (!gitDate)
|
|
29656
|
-
continue;
|
|
29657
|
-
const committed = new Date(`${gitDate}T00:00:00Z`);
|
|
29658
|
-
if (Number.isNaN(committed.getTime()))
|
|
29659
|
-
continue;
|
|
29660
|
-
if (committed.getTime() > reviewed.getTime()) {
|
|
29661
|
-
issues.push(issue("doc-meta", relPath2, `content changed after last-reviewed ${reviewedStr} (git: ${gitDate}) — bump last-reviewed or confirm review`, { severity: "warning" }));
|
|
29662
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);
|
|
29663
29996
|
}
|
|
29664
29997
|
return issues;
|
|
29665
29998
|
}
|
|
@@ -29674,57 +30007,88 @@ function resolveLink2(sourceFile, target) {
|
|
|
29674
30007
|
return sourceFile;
|
|
29675
30008
|
return resolve7(dirname7(sourceFile), withoutAnchor);
|
|
29676
30009
|
}
|
|
29677
|
-
function
|
|
29678
|
-
const
|
|
29679
|
-
|
|
29680
|
-
|
|
29681
|
-
|
|
29682
|
-
|
|
29683
|
-
|
|
29684
|
-
|
|
29685
|
-
|
|
29686
|
-
|
|
29687
|
-
|
|
29688
|
-
|
|
29689
|
-
|
|
29690
|
-
|
|
29691
|
-
|
|
29692
|
-
|
|
29693
|
-
|
|
29694
|
-
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29698
|
-
|
|
29699
|
-
|
|
29700
|
-
|
|
29701
|
-
return issues;
|
|
29702
|
-
}
|
|
29703
|
-
}
|
|
29704
|
-
if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
|
|
29705
|
-
const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
|
|
29706
|
-
if (!existsSync15(agentPath)) {
|
|
29707
|
-
issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
|
|
29708
|
-
}
|
|
29709
|
-
return issues;
|
|
29710
|
-
}
|
|
29711
|
-
if (pathPart && !existsSync15(resolved)) {
|
|
29712
|
-
issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
|
|
29713
|
-
link: linkLabel
|
|
29714
|
-
}));
|
|
29715
|
-
return issues;
|
|
29716
|
-
}
|
|
29717
|
-
if (anchor && existsSync15(resolved)) {
|
|
29718
|
-
const targetContent = readFileSync12(resolved, "utf8");
|
|
29719
|
-
const slugs = extractHeadingSlugs(targetContent, resolved);
|
|
29720
|
-
const anchorSlug = slugifyAnchor(anchor);
|
|
29721
|
-
if (!slugs.has(anchorSlug)) {
|
|
29722
|
-
issues.push(issue("links", relSource, `broken anchor → #${anchor} in ${relTarget}`, {
|
|
29723
|
-
link: linkLabel
|
|
29724
|
-
}));
|
|
29725
|
-
}
|
|
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;
|
|
29726
30034
|
}
|
|
29727
|
-
|
|
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] : [];
|
|
29728
30092
|
}
|
|
29729
30093
|
function runLinksRule(ctx) {
|
|
29730
30094
|
const issues = [];
|
|
@@ -29733,7 +30097,7 @@ function runLinksRule(ctx) {
|
|
|
29733
30097
|
const links = extractLinksFromMarkdown(content3, filePath);
|
|
29734
30098
|
for (const { target, line } of links) {
|
|
29735
30099
|
const linkLabel = line ? `line ${line}` : target;
|
|
29736
|
-
issues.push(...validateTarget(ctx, filePath, target, linkLabel));
|
|
30100
|
+
issues.push(...validateTarget({ ctx, sourceFile: filePath, target, linkLabel }));
|
|
29737
30101
|
}
|
|
29738
30102
|
}
|
|
29739
30103
|
return issues;
|
|
@@ -29741,6 +30105,44 @@ function runLinksRule(ctx) {
|
|
|
29741
30105
|
var linksRule = { id: "links", run: runLinksRule };
|
|
29742
30106
|
|
|
29743
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
|
+
}
|
|
29744
30146
|
function runProsePolicyRule(ctx) {
|
|
29745
30147
|
if (ctx.policies.length === 0)
|
|
29746
30148
|
return [];
|
|
@@ -29753,36 +30155,9 @@ function runProsePolicyRule(ctx) {
|
|
|
29753
30155
|
`);
|
|
29754
30156
|
const policies = policiesForFile(ctx.policies, rel);
|
|
29755
30157
|
for (const entry of policies) {
|
|
29756
|
-
if (entry.mode === "fingerprint")
|
|
29757
|
-
continue;
|
|
29758
|
-
if (!entry.regex)
|
|
30158
|
+
if (entry.mode === "fingerprint" || !entry.regex)
|
|
29759
30159
|
continue;
|
|
29760
|
-
|
|
29761
|
-
for (let i = 0;i < lines.length; i++) {
|
|
29762
|
-
if (entry.regex.test(lines[i] ?? "") && !isDraftPlacementAllowed(rel, draftPrefixes)) {
|
|
29763
|
-
issues.push(issue("prose-policy", rel, entry.message, {
|
|
29764
|
-
link: `line ${i + 1}`,
|
|
29765
|
-
severity: entry.severity
|
|
29766
|
-
}));
|
|
29767
|
-
}
|
|
29768
|
-
}
|
|
29769
|
-
continue;
|
|
29770
|
-
}
|
|
29771
|
-
const isMultiline = entry.pattern?.includes("[\\s\\S]");
|
|
29772
|
-
if (isMultiline) {
|
|
29773
|
-
if (entry.regex.test(content3)) {
|
|
29774
|
-
issues.push(issue("prose-policy", rel, entry.message, { severity: entry.severity }));
|
|
29775
|
-
}
|
|
29776
|
-
continue;
|
|
29777
|
-
}
|
|
29778
|
-
for (let i = 0;i < lines.length; i++) {
|
|
29779
|
-
if (entry.regex.test(lines[i] ?? "")) {
|
|
29780
|
-
issues.push(issue("prose-policy", rel, entry.message, {
|
|
29781
|
-
link: `line ${i + 1}`,
|
|
29782
|
-
severity: entry.severity
|
|
29783
|
-
}));
|
|
29784
|
-
}
|
|
29785
|
-
}
|
|
30160
|
+
issues.push(...checkPolicyEntry({ rel, content: content3, lines, entry, draftPrefixes }));
|
|
29786
30161
|
}
|
|
29787
30162
|
}
|
|
29788
30163
|
return issues;
|
|
@@ -29792,12 +30167,8 @@ var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
|
|
|
29792
30167
|
// src/audit/rules/registry.ts
|
|
29793
30168
|
import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
|
|
29794
30169
|
import { join as join13 } from "node:path";
|
|
29795
|
-
function
|
|
30170
|
+
function checkRegistryEntries(ctx) {
|
|
29796
30171
|
const issues = [];
|
|
29797
|
-
const registry = new Set(ctx.registryPaths);
|
|
29798
|
-
if (ctx.registryHasTableHeader && ctx.registryPaths.length === 0) {
|
|
29799
|
-
issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
|
|
29800
|
-
}
|
|
29801
30172
|
for (const rel of ctx.registryPaths) {
|
|
29802
30173
|
const abs = join13(ctx.root, rel);
|
|
29803
30174
|
if (!existsSync16(abs)) {
|
|
@@ -29809,11 +30180,15 @@ function runRegistryRule(ctx) {
|
|
|
29809
30180
|
issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
|
|
29810
30181
|
}
|
|
29811
30182
|
}
|
|
30183
|
+
return issues;
|
|
30184
|
+
}
|
|
30185
|
+
function checkUnregisteredBanners(ctx, registry) {
|
|
30186
|
+
const issues = [];
|
|
29812
30187
|
for (const filePath of ctx.files) {
|
|
29813
30188
|
const rel = relPath(filePath, ctx.root);
|
|
29814
30189
|
if (rel === REGISTRY_REL_PATH)
|
|
29815
30190
|
continue;
|
|
29816
|
-
if (!rel.endsWith(".md")
|
|
30191
|
+
if (!(rel.endsWith(".md") || rel.endsWith(".mdc")))
|
|
29817
30192
|
continue;
|
|
29818
30193
|
const content3 = readFileSync13(filePath, "utf8");
|
|
29819
30194
|
if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
|
|
@@ -29824,6 +30199,16 @@ function runRegistryRule(ctx) {
|
|
|
29824
30199
|
}
|
|
29825
30200
|
return issues;
|
|
29826
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
|
+
}
|
|
29827
30212
|
var registryRule = { id: "registry", run: runRegistryRule };
|
|
29828
30213
|
|
|
29829
30214
|
// src/audit/rules/scan-gaps.ts
|
|
@@ -29835,7 +30220,10 @@ function runCoverageGapsRule(ctx) {
|
|
|
29835
30220
|
for (const rel of candidates) {
|
|
29836
30221
|
if (scanned.has(rel))
|
|
29837
30222
|
continue;
|
|
29838
|
-
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
|
+
}));
|
|
29839
30227
|
}
|
|
29840
30228
|
return issues;
|
|
29841
30229
|
}
|
|
@@ -29905,70 +30293,79 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
|
|
|
29905
30293
|
}
|
|
29906
30294
|
return issues;
|
|
29907
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
|
+
}
|
|
29908
30322
|
function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
|
|
29909
30323
|
const issues = [];
|
|
29910
30324
|
const nonPublic = new Set(nonPublicSkills(ctx.config));
|
|
29911
30325
|
for (const skillRoot of index2.roots) {
|
|
29912
30326
|
if (skillRoot.kind !== "nested")
|
|
29913
30327
|
continue;
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
|
|
29917
|
-
|
|
29918
|
-
|
|
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))
|
|
29919
30342
|
continue;
|
|
29920
|
-
const
|
|
29921
|
-
|
|
29922
|
-
const foreign = new Set(index2.foreignSlugs);
|
|
29923
|
-
const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
|
|
29924
|
-
const relReadme = `${skillRoot.relPath}/README.md`;
|
|
29925
|
-
for (const slug2 of publicSlugs) {
|
|
29926
|
-
if (!taxonomySlugs.includes(slug2)) {
|
|
29927
|
-
issues.push(issue("skill-index", relReadme, `taxonomy missing public skill "${slug2}"`));
|
|
29928
|
-
}
|
|
29929
|
-
}
|
|
29930
|
-
for (const slug2 of taxonomySlugs) {
|
|
29931
|
-
if (!nestedSlugs.includes(slug2)) {
|
|
29932
|
-
issues.push(issue("skill-index", relReadme, `taxonomy lists skill "${slug2}" with no SKILL.md on disk`));
|
|
29933
|
-
}
|
|
30343
|
+
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
30344
|
+
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29934
30345
|
}
|
|
29935
30346
|
}
|
|
29936
30347
|
return issues;
|
|
29937
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
|
+
}
|
|
29938
30357
|
function runSkillIndexRule(ctx) {
|
|
29939
30358
|
const issues = [];
|
|
29940
30359
|
const index2 = ctx.skillIndex;
|
|
29941
30360
|
const diskSlugs = listSkillSlugs(index2);
|
|
29942
30361
|
for (const warning of index2.provenance.warnings) {
|
|
29943
|
-
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}`,
|
|
29944
30364
|
severity: "warning"
|
|
29945
30365
|
}));
|
|
29946
30366
|
}
|
|
29947
30367
|
issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
|
|
29948
|
-
|
|
29949
|
-
for (const skillRoot of index2.roots) {
|
|
29950
|
-
if (skillRoot.kind === "nested") {
|
|
29951
|
-
for (const slug2 of index2.ownedSlugs) {
|
|
29952
|
-
const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
|
|
29953
|
-
if (!existsSync17(skillDir))
|
|
29954
|
-
continue;
|
|
29955
|
-
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29956
|
-
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29957
|
-
}
|
|
29958
|
-
}
|
|
29959
|
-
continue;
|
|
29960
|
-
}
|
|
29961
|
-
for (const slug2 of index2.flatSlugs) {
|
|
29962
|
-
if (!owned.has(slug2))
|
|
29963
|
-
continue;
|
|
29964
|
-
const skillDir = join14(ctx.root, slug2);
|
|
29965
|
-
if (!existsSync17(skillDir))
|
|
29966
|
-
continue;
|
|
29967
|
-
for (const skillMd of walkSkillMarkdown(skillDir)) {
|
|
29968
|
-
issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
|
|
29969
|
-
}
|
|
29970
|
-
}
|
|
29971
|
-
}
|
|
30368
|
+
issues.push(...auditOwnedSkillFiles(ctx, index2));
|
|
29972
30369
|
return issues;
|
|
29973
30370
|
}
|
|
29974
30371
|
var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
|
|
@@ -29996,6 +30393,19 @@ var skillsRules = [
|
|
|
29996
30393
|
prosePolicyRule
|
|
29997
30394
|
];
|
|
29998
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
|
+
}
|
|
29999
30409
|
function assembleRules(pluginRules = []) {
|
|
30000
30410
|
const coreIds = new Set(allRules.map((rule) => rule.id));
|
|
30001
30411
|
const seenPlugin = new Set;
|
|
@@ -30008,17 +30418,7 @@ function assembleRules(pluginRules = []) {
|
|
|
30008
30418
|
const docs = [...docsRules];
|
|
30009
30419
|
const skills = [...skillsRules];
|
|
30010
30420
|
for (const rule of pluginRules) {
|
|
30011
|
-
|
|
30012
|
-
const inDocs = suites.includes("docs");
|
|
30013
|
-
const inSkills = suites.includes("skills");
|
|
30014
|
-
if (!inDocs && !inSkills) {
|
|
30015
|
-
const listed = suites.length === 0 ? "(empty)" : suites.join(", ");
|
|
30016
|
-
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.`);
|
|
30017
|
-
}
|
|
30018
|
-
if (inDocs)
|
|
30019
|
-
docs.push(rule);
|
|
30020
|
-
if (inSkills)
|
|
30021
|
-
skills.push(rule);
|
|
30421
|
+
attachPluginRule(docs, skills, rule);
|
|
30022
30422
|
}
|
|
30023
30423
|
const selfById = new Map;
|
|
30024
30424
|
for (const rule of [...docs, ...skills]) {
|
|
@@ -30041,46 +30441,70 @@ function rulesForSuite(suite, pluginRules = []) {
|
|
|
30041
30441
|
}
|
|
30042
30442
|
|
|
30043
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
|
+
}
|
|
30044
30494
|
function parseAuditArgs(argv) {
|
|
30045
|
-
|
|
30046
|
-
|
|
30047
|
-
|
|
30048
|
-
|
|
30049
|
-
|
|
30050
|
-
|
|
30051
|
-
|
|
30495
|
+
const state = {
|
|
30496
|
+
suite: "docs",
|
|
30497
|
+
strict: false,
|
|
30498
|
+
json: false,
|
|
30499
|
+
dryRun: false,
|
|
30500
|
+
paths: [],
|
|
30501
|
+
only: null,
|
|
30502
|
+
fix: null
|
|
30503
|
+
};
|
|
30052
30504
|
for (let i = 0;i < argv.length; i++) {
|
|
30053
|
-
|
|
30054
|
-
if (arg.startsWith("--suite=")) {
|
|
30055
|
-
suite = arg.slice("--suite=".length);
|
|
30056
|
-
} else if (arg === "--strict") {
|
|
30057
|
-
strict = true;
|
|
30058
|
-
} else if (arg === "--json") {
|
|
30059
|
-
json = true;
|
|
30060
|
-
} else if (arg === "--dry-run") {
|
|
30061
|
-
dryRun = true;
|
|
30062
|
-
} else if (arg.startsWith("--dry-run=")) {
|
|
30063
|
-
throw new Error("audit: use --dry-run (boolean flag), not --dry-run=<value>");
|
|
30064
|
-
} else if (arg === "--fix") {
|
|
30065
|
-
const next = argv[i + 1];
|
|
30066
|
-
if (next && !next.startsWith("-")) {
|
|
30067
|
-
if (next !== "doc-meta" && next !== "anchors") {
|
|
30068
|
-
throw new Error(`Unknown --fix kind: ${next}. Use --fix, --fix=doc-meta, or --fix=anchors.`);
|
|
30069
|
-
}
|
|
30070
|
-
fix = next;
|
|
30071
|
-
i++;
|
|
30072
|
-
} else {
|
|
30073
|
-
fix = true;
|
|
30074
|
-
}
|
|
30075
|
-
} else if (arg.startsWith("--fix=")) {
|
|
30076
|
-
fix = arg.slice("--fix=".length);
|
|
30077
|
-
} else if (arg.startsWith("--paths=")) {
|
|
30078
|
-
paths = arg.slice("--paths=".length).split(",").map((path2) => path2.trim()).filter(Boolean);
|
|
30079
|
-
} else if (arg.startsWith("--only=")) {
|
|
30080
|
-
only = new Set(arg.slice("--only=".length).split(",").filter(Boolean));
|
|
30081
|
-
}
|
|
30505
|
+
i = applyAuditFlag({ state, arg: argv[i] ?? "", argv, index: i });
|
|
30082
30506
|
}
|
|
30083
|
-
return
|
|
30507
|
+
return state;
|
|
30084
30508
|
}
|
|
30085
30509
|
function labelForSuite(suite) {
|
|
30086
30510
|
switch (suite) {
|
|
@@ -30103,6 +30527,29 @@ function shouldRunRule(rule, options, pathScoped) {
|
|
|
30103
30527
|
return false;
|
|
30104
30528
|
return true;
|
|
30105
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
|
+
}
|
|
30106
30553
|
async function runAudit(options) {
|
|
30107
30554
|
const pathScoped = options.paths.length > 0;
|
|
30108
30555
|
const base = createContext({
|
|
@@ -30112,26 +30559,9 @@ async function runAudit(options) {
|
|
|
30112
30559
|
});
|
|
30113
30560
|
const loaded = await loadPlugins(base.root, base.config);
|
|
30114
30561
|
const ctx = { ...base, policies: loaded.policies };
|
|
30115
|
-
|
|
30116
|
-
|
|
30117
|
-
|
|
30118
|
-
return 1;
|
|
30119
|
-
}
|
|
30120
|
-
const kinds = fixKindsForOnly(parseFixKinds(options.fix), options.only);
|
|
30121
|
-
if (kinds.length === 0) {
|
|
30122
|
-
console.error("--fix has no overlapping rules with --only (doc-meta → doc-meta, anchors → links).");
|
|
30123
|
-
return 1;
|
|
30124
|
-
}
|
|
30125
|
-
applyFixes(ctx, { kinds, dryRun: options.dryRun });
|
|
30126
|
-
if (!options.dryRun) {
|
|
30127
|
-
const refreshed = createContext({
|
|
30128
|
-
root: options.root,
|
|
30129
|
-
paths: options.paths.length > 0 ? options.paths : undefined,
|
|
30130
|
-
policies: loaded.policies
|
|
30131
|
-
});
|
|
30132
|
-
Object.assign(ctx, refreshed);
|
|
30133
|
-
}
|
|
30134
|
-
}
|
|
30562
|
+
const fixExit = await runAuditFixes(options, ctx, loaded);
|
|
30563
|
+
if (fixExit !== null)
|
|
30564
|
+
return fixExit;
|
|
30135
30565
|
const rules = rulesForSuite(options.suite, loaded.rules).filter((r) => !options.only || options.only.has(r.id));
|
|
30136
30566
|
const skipGlobalsForPaths = pathScoped && !options.globalOnly;
|
|
30137
30567
|
const issues = [];
|
|
@@ -30161,7 +30591,7 @@ function customizePathForSlug(root2, slug2) {
|
|
|
30161
30591
|
return join15(customizeDir(root2), `${slug2}.md`);
|
|
30162
30592
|
}
|
|
30163
30593
|
function findCustomizeViaRegistry(root2, slug2) {
|
|
30164
|
-
for (const rel of
|
|
30594
|
+
for (const rel of parseRegistry(root2).paths) {
|
|
30165
30595
|
const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
|
|
30166
30596
|
if (normalizeRelPath(rel) === expected && existsSync18(join15(root2, rel))) {
|
|
30167
30597
|
return rel;
|
|
@@ -30214,11 +30644,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
|
|
|
30214
30644
|
function resolveCustomize(root2, slug2) {
|
|
30215
30645
|
const slugFile = resolveSlugFile(root2, slug2);
|
|
30216
30646
|
const alwaysNames = alwaysIncludeBasenames(root2);
|
|
30217
|
-
const skip = slugFile.path
|
|
30647
|
+
const skip = slugFile.path !== null && slugFile.path !== undefined ? basename3(slugFile.path) : null;
|
|
30218
30648
|
const always = readAlwaysInclude(root2, alwaysNames, skip);
|
|
30219
30649
|
const parts = [];
|
|
30220
30650
|
const included = [];
|
|
30221
|
-
if (slugFile.content
|
|
30651
|
+
if (slugFile.content !== null && slugFile.content !== undefined && slugFile.content.trim().length > 0) {
|
|
30222
30652
|
parts.push(slugFile.content.trimEnd());
|
|
30223
30653
|
if (slugFile.path)
|
|
30224
30654
|
included.push(slugFile.path);
|
|
@@ -30246,6 +30676,7 @@ function resolveCustomizeFromRoot(slug2, startDir) {
|
|
|
30246
30676
|
}
|
|
30247
30677
|
|
|
30248
30678
|
// src/hooks/run.ts
|
|
30679
|
+
import process3 from "node:process";
|
|
30249
30680
|
function parsePayload(raw) {
|
|
30250
30681
|
if (!raw.trim())
|
|
30251
30682
|
return {};
|
|
@@ -30276,7 +30707,7 @@ function extractSkillSlug(payload) {
|
|
|
30276
30707
|
}
|
|
30277
30708
|
const path2 = extractPath(payload);
|
|
30278
30709
|
if (path2)
|
|
30279
|
-
return slugFromPath(path2,
|
|
30710
|
+
return slugFromPath(path2, process3.cwd());
|
|
30280
30711
|
return null;
|
|
30281
30712
|
}
|
|
30282
30713
|
function cursorResponse(content3) {
|
|
@@ -30331,6 +30762,7 @@ Customize override for /${slug2} (from ${from}):
|
|
|
30331
30762
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
30332
30763
|
import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
|
|
30333
30764
|
import { join as join19 } from "node:path";
|
|
30765
|
+
import process4 from "node:process";
|
|
30334
30766
|
|
|
30335
30767
|
// src/init/merge-hooks.ts
|
|
30336
30768
|
import { existsSync as existsSync21, mkdirSync, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -30400,7 +30832,7 @@ function walkNodeModulesCli(cwd) {
|
|
|
30400
30832
|
}
|
|
30401
30833
|
function isInsidePackageRoot(cwd) {
|
|
30402
30834
|
const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
|
|
30403
|
-
return rel === "" || !rel.startsWith("..")
|
|
30835
|
+
return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
|
|
30404
30836
|
}
|
|
30405
30837
|
function nodeCliHookCommand(cliPath) {
|
|
30406
30838
|
return `node ${cliPath} hook customize`;
|
|
@@ -30450,6 +30882,36 @@ function writeJson(path2, value) {
|
|
|
30450
30882
|
function deepEqual(a, b) {
|
|
30451
30883
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
30452
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
|
+
}
|
|
30453
30915
|
function mergeCursorHooks(targetPath, fragment, opts) {
|
|
30454
30916
|
const existing = readJson(targetPath) ?? {};
|
|
30455
30917
|
const hooks = existing.hooks ?? {};
|
|
@@ -30460,79 +30922,103 @@ function mergeCursorHooks(targetPath, fragment, opts) {
|
|
|
30460
30922
|
const skeletonIdx = postToolUse.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
30461
30923
|
const canonical = { ...incoming, matcher: incoming.matcher ?? "Read" };
|
|
30462
30924
|
if (skeletonIdx >= 0) {
|
|
30463
|
-
const
|
|
30464
|
-
|
|
30465
|
-
|
|
30466
|
-
return {
|
|
30467
|
-
platform: "cursor",
|
|
30468
|
-
action: "conflict",
|
|
30469
|
-
message: identityKey("cursor", "postToolUse", String(current.matcher ?? "Read"))
|
|
30470
|
-
};
|
|
30471
|
-
}
|
|
30472
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["command", "matcher"].includes(key)));
|
|
30473
|
-
const merged = { ...extras, ...canonical };
|
|
30474
|
-
if (deepEqual(current, merged))
|
|
30475
|
-
return { platform: "cursor", action: "skipped" };
|
|
30476
|
-
postToolUse[skeletonIdx] = merged;
|
|
30925
|
+
const conflict = mergeExistingCursorHook({ postToolUse, skeletonIdx, canonical, opts });
|
|
30926
|
+
if (conflict)
|
|
30927
|
+
return conflict;
|
|
30477
30928
|
} else {
|
|
30478
30929
|
postToolUse.push(canonical);
|
|
30479
30930
|
}
|
|
30480
|
-
|
|
30481
|
-
...existing,
|
|
30482
|
-
version: existing.version ?? 1,
|
|
30483
|
-
hooks: { ...hooks, postToolUse }
|
|
30484
|
-
};
|
|
30485
|
-
if (deepEqual(existing, next))
|
|
30486
|
-
return { platform: "cursor", action: "skipped" };
|
|
30487
|
-
writeJson(targetPath, next);
|
|
30488
|
-
return { platform: "cursor", action: skeletonIdx >= 0 ? "updated" : "added" };
|
|
30931
|
+
return persistCursorHooks({ targetPath, existing, hooks, postToolUse, skeletonIdx });
|
|
30489
30932
|
}
|
|
30490
|
-
function
|
|
30491
|
-
const
|
|
30492
|
-
const
|
|
30493
|
-
const
|
|
30494
|
-
|
|
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) {
|
|
30495
30986
|
let changed = false;
|
|
30496
|
-
for (const incomingGroup of incomingGroups) {
|
|
30497
|
-
const matcher = incomingGroup.matcher ?? "";
|
|
30987
|
+
for (const incomingGroup of args.incomingGroups) {
|
|
30498
30988
|
const incomingHook = incomingGroup.hooks?.[0];
|
|
30499
30989
|
if (!incomingHook)
|
|
30500
30990
|
continue;
|
|
30501
|
-
const
|
|
30502
|
-
|
|
30503
|
-
|
|
30504
|
-
|
|
30505
|
-
|
|
30506
|
-
|
|
30507
|
-
|
|
30508
|
-
|
|
30509
|
-
|
|
30510
|
-
|
|
30511
|
-
|
|
30512
|
-
const skeletonIdx = hooks.findIndex((entry) => isSkeletonHookCommand(entry.command));
|
|
30513
|
-
if (skeletonIdx >= 0) {
|
|
30514
|
-
const current = hooks[skeletonIdx];
|
|
30515
|
-
const userEdited = current && !opts.forceHooks && (current.type !== incomingHook.type || !isSkeletonHookCommand(current.command));
|
|
30516
|
-
if (userEdited) {
|
|
30517
|
-
return {
|
|
30518
|
-
platform,
|
|
30519
|
-
action: "conflict",
|
|
30520
|
-
message: identityKey(platform, eventName, matcher)
|
|
30521
|
-
};
|
|
30522
|
-
}
|
|
30523
|
-
const extras = Object.fromEntries(Object.entries(current ?? {}).filter(([key]) => !["type", "command"].includes(key)));
|
|
30524
|
-
const merged = { ...extras, ...incomingHook };
|
|
30525
|
-
if (!deepEqual(current, merged)) {
|
|
30526
|
-
hooks[skeletonIdx] = merged;
|
|
30527
|
-
changed = true;
|
|
30528
|
-
}
|
|
30529
|
-
} else {
|
|
30530
|
-
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)
|
|
30531
31002
|
changed = true;
|
|
30532
|
-
}
|
|
30533
|
-
eventHooks[groupIdx] = { ...group, matcher, hooks };
|
|
30534
31003
|
}
|
|
30535
|
-
|
|
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)
|
|
30536
31022
|
return { platform, action: "skipped" };
|
|
30537
31023
|
const next = {
|
|
30538
31024
|
...existing,
|
|
@@ -30548,11 +31034,23 @@ function mergeHookConfigs(opts) {
|
|
|
30548
31034
|
results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
|
|
30549
31035
|
const claudePath = join18(opts.cwd, ".claude/settings.json");
|
|
30550
31036
|
const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
|
|
30551
|
-
results.push(mergeNestedHooks(
|
|
31037
|
+
results.push(mergeNestedHooks({
|
|
31038
|
+
platform: "claude",
|
|
31039
|
+
targetPath: claudePath,
|
|
31040
|
+
fragment: claudeFragment,
|
|
31041
|
+
eventName: "PostToolUse",
|
|
31042
|
+
opts
|
|
31043
|
+
}));
|
|
30552
31044
|
const codexPath = join18(opts.cwd, ".codex/hooks.json");
|
|
30553
31045
|
if (existsSync21(join18(opts.cwd, ".codex"))) {
|
|
30554
31046
|
const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
|
|
30555
|
-
results.push(mergeNestedHooks(
|
|
31047
|
+
results.push(mergeNestedHooks({
|
|
31048
|
+
platform: "codex",
|
|
31049
|
+
targetPath: codexPath,
|
|
31050
|
+
fragment: codexFragment,
|
|
31051
|
+
eventName: "PostToolUse",
|
|
31052
|
+
opts
|
|
31053
|
+
}));
|
|
30556
31054
|
} else {
|
|
30557
31055
|
results.push({ platform: "codex", action: "skipped", message: "missing .codex directory" });
|
|
30558
31056
|
}
|
|
@@ -30657,26 +31155,36 @@ function runSkillsAdd(args, cwd) {
|
|
|
30657
31155
|
});
|
|
30658
31156
|
return result.status ?? 1;
|
|
30659
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
|
+
}
|
|
30660
31179
|
function runInit(options = {}) {
|
|
30661
|
-
const cwd = options.cwd ??
|
|
31180
|
+
const cwd = options.cwd ?? process4.cwd();
|
|
30662
31181
|
assertPackageResolvable(cwd);
|
|
30663
31182
|
const scaffold = writeScaffold(cwd);
|
|
30664
31183
|
const hookCommand = resolveHookCommand(cwd);
|
|
30665
|
-
const hooks = mergeHookConfigs({
|
|
30666
|
-
cwd,
|
|
30667
|
-
hookCommand,
|
|
30668
|
-
forceHooks: options.forceHooks
|
|
30669
|
-
});
|
|
31184
|
+
const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
|
|
30670
31185
|
const scripts = mergePackageJsonScripts(cwd);
|
|
30671
|
-
for (const result of hooks)
|
|
30672
|
-
|
|
30673
|
-
console.error(`init: skipped ${result.platform} hook (user-edited) — re-run with --force-hooks to restore`);
|
|
30674
|
-
} else if (result.action === "added") {
|
|
30675
|
-
console.log(`init: added ${result.platform} customize hook`);
|
|
30676
|
-
} else if (result.action === "updated") {
|
|
30677
|
-
console.log(`init: updated ${result.platform} customize hook`);
|
|
30678
|
-
}
|
|
30679
|
-
}
|
|
31186
|
+
for (const result of hooks)
|
|
31187
|
+
logHookMergeResult(result);
|
|
30680
31188
|
if (scaffold === "created") {
|
|
30681
31189
|
console.log("init: wrote .skeleton/config.yaml and registry.md");
|
|
30682
31190
|
} else {
|
|
@@ -30685,17 +31193,7 @@ function runInit(options = {}) {
|
|
|
30685
31193
|
if (scripts === "updated") {
|
|
30686
31194
|
console.log("init: merged validate/audit scripts into package.json");
|
|
30687
31195
|
}
|
|
30688
|
-
|
|
30689
|
-
if (options.skills && !options.noSkills) {
|
|
30690
|
-
const args = skillsAddArgs({ skillsFlags: options.skillsFlags });
|
|
30691
|
-
const run = options.runSkillsCommand ?? runSkillsAdd;
|
|
30692
|
-
const exitCode = run(args, cwd);
|
|
30693
|
-
if (exitCode !== 0) {
|
|
30694
|
-
throw new Error(`skills install failed: npx ${args.join(" ")}`);
|
|
30695
|
-
}
|
|
30696
|
-
skills = "installed";
|
|
30697
|
-
console.log("init: installed /skeleton skill");
|
|
30698
|
-
}
|
|
31196
|
+
const skills = installSkillsIfRequested(options, cwd);
|
|
30699
31197
|
return { scaffold, hooks, scripts, skills };
|
|
30700
31198
|
}
|
|
30701
31199
|
|
|
@@ -30786,7 +31284,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
|
|
|
30786
31284
|
hash.update("\x00");
|
|
30787
31285
|
hash.update(content3);
|
|
30788
31286
|
hash.update("\x00");
|
|
30789
|
-
for (const dep of localImportPaths(abs, content3).sort()) {
|
|
31287
|
+
for (const dep of localImportPaths(abs, content3).sort((a, b) => a.localeCompare(b))) {
|
|
30790
31288
|
walk(dep);
|
|
30791
31289
|
}
|
|
30792
31290
|
}
|
|
@@ -30857,6 +31355,9 @@ async function runBuildPlugin(options = {}) {
|
|
|
30857
31355
|
return { built, checked };
|
|
30858
31356
|
}
|
|
30859
31357
|
|
|
31358
|
+
// src/references/run.ts
|
|
31359
|
+
import process6 from "node:process";
|
|
31360
|
+
|
|
30860
31361
|
// src/references/sync.ts
|
|
30861
31362
|
import {
|
|
30862
31363
|
existsSync as existsSync24,
|
|
@@ -30867,14 +31368,13 @@ import {
|
|
|
30867
31368
|
writeFileSync as writeFileSync4
|
|
30868
31369
|
} from "node:fs";
|
|
30869
31370
|
import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
|
|
31371
|
+
import process5 from "node:process";
|
|
30870
31372
|
function resolveOwnership(root2, override) {
|
|
30871
31373
|
if (override !== undefined)
|
|
30872
31374
|
return override;
|
|
30873
31375
|
try {
|
|
30874
31376
|
return loadConfig(root2).skillOwnership;
|
|
30875
|
-
} catch {
|
|
30876
|
-
return;
|
|
30877
|
-
}
|
|
31377
|
+
} catch {}
|
|
30878
31378
|
}
|
|
30879
31379
|
function walkMarkdownFiles2(dir, root2) {
|
|
30880
31380
|
const files = [];
|
|
@@ -30894,89 +31394,97 @@ function walkMarkdownFiles2(dir, root2) {
|
|
|
30894
31394
|
}
|
|
30895
31395
|
return files;
|
|
30896
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
|
+
}
|
|
30897
31414
|
function listGeneratedReferenceFiles(skillDir, skill) {
|
|
30898
31415
|
const refsDir = join20(skillDir, "references");
|
|
30899
31416
|
if (!existsSync24(refsDir))
|
|
30900
31417
|
return [];
|
|
30901
31418
|
const files = [];
|
|
30902
|
-
|
|
30903
|
-
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
30904
|
-
const fullPath = join20(dir, entry.name);
|
|
30905
|
-
if (entry.isDirectory()) {
|
|
30906
|
-
walk(fullPath);
|
|
30907
|
-
continue;
|
|
30908
|
-
}
|
|
30909
|
-
if (!entry.name.endsWith(".md"))
|
|
30910
|
-
continue;
|
|
30911
|
-
const content3 = readFileSync19(fullPath, "utf8");
|
|
30912
|
-
if (isGeneratedReference(content3)) {
|
|
30913
|
-
const refPath = normalizeRelPath(relative11(refsDir, fullPath));
|
|
30914
|
-
files.push(generatedRefPath(skill, refPath));
|
|
30915
|
-
}
|
|
30916
|
-
}
|
|
30917
|
-
};
|
|
30918
|
-
walk(refsDir);
|
|
31419
|
+
collectGeneratedInDir({ dir: refsDir, refsDir, skill, files });
|
|
30919
31420
|
return files;
|
|
30920
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
|
+
}
|
|
30921
31478
|
function syncReferences(options = {}) {
|
|
30922
|
-
const root2 = options.root ??
|
|
31479
|
+
const root2 = options.root ?? process5.cwd();
|
|
30923
31480
|
const canonicalDir = join20(root2, CANONICAL_REFS_DIR);
|
|
30924
31481
|
if (!existsSync24(canonicalDir)) {
|
|
30925
31482
|
throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
|
|
30926
31483
|
}
|
|
30927
|
-
const result = {
|
|
30928
|
-
written: [],
|
|
30929
|
-
rewritten: [],
|
|
30930
|
-
removed: [],
|
|
30931
|
-
skipped: []
|
|
30932
|
-
};
|
|
31484
|
+
const result = { written: [], rewritten: [], removed: [], skipped: [] };
|
|
30933
31485
|
const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
|
|
30934
|
-
for (const plan of plans)
|
|
30935
|
-
|
|
30936
|
-
for (const refPath of plan.refPaths) {
|
|
30937
|
-
const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
|
|
30938
|
-
const canonicalPath = join20(root2, sourceRel);
|
|
30939
|
-
if (!existsSync24(canonicalPath)) {
|
|
30940
|
-
throw new Error(`canonical reference missing: ${sourceRel}`);
|
|
30941
|
-
}
|
|
30942
|
-
const targetRel = generatedRefPath(plan.skill, refPath);
|
|
30943
|
-
const targetPath = join20(root2, targetRel);
|
|
30944
|
-
const canonicalContent = readFileSync19(canonicalPath, "utf8");
|
|
30945
|
-
const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
|
|
30946
|
-
if (!options.dryRun) {
|
|
30947
|
-
mkdirSync3(dirname12(targetPath), { recursive: true });
|
|
30948
|
-
}
|
|
30949
|
-
const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
|
|
30950
|
-
if (existing !== nextContent) {
|
|
30951
|
-
if (!options.dryRun)
|
|
30952
|
-
writeFileSync4(targetPath, nextContent, "utf8");
|
|
30953
|
-
result.written.push(targetRel);
|
|
30954
|
-
} else {
|
|
30955
|
-
result.skipped.push(targetRel);
|
|
30956
|
-
}
|
|
30957
|
-
}
|
|
30958
|
-
if (options.rewriteLinks !== false) {
|
|
30959
|
-
for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
|
|
30960
|
-
const filePath = join20(root2, relFile);
|
|
30961
|
-
const content3 = readFileSync19(filePath, "utf8");
|
|
30962
|
-
const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
|
|
30963
|
-
if (next !== content3) {
|
|
30964
|
-
if (!options.dryRun)
|
|
30965
|
-
writeFileSync4(filePath, next, "utf8");
|
|
30966
|
-
result.rewritten.push(relFile);
|
|
30967
|
-
}
|
|
30968
|
-
}
|
|
30969
|
-
}
|
|
30970
|
-
for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
|
|
30971
|
-
const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
|
|
30972
|
-
if (!plan.refPaths.has(refPath)) {
|
|
30973
|
-
const fullPath = join20(root2, generatedRel);
|
|
30974
|
-
if (!options.dryRun)
|
|
30975
|
-
unlinkSync(fullPath);
|
|
30976
|
-
result.removed.push(generatedRel);
|
|
30977
|
-
}
|
|
30978
|
-
}
|
|
30979
|
-
}
|
|
31486
|
+
for (const plan of plans)
|
|
31487
|
+
syncPlan({ root: root2, plan, options, result });
|
|
30980
31488
|
return result;
|
|
30981
31489
|
}
|
|
30982
31490
|
|
|
@@ -30985,7 +31493,7 @@ function runReferencesSync(options = {}) {
|
|
|
30985
31493
|
return syncReferences(options);
|
|
30986
31494
|
}
|
|
30987
31495
|
function runReferencesCheck(options = {}) {
|
|
30988
|
-
const root2 = options.root ??
|
|
31496
|
+
const root2 = options.root ?? process6.cwd();
|
|
30989
31497
|
let ownership;
|
|
30990
31498
|
try {
|
|
30991
31499
|
ownership = loadConfig(root2).skillOwnership;
|
|
@@ -30999,22 +31507,17 @@ function runReferencesCheck(options = {}) {
|
|
|
30999
31507
|
label: "References check"
|
|
31000
31508
|
});
|
|
31001
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
|
+
}
|
|
31002
31517
|
function printSyncResult(result) {
|
|
31003
|
-
|
|
31004
|
-
|
|
31005
|
-
|
|
31006
|
-
console.log(` + ${file}`);
|
|
31007
|
-
}
|
|
31008
|
-
if (result.rewritten.length > 0) {
|
|
31009
|
-
console.log(`references sync: rewrote links in ${result.rewritten.length} file(s)`);
|
|
31010
|
-
for (const file of result.rewritten)
|
|
31011
|
-
console.log(` ~ ${file}`);
|
|
31012
|
-
}
|
|
31013
|
-
if (result.removed.length > 0) {
|
|
31014
|
-
console.log(`references sync: removed ${result.removed.length} stale file(s)`);
|
|
31015
|
-
for (const file of result.removed)
|
|
31016
|
-
console.log(` - ${file}`);
|
|
31017
|
-
}
|
|
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, "-");
|
|
31018
31521
|
if (result.written.length === 0 && result.rewritten.length === 0 && result.removed.length === 0) {
|
|
31019
31522
|
console.log(`references sync: up to date (${result.skipped.length} file(s) checked)`);
|
|
31020
31523
|
}
|
|
@@ -31048,7 +31551,7 @@ function parseRegistryRows(content3) {
|
|
|
31048
31551
|
for (const line of content3.split(`
|
|
31049
31552
|
`)) {
|
|
31050
31553
|
const match = REGISTRY_TABLE_ROW_RE2.exec(line);
|
|
31051
|
-
if (!match?.[1]
|
|
31554
|
+
if (!(match?.[1] && match[2]))
|
|
31052
31555
|
continue;
|
|
31053
31556
|
rows.push({ topic: match[1].trim(), link: match[2].trim(), line });
|
|
31054
31557
|
}
|
|
@@ -31079,7 +31582,8 @@ ${REGISTRY_TABLE_HEADER}
|
|
|
31079
31582
|
|
|
31080
31583
|
`;
|
|
31081
31584
|
}
|
|
31082
|
-
function upsertRow(
|
|
31585
|
+
function upsertRow(opts) {
|
|
31586
|
+
const { content: content3, topic, link: link2, section, root: root2 } = opts;
|
|
31083
31587
|
const rows = parseRegistryRows(content3);
|
|
31084
31588
|
const targetPath = link2;
|
|
31085
31589
|
const existingByLink = rows.find((row) => row.link === targetPath);
|
|
@@ -31119,13 +31623,19 @@ ${newLine}
|
|
|
31119
31623
|
`;
|
|
31120
31624
|
return { content: appended, action: "added" };
|
|
31121
31625
|
}
|
|
31122
|
-
function
|
|
31123
|
-
|
|
31124
|
-
|
|
31125
|
-
|
|
31126
|
-
|
|
31127
|
-
|
|
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}`);
|
|
31128
31635
|
}
|
|
31636
|
+
}
|
|
31637
|
+
function resolveRegistrationTopic(input) {
|
|
31638
|
+
const { options, root: root2, relPath: relPath2, absPath } = input;
|
|
31129
31639
|
const content3 = readFileSync20(absPath, "utf8");
|
|
31130
31640
|
let topic = options.topic ?? extractTopic(content3);
|
|
31131
31641
|
if (!topic) {
|
|
@@ -31133,13 +31643,36 @@ function registerPath(options) {
|
|
|
31133
31643
|
}
|
|
31134
31644
|
const registryLink = toRegistryLink(root2, absPath);
|
|
31135
31645
|
topic = ensureCustomizeTopic(topic, registryLink);
|
|
31136
|
-
|
|
31646
|
+
return { topic, registryLink, section: inferSection(registryLink) };
|
|
31647
|
+
}
|
|
31648
|
+
function loadRegistryContent(root2) {
|
|
31137
31649
|
const registryAbs = join21(root2, REGISTRY_REL_PATH);
|
|
31138
|
-
|
|
31139
|
-
if (!existsSync25(registryAbs) && !existsSync25(join21(root2, ".skeleton/config.yaml"))) {
|
|
31650
|
+
if (!(existsSync25(registryAbs) || existsSync25(join21(root2, ".skeleton/config.yaml")))) {
|
|
31140
31651
|
throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
|
|
31141
31652
|
}
|
|
31142
|
-
|
|
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
|
+
});
|
|
31143
31676
|
registryContent = updated;
|
|
31144
31677
|
const result = {
|
|
31145
31678
|
topic,
|
|
@@ -31148,6 +31681,7 @@ function registerPath(options) {
|
|
|
31148
31681
|
action,
|
|
31149
31682
|
warnOutsideScan: isOutsideScan(root2, relPath2)
|
|
31150
31683
|
};
|
|
31684
|
+
const registryAbs = join21(root2, REGISTRY_REL_PATH);
|
|
31151
31685
|
if (!options.dryRun && action !== "noop") {
|
|
31152
31686
|
const dir = dirname13(registryAbs);
|
|
31153
31687
|
if (!existsSync25(dir)) {
|
|
@@ -31158,15 +31692,7 @@ function registerPath(options) {
|
|
|
31158
31692
|
if (result.warnOutsideScan) {
|
|
31159
31693
|
console.error(`warning: ${relPath2} is outside scan.include — register succeeded but audit will not scan it`);
|
|
31160
31694
|
}
|
|
31161
|
-
|
|
31162
|
-
console.log(JSON.stringify(result, null, 2));
|
|
31163
|
-
} else if (options.dryRun) {
|
|
31164
|
-
console.log(`dry-run: would ${action} registry row for ${relPath2} → ${topic}`);
|
|
31165
|
-
} else if (action === "noop") {
|
|
31166
|
-
console.log(`register: ${relPath2} already registered (${topic})`);
|
|
31167
|
-
} else {
|
|
31168
|
-
console.log(`register: ${action} ${relPath2} → ${topic}`);
|
|
31169
|
-
}
|
|
31695
|
+
printRegisterResult(options, result, relPath2);
|
|
31170
31696
|
return result;
|
|
31171
31697
|
}
|
|
31172
31698
|
|
|
@@ -31212,41 +31738,49 @@ function isSkeletonYamlCandidate(normalized, ext) {
|
|
|
31212
31738
|
return false;
|
|
31213
31739
|
return true;
|
|
31214
31740
|
}
|
|
31215
|
-
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) {
|
|
31216
31759
|
const normalized = normalizeRelPath(relPath2);
|
|
31217
31760
|
const ext = extname2(normalized).toLowerCase();
|
|
31218
31761
|
const name = basename5(normalized);
|
|
31219
|
-
if (SKIP_EXTENSIONS.has(ext))
|
|
31220
|
-
return "skip";
|
|
31221
|
-
if (COMMAND_CONFIG_NAMES.has(name))
|
|
31762
|
+
if (SKIP_EXTENSIONS.has(ext) || COMMAND_CONFIG_NAMES.has(name))
|
|
31222
31763
|
return "skip";
|
|
31223
31764
|
if (isSkeletonYamlCandidate(normalized, ext)) {
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
|
|
31231
|
-
return
|
|
31232
|
-
}
|
|
31233
|
-
if (DOC_EXTENSIONS.has(ext)) {
|
|
31234
|
-
const config = loadConfig(root2);
|
|
31235
|
-
if (isInScanPerimeter(normalized, config, root2, skillIndex))
|
|
31236
|
-
return "docs";
|
|
31237
|
-
return "skip";
|
|
31238
|
-
}
|
|
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;
|
|
31239
31773
|
if (SHELL_EXTENSIONS.has(ext))
|
|
31240
31774
|
return "shell";
|
|
31241
31775
|
if (ext === ".json")
|
|
31242
31776
|
return "json";
|
|
31243
31777
|
return "skip";
|
|
31244
31778
|
}
|
|
31245
|
-
function isInScanPerimeter(relPath2,
|
|
31246
|
-
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)));
|
|
31247
31781
|
if (scanned.has(relPath2))
|
|
31248
31782
|
return true;
|
|
31249
|
-
return config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
|
|
31783
|
+
return ctx.config.scan.include.some((pattern) => matchesGlobScope(relPath2, pattern));
|
|
31250
31784
|
}
|
|
31251
31785
|
function parseJsonContent(content3) {
|
|
31252
31786
|
try {
|
|
@@ -31298,27 +31832,30 @@ function resolvePaths(options) {
|
|
|
31298
31832
|
root: options.root
|
|
31299
31833
|
});
|
|
31300
31834
|
}
|
|
31301
|
-
function
|
|
31302
|
-
let pm2 = null;
|
|
31835
|
+
function packageManagerFromPackageJson(root2) {
|
|
31303
31836
|
const pkgPath = join22(root2, "package.json");
|
|
31304
|
-
if (existsSync26(pkgPath))
|
|
31305
|
-
|
|
31306
|
-
|
|
31307
|
-
|
|
31308
|
-
|
|
31309
|
-
|
|
31310
|
-
|
|
31311
|
-
}
|
|
31312
|
-
|
|
31313
|
-
|
|
31314
|
-
|
|
31315
|
-
|
|
31316
|
-
|
|
31317
|
-
|
|
31318
|
-
|
|
31319
|
-
|
|
31320
|
-
|
|
31321
|
-
|
|
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) {
|
|
31322
31859
|
switch (pm2) {
|
|
31323
31860
|
case "bun":
|
|
31324
31861
|
return " Run: bun test && bun run typecheck && bun run build";
|
|
@@ -31328,126 +31865,113 @@ function codeValidationHint(root2) {
|
|
|
31328
31865
|
return " Run: pnpm test && pnpm run typecheck";
|
|
31329
31866
|
case "yarn":
|
|
31330
31867
|
return " Run: yarn test && yarn typecheck";
|
|
31331
|
-
default:
|
|
31332
|
-
return " Run your local code validation gates (test + typecheck + build).";
|
|
31333
31868
|
}
|
|
31334
31869
|
}
|
|
31335
|
-
|
|
31336
|
-
const
|
|
31337
|
-
|
|
31338
|
-
|
|
31339
|
-
|
|
31340
|
-
|
|
31341
|
-
|
|
31342
|
-
|
|
31343
|
-
const
|
|
31344
|
-
|
|
31345
|
-
|
|
31346
|
-
|
|
31347
|
-
|
|
31348
|
-
console.error(`validate changed:
|
|
31349
|
-
return
|
|
31350
|
-
}
|
|
31351
|
-
const buckets = {
|
|
31352
|
-
docs: [],
|
|
31353
|
-
skills: [],
|
|
31354
|
-
shell: [],
|
|
31355
|
-
json: [],
|
|
31356
|
-
policy: []
|
|
31357
|
-
};
|
|
31358
|
-
let missing = 0;
|
|
31359
|
-
let skipped = 0;
|
|
31360
|
-
let foreignSkipped = 0;
|
|
31361
|
-
const orphans = [];
|
|
31362
|
-
for (const relPath2 of relPaths) {
|
|
31363
|
-
const normalized = normalizeRelPath(relPath2);
|
|
31364
|
-
const abs = join22(root2, normalized);
|
|
31365
|
-
if (!existsSync26(abs)) {
|
|
31366
|
-
missing++;
|
|
31367
|
-
console.error(`validate changed: path not found: ${relPath2}`);
|
|
31368
|
-
continue;
|
|
31369
|
-
}
|
|
31370
|
-
const ext = extname2(normalized).toLowerCase();
|
|
31371
|
-
if (isSkeletonYamlCandidate(normalized, ext) && !wiredPolicies.has(normalized)) {
|
|
31372
|
-
orphans.push(normalized);
|
|
31373
|
-
continue;
|
|
31374
|
-
}
|
|
31375
|
-
const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
|
|
31376
|
-
if (bucket === "skip") {
|
|
31377
|
-
skipped++;
|
|
31378
|
-
continue;
|
|
31379
|
-
}
|
|
31380
|
-
if (bucket === "foreign-skill") {
|
|
31381
|
-
foreignSkipped++;
|
|
31382
|
-
console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
|
|
31383
|
-
continue;
|
|
31384
|
-
}
|
|
31385
|
-
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;
|
|
31386
31885
|
}
|
|
31387
|
-
|
|
31388
|
-
|
|
31389
|
-
|
|
31390
|
-
|
|
31391
|
-
}
|
|
31392
|
-
return 1;
|
|
31886
|
+
const ext = extname2(normalized).toLowerCase();
|
|
31887
|
+
if (isSkeletonYamlCandidate(normalized, ext) && !ctx.wiredPolicies.has(normalized)) {
|
|
31888
|
+
state.orphans.push(normalized);
|
|
31889
|
+
return;
|
|
31393
31890
|
}
|
|
31394
|
-
const
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
return 1;
|
|
31891
|
+
const bucket = bucketFor(normalized, bucketCtx);
|
|
31892
|
+
if (bucket === "skip") {
|
|
31893
|
+
state.skipped++;
|
|
31894
|
+
return;
|
|
31399
31895
|
}
|
|
31400
|
-
if (
|
|
31401
|
-
|
|
31402
|
-
|
|
31403
|
-
|
|
31404
|
-
json: false,
|
|
31405
|
-
paths: [],
|
|
31406
|
-
only: null,
|
|
31407
|
-
root: root2,
|
|
31408
|
-
globalOnly: true
|
|
31409
|
-
});
|
|
31410
|
-
if (globalExit !== 0)
|
|
31411
|
-
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;
|
|
31412
31900
|
}
|
|
31413
|
-
|
|
31414
|
-
|
|
31415
|
-
|
|
31416
|
-
|
|
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 });
|
|
31417
31918
|
}
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
31421
|
-
|
|
31422
|
-
|
|
31423
|
-
|
|
31424
|
-
only: null,
|
|
31425
|
-
root: root2,
|
|
31426
|
-
pathScopedOnly: true
|
|
31427
|
-
});
|
|
31428
|
-
if (docExit !== 0)
|
|
31429
|
-
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.");
|
|
31430
31925
|
}
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
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).
|
|
31434
31959
|
` + ` Run: skeleton audit skills
|
|
31435
31960
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31436
|
-
|
|
31437
|
-
} else {
|
|
31438
|
-
const skillExit = await runAudit({
|
|
31439
|
-
suite: "skills",
|
|
31440
|
-
strict: false,
|
|
31441
|
-
json: false,
|
|
31442
|
-
paths: buckets.skills,
|
|
31443
|
-
only: null,
|
|
31444
|
-
root: root2,
|
|
31445
|
-
pathScopedOnly: true
|
|
31446
|
-
});
|
|
31447
|
-
if (skillExit !== 0)
|
|
31448
|
-
exitCode = 1;
|
|
31449
|
-
}
|
|
31961
|
+
return 1;
|
|
31450
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;
|
|
31451
31975
|
for (const relPath2 of buckets.shell) {
|
|
31452
31976
|
if (validateShell(relPath2, root2) !== 0)
|
|
31453
31977
|
exitCode = 1;
|
|
@@ -31460,49 +31984,124 @@ async function runValidateChanged(options = {}) {
|
|
|
31460
31984
|
if (validatePolicy(relPath2, root2) !== 0)
|
|
31461
31985
|
exitCode = 1;
|
|
31462
31986
|
}
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31488
|
-
|
|
31489
|
-
|
|
31490
|
-
|
|
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).
|
|
31491
32019
|
` + ` Run: skeleton audit docs
|
|
31492
32020
|
` + ` And: skeleton audit skills
|
|
31493
32021
|
` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
|
|
31494
|
-
|
|
31495
|
-
}
|
|
32022
|
+
return 1;
|
|
31496
32023
|
}
|
|
31497
|
-
|
|
31498
|
-
|
|
31499
|
-
|
|
31500
|
-
|
|
31501
|
-
|
|
31502
|
-
|
|
31503
|
-
|
|
31504
|
-
|
|
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;
|
|
31505
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);
|
|
31506
32105
|
return exitCode;
|
|
31507
32106
|
}
|
|
31508
32107
|
|
|
@@ -31534,123 +32133,153 @@ function parseRegisterArgs(argv) {
|
|
|
31534
32133
|
json = true;
|
|
31535
32134
|
else if (arg.startsWith("--topic="))
|
|
31536
32135
|
topic = arg.slice("--topic=".length);
|
|
31537
|
-
else if (!arg.startsWith("-")
|
|
32136
|
+
else if (!(arg.startsWith("-") || path2))
|
|
31538
32137
|
path2 = arg;
|
|
31539
32138
|
}
|
|
31540
32139
|
return { path: path2, topic, dryRun, json };
|
|
31541
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
|
+
}
|
|
31542
32266
|
async function main() {
|
|
31543
|
-
const argv =
|
|
32267
|
+
const argv = process7.argv.slice(2);
|
|
31544
32268
|
const command = argv[0];
|
|
31545
32269
|
if (!command || command === "--help" || command === "-h") {
|
|
31546
32270
|
usage();
|
|
31547
|
-
|
|
32271
|
+
process7.exit(command ? 0 : 1);
|
|
31548
32272
|
}
|
|
31549
32273
|
try {
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
if (sub !== "docs" && sub !== "self" && sub !== "skills") {
|
|
31553
|
-
usage();
|
|
31554
|
-
process.exit(1);
|
|
31555
|
-
}
|
|
31556
|
-
const options = parseAuditArgs(argv.slice(2));
|
|
31557
|
-
options.suite = sub;
|
|
31558
|
-
process.exit(await runAudit(options));
|
|
31559
|
-
}
|
|
31560
|
-
if (command === "build-plugin") {
|
|
31561
|
-
const { entry, check } = parseBuildPluginArgs(argv.slice(1));
|
|
31562
|
-
const root2 = findRepoRoot();
|
|
31563
|
-
const result = await runBuildPlugin({ root: root2, entry, check });
|
|
31564
|
-
if (check) {
|
|
31565
|
-
console.log(result.checked.length === 0 ? "build-plugin --check: no plugins configured." : `build-plugin --check: ${result.checked.length} plugin(s) up to date.`);
|
|
31566
|
-
} else {
|
|
31567
|
-
console.log(result.built.length === 0 ? "build-plugin: no plugins configured." : `build-plugin: built ${result.built.length} plugin(s).`);
|
|
31568
|
-
}
|
|
31569
|
-
process.exit(0);
|
|
31570
|
-
}
|
|
31571
|
-
if (command === "validate" && argv[1] === "changed") {
|
|
31572
|
-
const rest = argv.slice(2);
|
|
31573
|
-
const paths = [];
|
|
31574
|
-
let staged = false;
|
|
31575
|
-
let base;
|
|
31576
|
-
for (let i = 0;i < rest.length; i++) {
|
|
31577
|
-
const arg = rest[i];
|
|
31578
|
-
if (arg === "--staged")
|
|
31579
|
-
staged = true;
|
|
31580
|
-
else if (arg === "--base")
|
|
31581
|
-
base = rest[++i];
|
|
31582
|
-
else if (arg?.startsWith("--base="))
|
|
31583
|
-
base = arg.slice("--base=".length);
|
|
31584
|
-
else if (arg && !arg.startsWith("-"))
|
|
31585
|
-
paths.push(arg);
|
|
31586
|
-
}
|
|
31587
|
-
process.exit(await runValidateChanged({ paths, staged, base }));
|
|
31588
|
-
}
|
|
31589
|
-
if (command === "register") {
|
|
31590
|
-
const opts = parseRegisterArgs(argv.slice(1));
|
|
31591
|
-
if (!opts.path) {
|
|
31592
|
-
console.error("register: path required");
|
|
31593
|
-
process.exit(1);
|
|
31594
|
-
}
|
|
31595
|
-
registerPath({
|
|
31596
|
-
path: opts.path,
|
|
31597
|
-
topic: opts.topic,
|
|
31598
|
-
dryRun: opts.dryRun,
|
|
31599
|
-
json: opts.json
|
|
31600
|
-
});
|
|
31601
|
-
process.exit(0);
|
|
31602
|
-
}
|
|
31603
|
-
if (command === "customize" && argv[1] === "resolve") {
|
|
31604
|
-
const slug2 = argv[2];
|
|
31605
|
-
const json = argv.includes("--json");
|
|
31606
|
-
if (!slug2) {
|
|
31607
|
-
console.error("customize resolve: slug required");
|
|
31608
|
-
process.exit(1);
|
|
31609
|
-
}
|
|
31610
|
-
const result = resolveCustomizeFromRoot(slug2);
|
|
31611
|
-
if (json) {
|
|
31612
|
-
console.log(JSON.stringify(result, null, 2));
|
|
31613
|
-
} else if (result.content) {
|
|
31614
|
-
process.stdout.write(result.content);
|
|
31615
|
-
}
|
|
31616
|
-
process.exit(0);
|
|
31617
|
-
}
|
|
31618
|
-
if (command === "hook") {
|
|
31619
|
-
if (argv[1] !== "customize") {
|
|
31620
|
-
usage();
|
|
31621
|
-
process.exit(1);
|
|
31622
|
-
}
|
|
31623
|
-
process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
|
|
31624
|
-
process.exit(0);
|
|
31625
|
-
}
|
|
31626
|
-
if (command === "init") {
|
|
31627
|
-
const parsed = parseInitArgs(argv.slice(1));
|
|
31628
|
-
runInit(parsed);
|
|
31629
|
-
process.exit(0);
|
|
31630
|
-
}
|
|
31631
|
-
if (command === "references") {
|
|
31632
|
-
const sub = argv[1];
|
|
31633
|
-
if (sub === "sync") {
|
|
31634
|
-
const dryRun = argv.includes("--dry-run");
|
|
31635
|
-
const rewriteLinks = !argv.includes("--no-rewrite-links");
|
|
31636
|
-
const result = runReferencesSync({ dryRun, rewriteLinks });
|
|
31637
|
-
printSyncResult(result);
|
|
31638
|
-
process.exit(0);
|
|
31639
|
-
}
|
|
31640
|
-
if (sub === "check") {
|
|
31641
|
-
process.exit(runReferencesCheck({
|
|
31642
|
-
json: argv.includes("--json"),
|
|
31643
|
-
strict: argv.includes("--strict")
|
|
31644
|
-
}));
|
|
31645
|
-
}
|
|
32274
|
+
const exitCode = await dispatchCommand(argv);
|
|
32275
|
+
if (exitCode === null) {
|
|
31646
32276
|
usage();
|
|
31647
|
-
|
|
32277
|
+
process7.exit(1);
|
|
31648
32278
|
}
|
|
31649
|
-
|
|
31650
|
-
process.exit(1);
|
|
32279
|
+
process7.exit(exitCode);
|
|
31651
32280
|
} catch (error) {
|
|
31652
32281
|
console.error(String(error));
|
|
31653
|
-
|
|
32282
|
+
process7.exit(1);
|
|
31654
32283
|
}
|
|
31655
32284
|
}
|
|
31656
32285
|
main();
|