claude-crap 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +74 -7
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/dart-analyzer.d.ts +41 -0
- package/dist/adapters/dart-analyzer.d.ts.map +1 -0
- package/dist/adapters/dart-analyzer.js +120 -0
- package/dist/adapters/dart-analyzer.js.map +1 -0
- package/dist/adapters/dotnet-format.d.ts +35 -0
- package/dist/adapters/dotnet-format.d.ts.map +1 -0
- package/dist/adapters/dotnet-format.js +96 -0
- package/dist/adapters/dotnet-format.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +4 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +51 -28
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +7 -12
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +89 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/workspace-walker.d.ts +4 -1
- package/dist/metrics/workspace-walker.d.ts.map +1 -1
- package/dist/metrics/workspace-walker.js +12 -28
- package/dist/metrics/workspace-walker.js.map +1 -1
- package/dist/monorepo/project-map.d.ts +112 -0
- package/dist/monorepo/project-map.d.ts.map +1 -0
- package/dist/monorepo/project-map.js +384 -0
- package/dist/monorepo/project-map.js.map +1 -0
- package/dist/scanner/auto-scan.d.ts +1 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -5
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +15 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +2 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -1
- package/dist/scanner/complexity-scanner.js +11 -26
- package/dist/scanner/complexity-scanner.js.map +1 -1
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +110 -10
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts +4 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +25 -3
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +16 -1
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +16 -1
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/dist/shared/exclusions.d.ts +53 -0
- package/dist/shared/exclusions.d.ts.map +1 -0
- package/dist/shared/exclusions.js +126 -0
- package/dist/shared/exclusions.js.map +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +762 -144
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +15 -2
- package/plugin/package.json +2 -1
- package/scripts/bundle-plugin.mjs +2 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dart-analyzer.ts +161 -0
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +8 -0
- package/src/crap-config.ts +78 -18
- package/src/dashboard/file-detail.ts +0 -2
- package/src/dashboard/server.ts +9 -10
- package/src/index.ts +103 -5
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/auto-scan.ts +17 -6
- package/src/scanner/bootstrap.ts +18 -1
- package/src/scanner/complexity-scanner.ts +15 -26
- package/src/scanner/detector.ts +119 -10
- package/src/scanner/runner.ts +25 -2
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/shared/exclusions.ts +156 -0
- package/src/tests/adapters/dispatch.test.ts +2 -2
- package/src/tests/auto-scan.test.ts +2 -2
- package/src/tests/boot-monorepo.test.ts +804 -0
- package/src/tests/boot-scanner-detection.test.ts +692 -0
- package/src/tests/boot-single-project.test.ts +780 -0
- package/src/tests/exclusions.test.ts +117 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/project-map.test.ts +302 -0
- package/src/tests/scanner-detector.test.ts +31 -11
|
@@ -2977,7 +2977,7 @@ var require_compile = __commonJS({
|
|
|
2977
2977
|
const schOrFunc = root.refs[ref];
|
|
2978
2978
|
if (schOrFunc)
|
|
2979
2979
|
return schOrFunc;
|
|
2980
|
-
let _sch =
|
|
2980
|
+
let _sch = resolve8.call(this, root, ref);
|
|
2981
2981
|
if (_sch === void 0) {
|
|
2982
2982
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2983
2983
|
const { schemaId } = this.opts;
|
|
@@ -3004,7 +3004,7 @@ var require_compile = __commonJS({
|
|
|
3004
3004
|
function sameSchemaEnv(s1, s2) {
|
|
3005
3005
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3006
3006
|
}
|
|
3007
|
-
function
|
|
3007
|
+
function resolve8(root, ref) {
|
|
3008
3008
|
let sch;
|
|
3009
3009
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3010
3010
|
ref = sch;
|
|
@@ -3579,55 +3579,55 @@ var require_fast_uri = __commonJS({
|
|
|
3579
3579
|
}
|
|
3580
3580
|
return uri;
|
|
3581
3581
|
}
|
|
3582
|
-
function
|
|
3582
|
+
function resolve8(baseURI, relativeURI, options) {
|
|
3583
3583
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3584
3584
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3585
3585
|
schemelessOptions.skipEscape = true;
|
|
3586
3586
|
return serialize(resolved, schemelessOptions);
|
|
3587
3587
|
}
|
|
3588
|
-
function resolveComponent(base,
|
|
3588
|
+
function resolveComponent(base, relative4, options, skipNormalization) {
|
|
3589
3589
|
const target = {};
|
|
3590
3590
|
if (!skipNormalization) {
|
|
3591
3591
|
base = parse(serialize(base, options), options);
|
|
3592
|
-
|
|
3592
|
+
relative4 = parse(serialize(relative4, options), options);
|
|
3593
3593
|
}
|
|
3594
3594
|
options = options || {};
|
|
3595
|
-
if (!options.tolerant &&
|
|
3596
|
-
target.scheme =
|
|
3597
|
-
target.userinfo =
|
|
3598
|
-
target.host =
|
|
3599
|
-
target.port =
|
|
3600
|
-
target.path = removeDotSegments(
|
|
3601
|
-
target.query =
|
|
3595
|
+
if (!options.tolerant && relative4.scheme) {
|
|
3596
|
+
target.scheme = relative4.scheme;
|
|
3597
|
+
target.userinfo = relative4.userinfo;
|
|
3598
|
+
target.host = relative4.host;
|
|
3599
|
+
target.port = relative4.port;
|
|
3600
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3601
|
+
target.query = relative4.query;
|
|
3602
3602
|
} else {
|
|
3603
|
-
if (
|
|
3604
|
-
target.userinfo =
|
|
3605
|
-
target.host =
|
|
3606
|
-
target.port =
|
|
3607
|
-
target.path = removeDotSegments(
|
|
3608
|
-
target.query =
|
|
3603
|
+
if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
|
|
3604
|
+
target.userinfo = relative4.userinfo;
|
|
3605
|
+
target.host = relative4.host;
|
|
3606
|
+
target.port = relative4.port;
|
|
3607
|
+
target.path = removeDotSegments(relative4.path || "");
|
|
3608
|
+
target.query = relative4.query;
|
|
3609
3609
|
} else {
|
|
3610
|
-
if (!
|
|
3610
|
+
if (!relative4.path) {
|
|
3611
3611
|
target.path = base.path;
|
|
3612
|
-
if (
|
|
3613
|
-
target.query =
|
|
3612
|
+
if (relative4.query !== void 0) {
|
|
3613
|
+
target.query = relative4.query;
|
|
3614
3614
|
} else {
|
|
3615
3615
|
target.query = base.query;
|
|
3616
3616
|
}
|
|
3617
3617
|
} else {
|
|
3618
|
-
if (
|
|
3619
|
-
target.path = removeDotSegments(
|
|
3618
|
+
if (relative4.path[0] === "/") {
|
|
3619
|
+
target.path = removeDotSegments(relative4.path);
|
|
3620
3620
|
} else {
|
|
3621
3621
|
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
|
|
3622
|
-
target.path = "/" +
|
|
3622
|
+
target.path = "/" + relative4.path;
|
|
3623
3623
|
} else if (!base.path) {
|
|
3624
|
-
target.path =
|
|
3624
|
+
target.path = relative4.path;
|
|
3625
3625
|
} else {
|
|
3626
|
-
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
3626
|
+
target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
|
|
3627
3627
|
}
|
|
3628
3628
|
target.path = removeDotSegments(target.path);
|
|
3629
3629
|
}
|
|
3630
|
-
target.query =
|
|
3630
|
+
target.query = relative4.query;
|
|
3631
3631
|
}
|
|
3632
3632
|
target.userinfo = base.userinfo;
|
|
3633
3633
|
target.host = base.host;
|
|
@@ -3635,7 +3635,7 @@ var require_fast_uri = __commonJS({
|
|
|
3635
3635
|
}
|
|
3636
3636
|
target.scheme = base.scheme;
|
|
3637
3637
|
}
|
|
3638
|
-
target.fragment =
|
|
3638
|
+
target.fragment = relative4.fragment;
|
|
3639
3639
|
return target;
|
|
3640
3640
|
}
|
|
3641
3641
|
function equal(uriA, uriB, options) {
|
|
@@ -3806,7 +3806,7 @@ var require_fast_uri = __commonJS({
|
|
|
3806
3806
|
var fastUri = {
|
|
3807
3807
|
SCHEMES,
|
|
3808
3808
|
normalize,
|
|
3809
|
-
resolve:
|
|
3809
|
+
resolve: resolve8,
|
|
3810
3810
|
resolveComponent,
|
|
3811
3811
|
equal,
|
|
3812
3812
|
serialize,
|
|
@@ -6853,6 +6853,145 @@ function buildSarifResult3(opts) {
|
|
|
6853
6853
|
};
|
|
6854
6854
|
}
|
|
6855
6855
|
|
|
6856
|
+
// src/adapters/dart-analyzer.ts
|
|
6857
|
+
function mapSeverity3(dartSeverity) {
|
|
6858
|
+
switch (dartSeverity.toUpperCase()) {
|
|
6859
|
+
case "ERROR":
|
|
6860
|
+
return "error";
|
|
6861
|
+
case "WARNING":
|
|
6862
|
+
return "warning";
|
|
6863
|
+
case "INFO":
|
|
6864
|
+
return "note";
|
|
6865
|
+
default:
|
|
6866
|
+
return "warning";
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
var EFFORT_BY_SEVERITY = {
|
|
6870
|
+
error: 30,
|
|
6871
|
+
warning: 15,
|
|
6872
|
+
note: 5,
|
|
6873
|
+
none: 0
|
|
6874
|
+
};
|
|
6875
|
+
function adaptDartAnalyzer(rawOutput) {
|
|
6876
|
+
let parsed;
|
|
6877
|
+
if (typeof rawOutput === "string") {
|
|
6878
|
+
try {
|
|
6879
|
+
parsed = JSON.parse(rawOutput);
|
|
6880
|
+
} catch {
|
|
6881
|
+
throw new Error("[dart-analyzer adapter] rawOutput is not valid JSON");
|
|
6882
|
+
}
|
|
6883
|
+
} else if (rawOutput && typeof rawOutput === "object" && "diagnostics" in rawOutput) {
|
|
6884
|
+
parsed = rawOutput;
|
|
6885
|
+
} else {
|
|
6886
|
+
throw new Error(
|
|
6887
|
+
"[dart-analyzer adapter] rawOutput must be a JSON string or an object with a 'diagnostics' array"
|
|
6888
|
+
);
|
|
6889
|
+
}
|
|
6890
|
+
if (!Array.isArray(parsed.diagnostics)) {
|
|
6891
|
+
throw new Error("[dart-analyzer adapter] 'diagnostics' must be an array");
|
|
6892
|
+
}
|
|
6893
|
+
const results = [];
|
|
6894
|
+
let totalEffortMinutes = 0;
|
|
6895
|
+
for (const diag of parsed.diagnostics) {
|
|
6896
|
+
const level = mapSeverity3(diag.severity);
|
|
6897
|
+
const effort = EFFORT_BY_SEVERITY[level] ?? estimateEffortMinutes(level);
|
|
6898
|
+
totalEffortMinutes += effort;
|
|
6899
|
+
results.push({
|
|
6900
|
+
ruleId: diag.code,
|
|
6901
|
+
level,
|
|
6902
|
+
message: {
|
|
6903
|
+
text: diag.problemMessage + (diag.correctionMessage ? ` ${diag.correctionMessage}` : "")
|
|
6904
|
+
},
|
|
6905
|
+
locations: [
|
|
6906
|
+
{
|
|
6907
|
+
physicalLocation: {
|
|
6908
|
+
artifactLocation: {
|
|
6909
|
+
uri: diag.location.file
|
|
6910
|
+
},
|
|
6911
|
+
region: {
|
|
6912
|
+
startLine: diag.location.range.start.line,
|
|
6913
|
+
startColumn: diag.location.range.start.column,
|
|
6914
|
+
endLine: diag.location.range.end.line,
|
|
6915
|
+
endColumn: diag.location.range.end.column
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
],
|
|
6920
|
+
properties: {
|
|
6921
|
+
effortMinutes: effort,
|
|
6922
|
+
...diag.documentation ? { helpUri: diag.documentation } : {}
|
|
6923
|
+
}
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
return {
|
|
6927
|
+
document: wrapResultsInSarif("dart_analyze", "1.0.0", results),
|
|
6928
|
+
sourceTool: "dart_analyze",
|
|
6929
|
+
findingCount: parsed.diagnostics.length,
|
|
6930
|
+
totalEffortMinutes
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
// src/adapters/dotnet-format.ts
|
|
6935
|
+
function adaptDotnetFormat(rawOutput) {
|
|
6936
|
+
let parsed;
|
|
6937
|
+
if (typeof rawOutput === "string") {
|
|
6938
|
+
try {
|
|
6939
|
+
parsed = JSON.parse(rawOutput);
|
|
6940
|
+
} catch {
|
|
6941
|
+
throw new Error("[dotnet-format adapter] rawOutput is not valid JSON");
|
|
6942
|
+
}
|
|
6943
|
+
} else if (Array.isArray(rawOutput)) {
|
|
6944
|
+
parsed = rawOutput;
|
|
6945
|
+
} else {
|
|
6946
|
+
throw new Error(
|
|
6947
|
+
"[dotnet-format adapter] rawOutput must be a JSON string or an array of document entries"
|
|
6948
|
+
);
|
|
6949
|
+
}
|
|
6950
|
+
if (!Array.isArray(parsed)) {
|
|
6951
|
+
throw new Error("[dotnet-format adapter] parsed output must be an array");
|
|
6952
|
+
}
|
|
6953
|
+
const EFFORT_MINUTES = 5;
|
|
6954
|
+
const results = [];
|
|
6955
|
+
let findingCount = 0;
|
|
6956
|
+
let totalEffortMinutes = 0;
|
|
6957
|
+
for (const doc of parsed) {
|
|
6958
|
+
if (!Array.isArray(doc.FileChanges)) continue;
|
|
6959
|
+
for (const change of doc.FileChanges) {
|
|
6960
|
+
findingCount++;
|
|
6961
|
+
totalEffortMinutes += EFFORT_MINUTES;
|
|
6962
|
+
results.push({
|
|
6963
|
+
ruleId: change.DiagnosticId,
|
|
6964
|
+
level: "warning",
|
|
6965
|
+
message: {
|
|
6966
|
+
text: change.FormatDescription
|
|
6967
|
+
},
|
|
6968
|
+
locations: [
|
|
6969
|
+
{
|
|
6970
|
+
physicalLocation: {
|
|
6971
|
+
artifactLocation: {
|
|
6972
|
+
uri: doc.FilePath
|
|
6973
|
+
},
|
|
6974
|
+
region: {
|
|
6975
|
+
startLine: change.LineNumber,
|
|
6976
|
+
startColumn: change.CharNumber
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
],
|
|
6981
|
+
properties: {
|
|
6982
|
+
effortMinutes: EFFORT_MINUTES
|
|
6983
|
+
}
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
return {
|
|
6988
|
+
document: wrapResultsInSarif("dotnet_format", "1.0.0", results),
|
|
6989
|
+
sourceTool: "dotnet_format",
|
|
6990
|
+
findingCount,
|
|
6991
|
+
totalEffortMinutes
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6856
6995
|
// src/adapters/index.ts
|
|
6857
6996
|
function adaptScannerOutput(scanner, rawOutput) {
|
|
6858
6997
|
switch (scanner) {
|
|
@@ -6864,6 +7003,10 @@ function adaptScannerOutput(scanner, rawOutput) {
|
|
|
6864
7003
|
return adaptBandit(rawOutput);
|
|
6865
7004
|
case "stryker":
|
|
6866
7005
|
return adaptStryker(rawOutput);
|
|
7006
|
+
case "dart_analyze":
|
|
7007
|
+
return adaptDartAnalyzer(rawOutput);
|
|
7008
|
+
case "dotnet_format":
|
|
7009
|
+
return adaptDotnetFormat(rawOutput);
|
|
6867
7010
|
default: {
|
|
6868
7011
|
const exhaustive = scanner;
|
|
6869
7012
|
throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
|
|
@@ -7250,6 +7393,100 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
7250
7393
|
import Fastify from "fastify";
|
|
7251
7394
|
import fastifyStatic from "@fastify/static";
|
|
7252
7395
|
|
|
7396
|
+
// src/shared/exclusions.ts
|
|
7397
|
+
import picomatch from "picomatch";
|
|
7398
|
+
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
7399
|
+
// Package managers / vendored deps
|
|
7400
|
+
"node_modules",
|
|
7401
|
+
"vendor",
|
|
7402
|
+
// Version control
|
|
7403
|
+
".git",
|
|
7404
|
+
// Build outputs (general)
|
|
7405
|
+
"dist",
|
|
7406
|
+
"build",
|
|
7407
|
+
"bundle",
|
|
7408
|
+
"out",
|
|
7409
|
+
"target",
|
|
7410
|
+
"coverage",
|
|
7411
|
+
// Framework build outputs
|
|
7412
|
+
".next",
|
|
7413
|
+
// Next.js
|
|
7414
|
+
".nuxt",
|
|
7415
|
+
// Nuxt 2
|
|
7416
|
+
".output",
|
|
7417
|
+
// Nuxt 3
|
|
7418
|
+
".vercel",
|
|
7419
|
+
// Vercel
|
|
7420
|
+
".svelte-kit",
|
|
7421
|
+
// SvelteKit
|
|
7422
|
+
".astro",
|
|
7423
|
+
// Astro
|
|
7424
|
+
".angular",
|
|
7425
|
+
// Angular
|
|
7426
|
+
".turbo",
|
|
7427
|
+
// Turborepo
|
|
7428
|
+
".parcel-cache",
|
|
7429
|
+
// Parcel
|
|
7430
|
+
".expo",
|
|
7431
|
+
// Expo / React Native
|
|
7432
|
+
// Language-specific caches
|
|
7433
|
+
".venv",
|
|
7434
|
+
"venv",
|
|
7435
|
+
"__pycache__",
|
|
7436
|
+
".cache",
|
|
7437
|
+
".dart_tool",
|
|
7438
|
+
// Dart / Flutter
|
|
7439
|
+
".gradle",
|
|
7440
|
+
// Gradle
|
|
7441
|
+
// IDE state
|
|
7442
|
+
".idea",
|
|
7443
|
+
// Plugin state
|
|
7444
|
+
".claude-crap",
|
|
7445
|
+
".codesight"
|
|
7446
|
+
]);
|
|
7447
|
+
var DEFAULT_SKIP_PATTERNS = [
|
|
7448
|
+
"*.min.js",
|
|
7449
|
+
"*.min.css",
|
|
7450
|
+
"*.min.mjs",
|
|
7451
|
+
"*.min.cjs",
|
|
7452
|
+
"*.bundle.js",
|
|
7453
|
+
"*.chunk.js"
|
|
7454
|
+
];
|
|
7455
|
+
function createExclusionFilter(userExclusions) {
|
|
7456
|
+
const extraDirs = /* @__PURE__ */ new Set();
|
|
7457
|
+
const fileGlobs = [];
|
|
7458
|
+
for (const pattern of userExclusions ?? []) {
|
|
7459
|
+
if (pattern.endsWith("/")) {
|
|
7460
|
+
extraDirs.add(pattern.slice(0, -1));
|
|
7461
|
+
} else {
|
|
7462
|
+
fileGlobs.push(pattern);
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
const defaultFileMatchers = DEFAULT_SKIP_PATTERNS.map(
|
|
7466
|
+
(p) => picomatch(p, { dot: true })
|
|
7467
|
+
);
|
|
7468
|
+
const userFileMatchers = fileGlobs.map(
|
|
7469
|
+
(p) => picomatch(p, { dot: true })
|
|
7470
|
+
);
|
|
7471
|
+
return {
|
|
7472
|
+
shouldSkipDir(dirName) {
|
|
7473
|
+
if (dirName.startsWith(".") && dirName !== ".claude-plugin") {
|
|
7474
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || true;
|
|
7475
|
+
}
|
|
7476
|
+
return DEFAULT_SKIP_DIRS.has(dirName) || extraDirs.has(dirName);
|
|
7477
|
+
},
|
|
7478
|
+
shouldSkipFile(relativePath, fileName) {
|
|
7479
|
+
for (const matcher of defaultFileMatchers) {
|
|
7480
|
+
if (matcher(fileName)) return true;
|
|
7481
|
+
}
|
|
7482
|
+
for (const matcher of userFileMatchers) {
|
|
7483
|
+
if (matcher(relativePath) || matcher(fileName)) return true;
|
|
7484
|
+
}
|
|
7485
|
+
return false;
|
|
7486
|
+
}
|
|
7487
|
+
};
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7253
7490
|
// src/metrics/tdr.ts
|
|
7254
7491
|
var RATING_ORDER = ["A", "B", "C", "D", "E"];
|
|
7255
7492
|
function ratingToRank(rating) {
|
|
@@ -7524,7 +7761,7 @@ async function startDashboard(options) {
|
|
|
7524
7761
|
root: publicRoot,
|
|
7525
7762
|
prefix: "/"
|
|
7526
7763
|
});
|
|
7527
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
7764
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.4.0" }));
|
|
7528
7765
|
fastify.get("/api/score", async () => {
|
|
7529
7766
|
const stats = await workspaceStatsProvider();
|
|
7530
7767
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
@@ -7535,7 +7772,7 @@ async function startDashboard(options) {
|
|
|
7535
7772
|
if (!options.astEngine) {
|
|
7536
7773
|
return { threshold: config.cyclomaticMax, totalFunctions: 0, violationCount: 0, topFunctions: [] };
|
|
7537
7774
|
}
|
|
7538
|
-
return buildComplexityReport(config, options.astEngine, logger2);
|
|
7775
|
+
return buildComplexityReport(config, options.astEngine, logger2, options.exclude);
|
|
7539
7776
|
});
|
|
7540
7777
|
fastify.get("/api/file-detail", async (request, reply) => {
|
|
7541
7778
|
const { path: filePath } = request.query;
|
|
@@ -7681,24 +7918,9 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
|
|
|
7681
7918
|
removePidFile(pidFilePath);
|
|
7682
7919
|
await new Promise((r) => setTimeout(r, 300));
|
|
7683
7920
|
}
|
|
7684
|
-
|
|
7685
|
-
"node_modules",
|
|
7686
|
-
".git",
|
|
7687
|
-
"dist",
|
|
7688
|
-
"build",
|
|
7689
|
-
"out",
|
|
7690
|
-
"target",
|
|
7691
|
-
".venv",
|
|
7692
|
-
"venv",
|
|
7693
|
-
"__pycache__",
|
|
7694
|
-
".cache",
|
|
7695
|
-
".next",
|
|
7696
|
-
".nuxt",
|
|
7697
|
-
".claude-crap",
|
|
7698
|
-
".codesight"
|
|
7699
|
-
]);
|
|
7700
|
-
async function buildComplexityReport(config, engine, logger2) {
|
|
7921
|
+
async function buildComplexityReport(config, engine, logger2, exclude) {
|
|
7701
7922
|
const threshold = config.cyclomaticMax;
|
|
7923
|
+
const filter = createExclusionFilter(exclude);
|
|
7702
7924
|
const allFunctions = [];
|
|
7703
7925
|
let totalFunctions = 0;
|
|
7704
7926
|
async function walk2(dir) {
|
|
@@ -7709,10 +7931,9 @@ async function buildComplexityReport(config, engine, logger2) {
|
|
|
7709
7931
|
return;
|
|
7710
7932
|
}
|
|
7711
7933
|
for (const entry of entries) {
|
|
7712
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
7713
7934
|
const full = join2(dir, entry.name);
|
|
7714
7935
|
if (entry.isDirectory()) {
|
|
7715
|
-
if (
|
|
7936
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
7716
7937
|
await walk2(full);
|
|
7717
7938
|
continue;
|
|
7718
7939
|
}
|
|
@@ -7791,23 +8012,7 @@ function computeCrap(input, threshold) {
|
|
|
7791
8012
|
|
|
7792
8013
|
// src/metrics/workspace-walker.ts
|
|
7793
8014
|
import { promises as fs4 } from "node:fs";
|
|
7794
|
-
import { join as join3 } from "node:path";
|
|
7795
|
-
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
7796
|
-
"node_modules",
|
|
7797
|
-
".git",
|
|
7798
|
-
"dist",
|
|
7799
|
-
"build",
|
|
7800
|
-
"out",
|
|
7801
|
-
"target",
|
|
7802
|
-
".venv",
|
|
7803
|
-
"venv",
|
|
7804
|
-
"__pycache__",
|
|
7805
|
-
".cache",
|
|
7806
|
-
".next",
|
|
7807
|
-
".nuxt",
|
|
7808
|
-
".claude-crap",
|
|
7809
|
-
".codesight"
|
|
7810
|
-
]);
|
|
8015
|
+
import { join as join3, relative } from "node:path";
|
|
7811
8016
|
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7812
8017
|
".ts",
|
|
7813
8018
|
".tsx",
|
|
@@ -7831,7 +8036,8 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
7831
8036
|
".vue"
|
|
7832
8037
|
]);
|
|
7833
8038
|
var MAX_FILES_WALKED = 2e4;
|
|
7834
|
-
async function estimateWorkspaceLoc(workspaceRoot) {
|
|
8039
|
+
async function estimateWorkspaceLoc(workspaceRoot, options) {
|
|
8040
|
+
const filter = createExclusionFilter(options?.exclude);
|
|
7835
8041
|
let physicalLoc = 0;
|
|
7836
8042
|
let fileCount = 0;
|
|
7837
8043
|
let truncated = false;
|
|
@@ -7845,10 +8051,9 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7845
8051
|
}
|
|
7846
8052
|
for (const entry of entries) {
|
|
7847
8053
|
if (truncated) return;
|
|
7848
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
7849
8054
|
const full = join3(dir, entry.name);
|
|
7850
8055
|
if (entry.isDirectory()) {
|
|
7851
|
-
if (
|
|
8056
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
7852
8057
|
await walk2(full);
|
|
7853
8058
|
continue;
|
|
7854
8059
|
}
|
|
@@ -7858,6 +8063,8 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7858
8063
|
if (dot < 0) continue;
|
|
7859
8064
|
const ext = lower.substring(dot);
|
|
7860
8065
|
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
8066
|
+
const relPath = relative(workspaceRoot, full);
|
|
8067
|
+
if (filter.shouldSkipFile(relPath, entry.name)) continue;
|
|
7861
8068
|
fileCount += 1;
|
|
7862
8069
|
if (fileCount > MAX_FILES_WALKED) {
|
|
7863
8070
|
truncated = true;
|
|
@@ -8227,6 +8434,9 @@ var CrapConfigError = class extends Error {
|
|
|
8227
8434
|
}
|
|
8228
8435
|
};
|
|
8229
8436
|
function loadCrapConfig(options) {
|
|
8437
|
+
const fileResult = readFromFile(options.workspaceRoot);
|
|
8438
|
+
const exclude = fileResult?.exclude ?? [];
|
|
8439
|
+
const projectDirs = fileResult?.projectDirs ?? [];
|
|
8230
8440
|
const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
|
|
8231
8441
|
if (typeof envRaw === "string" && envRaw.trim() !== "") {
|
|
8232
8442
|
const normalized = envRaw.trim().toLowerCase();
|
|
@@ -8235,11 +8445,12 @@ function loadCrapConfig(options) {
|
|
|
8235
8445
|
`[crap-config] CLAUDE_CRAP_STRICTNESS="${envRaw}" is not a valid strictness. Expected one of: ${STRICTNESS_VALUES.join(", ")}.`
|
|
8236
8446
|
);
|
|
8237
8447
|
}
|
|
8238
|
-
return { strictness: normalized, strictnessSource: "env" };
|
|
8448
|
+
return { strictness: normalized, strictnessSource: "env", exclude, projectDirs };
|
|
8449
|
+
}
|
|
8450
|
+
if (fileResult?.strictness) {
|
|
8451
|
+
return { strictness: fileResult.strictness, strictnessSource: "file", exclude, projectDirs };
|
|
8239
8452
|
}
|
|
8240
|
-
|
|
8241
|
-
if (fromFile) return { strictness: fromFile, strictnessSource: "file" };
|
|
8242
|
-
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
|
|
8453
|
+
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude, projectDirs };
|
|
8243
8454
|
}
|
|
8244
8455
|
function readFromFile(workspaceRoot) {
|
|
8245
8456
|
const filePath = join5(workspaceRoot, ".claude-crap.json");
|
|
@@ -8267,20 +8478,57 @@ function readFromFile(workspaceRoot) {
|
|
|
8267
8478
|
);
|
|
8268
8479
|
}
|
|
8269
8480
|
const doc = parsed;
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8481
|
+
let strictness = null;
|
|
8482
|
+
if ("strictness" in doc) {
|
|
8483
|
+
const value = doc["strictness"];
|
|
8484
|
+
if (typeof value !== "string") {
|
|
8485
|
+
throw new CrapConfigError(
|
|
8486
|
+
`[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`
|
|
8487
|
+
);
|
|
8488
|
+
}
|
|
8489
|
+
const normalized = value.trim().toLowerCase();
|
|
8490
|
+
if (!isStrictness(normalized)) {
|
|
8491
|
+
throw new CrapConfigError(
|
|
8492
|
+
`[crap-config] ${filePath}: 'strictness' is "${value}"; expected one of ${STRICTNESS_VALUES.join(", ")}.`
|
|
8493
|
+
);
|
|
8494
|
+
}
|
|
8495
|
+
strictness = normalized;
|
|
8276
8496
|
}
|
|
8277
|
-
|
|
8278
|
-
if (
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8497
|
+
let exclude = [];
|
|
8498
|
+
if ("exclude" in doc) {
|
|
8499
|
+
const raw2 = doc["exclude"];
|
|
8500
|
+
if (!Array.isArray(raw2)) {
|
|
8501
|
+
throw new CrapConfigError(
|
|
8502
|
+
`[crap-config] ${filePath}: 'exclude' must be an array of strings`
|
|
8503
|
+
);
|
|
8504
|
+
}
|
|
8505
|
+
for (const item of raw2) {
|
|
8506
|
+
if (typeof item !== "string") {
|
|
8507
|
+
throw new CrapConfigError(
|
|
8508
|
+
`[crap-config] ${filePath}: every entry in 'exclude' must be a string, got ${typeof item}`
|
|
8509
|
+
);
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
exclude = raw2;
|
|
8282
8513
|
}
|
|
8283
|
-
|
|
8514
|
+
let projectDirs = [];
|
|
8515
|
+
if ("projectDirs" in doc) {
|
|
8516
|
+
const raw2 = doc["projectDirs"];
|
|
8517
|
+
if (!Array.isArray(raw2)) {
|
|
8518
|
+
throw new CrapConfigError(
|
|
8519
|
+
`[crap-config] ${filePath}: 'projectDirs' must be an array of strings`
|
|
8520
|
+
);
|
|
8521
|
+
}
|
|
8522
|
+
for (const item of raw2) {
|
|
8523
|
+
if (typeof item !== "string") {
|
|
8524
|
+
throw new CrapConfigError(
|
|
8525
|
+
`[crap-config] ${filePath}: every entry in 'projectDirs' must be a string, got ${typeof item}`
|
|
8526
|
+
);
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
projectDirs = raw2;
|
|
8530
|
+
}
|
|
8531
|
+
return { strictness, exclude, projectDirs };
|
|
8284
8532
|
}
|
|
8285
8533
|
function isStrictness(value) {
|
|
8286
8534
|
return STRICTNESS_VALUES.includes(value);
|
|
@@ -8288,7 +8536,7 @@ function isStrictness(value) {
|
|
|
8288
8536
|
|
|
8289
8537
|
// src/tools/test-harness.ts
|
|
8290
8538
|
import { promises as fs6 } from "node:fs";
|
|
8291
|
-
import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute3, join as join6, relative, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8539
|
+
import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8292
8540
|
var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
|
|
8293
8541
|
function isTestFile(filePath) {
|
|
8294
8542
|
const base = basename(filePath);
|
|
@@ -8303,7 +8551,7 @@ function candidatePaths(workspaceRoot, filePath) {
|
|
|
8303
8551
|
const base = basename(absSource, ext);
|
|
8304
8552
|
const dir = dirname4(absSource);
|
|
8305
8553
|
const absWorkspace = resolve5(workspaceRoot);
|
|
8306
|
-
const relFromRoot =
|
|
8554
|
+
const relFromRoot = relative2(absWorkspace, absSource);
|
|
8307
8555
|
const relDir = dirname4(relFromRoot);
|
|
8308
8556
|
const candidates = /* @__PURE__ */ new Set();
|
|
8309
8557
|
candidates.add(join6(dir, `${base}.test${ext}`));
|
|
@@ -8355,8 +8603,8 @@ import { existsSync as existsSync5 } from "node:fs";
|
|
|
8355
8603
|
import { join as join11 } from "node:path";
|
|
8356
8604
|
|
|
8357
8605
|
// src/scanner/detector.ts
|
|
8358
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
8359
|
-
import { join as join7 } from "node:path";
|
|
8606
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
|
|
8607
|
+
import { join as join7, resolve as resolve6 } from "node:path";
|
|
8360
8608
|
import { execFile } from "node:child_process";
|
|
8361
8609
|
var SCANNER_SIGNALS = {
|
|
8362
8610
|
eslint: {
|
|
@@ -8405,6 +8653,19 @@ var SCANNER_SIGNALS = {
|
|
|
8405
8653
|
],
|
|
8406
8654
|
packageJsonKeys: ["@stryker-mutator/core"],
|
|
8407
8655
|
binaryNames: ["stryker"]
|
|
8656
|
+
},
|
|
8657
|
+
dart_analyze: {
|
|
8658
|
+
configFiles: [
|
|
8659
|
+
"analysis_options.yaml",
|
|
8660
|
+
"pubspec.yaml"
|
|
8661
|
+
],
|
|
8662
|
+
packageJsonKeys: [],
|
|
8663
|
+
binaryNames: ["dart"]
|
|
8664
|
+
},
|
|
8665
|
+
dotnet_format: {
|
|
8666
|
+
configFiles: [],
|
|
8667
|
+
packageJsonKeys: [],
|
|
8668
|
+
binaryNames: ["dotnet"]
|
|
8408
8669
|
}
|
|
8409
8670
|
};
|
|
8410
8671
|
function probeConfigFiles(workspaceRoot, scanner) {
|
|
@@ -8435,14 +8696,14 @@ function probePackageJson(workspaceRoot, scanner) {
|
|
|
8435
8696
|
}
|
|
8436
8697
|
}
|
|
8437
8698
|
function probeBinary(binaryName) {
|
|
8438
|
-
return new Promise((
|
|
8699
|
+
return new Promise((resolve8) => {
|
|
8439
8700
|
execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
8440
|
-
|
|
8701
|
+
resolve8(err === null);
|
|
8441
8702
|
});
|
|
8442
8703
|
});
|
|
8443
8704
|
}
|
|
8444
8705
|
async function detectScanners(workspaceRoot) {
|
|
8445
|
-
const scanners = ["eslint", "semgrep", "bandit", "stryker"];
|
|
8706
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
8446
8707
|
const results = await Promise.all(
|
|
8447
8708
|
scanners.map(async (scanner) => {
|
|
8448
8709
|
const configProbe = probeConfigFiles(workspaceRoot, scanner);
|
|
@@ -8455,10 +8716,13 @@ async function detectScanners(workspaceRoot) {
|
|
|
8455
8716
|
};
|
|
8456
8717
|
}
|
|
8457
8718
|
if (probePackageJson(workspaceRoot, scanner)) {
|
|
8719
|
+
const binName = SCANNER_SIGNALS[scanner].binaryNames[0];
|
|
8720
|
+
const binPath = binName ? join7(workspaceRoot, "node_modules", ".bin", binName) : null;
|
|
8721
|
+
const installed = binPath !== null && existsSync2(binPath);
|
|
8458
8722
|
return {
|
|
8459
8723
|
scanner,
|
|
8460
|
-
available:
|
|
8461
|
-
reason: `found in package.json
|
|
8724
|
+
available: installed,
|
|
8725
|
+
reason: installed ? "found in package.json and installed" : `found in package.json but not installed (run \`npm install\`)`
|
|
8462
8726
|
};
|
|
8463
8727
|
}
|
|
8464
8728
|
const signals = SCANNER_SIGNALS[scanner];
|
|
@@ -8480,6 +8744,58 @@ async function detectScanners(workspaceRoot) {
|
|
|
8480
8744
|
);
|
|
8481
8745
|
return results;
|
|
8482
8746
|
}
|
|
8747
|
+
var MONOREPO_DIRS = ["apps", "packages", "libs", "modules", "services"];
|
|
8748
|
+
async function detectMonorepoScanners(workspaceRoot) {
|
|
8749
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
8750
|
+
try {
|
|
8751
|
+
const pkgPath = join7(workspaceRoot, "package.json");
|
|
8752
|
+
const raw = readFileSync3(pkgPath, "utf-8");
|
|
8753
|
+
const pkg = JSON.parse(raw);
|
|
8754
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
8755
|
+
for (const ws of pkg.workspaces) {
|
|
8756
|
+
if (typeof ws === "string" && !ws.includes("*")) {
|
|
8757
|
+
const full = resolve6(workspaceRoot, ws);
|
|
8758
|
+
if (existsSync2(full)) subdirs.add(full);
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
}
|
|
8762
|
+
} catch {
|
|
8763
|
+
}
|
|
8764
|
+
for (const dir of MONOREPO_DIRS) {
|
|
8765
|
+
const full = join7(workspaceRoot, dir);
|
|
8766
|
+
try {
|
|
8767
|
+
const entries = readdirSync(full, { withFileTypes: true });
|
|
8768
|
+
for (const entry of entries) {
|
|
8769
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
8770
|
+
subdirs.add(join7(full, entry.name));
|
|
8771
|
+
}
|
|
8772
|
+
}
|
|
8773
|
+
} catch {
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
if (subdirs.size === 0) return [];
|
|
8777
|
+
const detections = [];
|
|
8778
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
8779
|
+
for (const subdir of subdirs) {
|
|
8780
|
+
for (const scanner of scanners) {
|
|
8781
|
+
const configProbe = probeConfigFiles(subdir, scanner);
|
|
8782
|
+
if (!configProbe.found) continue;
|
|
8783
|
+
if (scanner === "dart_analyze") {
|
|
8784
|
+
const hasBinary = await probeBinary("dart");
|
|
8785
|
+
if (!hasBinary) continue;
|
|
8786
|
+
}
|
|
8787
|
+
const relDir = subdir.replace(workspaceRoot + "/", "");
|
|
8788
|
+
detections.push({
|
|
8789
|
+
scanner,
|
|
8790
|
+
available: true,
|
|
8791
|
+
reason: `config file found in ${relDir}/`,
|
|
8792
|
+
...configProbe.path ? { configPath: configProbe.path } : {},
|
|
8793
|
+
workingDir: subdir
|
|
8794
|
+
});
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
return detections;
|
|
8798
|
+
}
|
|
8483
8799
|
|
|
8484
8800
|
// src/scanner/runner.ts
|
|
8485
8801
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -8516,17 +8832,39 @@ function getScannerCommand(scanner, workspaceRoot) {
|
|
|
8516
8832
|
nonZeroIsNormal: false,
|
|
8517
8833
|
outputFile: join8(workspaceRoot, "reports", "mutation", "mutation.json")
|
|
8518
8834
|
};
|
|
8835
|
+
case "dart_analyze":
|
|
8836
|
+
return {
|
|
8837
|
+
command: "dart",
|
|
8838
|
+
args: ["analyze", "--format=json", "."],
|
|
8839
|
+
timeoutMs: 12e4,
|
|
8840
|
+
nonZeroIsNormal: true
|
|
8841
|
+
// exits 3 when findings exist
|
|
8842
|
+
};
|
|
8843
|
+
case "dotnet_format":
|
|
8844
|
+
return {
|
|
8845
|
+
command: "dotnet",
|
|
8846
|
+
args: [
|
|
8847
|
+
"format",
|
|
8848
|
+
"--verify-no-changes",
|
|
8849
|
+
"--report",
|
|
8850
|
+
join8(workspaceRoot, ".claude-crap", "dotnet-report.json")
|
|
8851
|
+
],
|
|
8852
|
+
timeoutMs: 12e4,
|
|
8853
|
+
nonZeroIsNormal: true,
|
|
8854
|
+
outputFile: join8(workspaceRoot, ".claude-crap", "dotnet-report.json")
|
|
8855
|
+
};
|
|
8519
8856
|
}
|
|
8520
8857
|
}
|
|
8521
|
-
function runScanner(scanner, workspaceRoot) {
|
|
8858
|
+
function runScanner(scanner, workspaceRoot, options) {
|
|
8522
8859
|
const start = Date.now();
|
|
8523
|
-
const
|
|
8524
|
-
|
|
8860
|
+
const cwd = options?.workingDir ?? workspaceRoot;
|
|
8861
|
+
const cmd = getScannerCommand(scanner, cwd);
|
|
8862
|
+
return new Promise((resolve8) => {
|
|
8525
8863
|
execFile2(
|
|
8526
8864
|
cmd.command,
|
|
8527
8865
|
cmd.args,
|
|
8528
8866
|
{
|
|
8529
|
-
cwd
|
|
8867
|
+
cwd,
|
|
8530
8868
|
timeout: cmd.timeoutMs,
|
|
8531
8869
|
maxBuffer: 50 * 1024 * 1024,
|
|
8532
8870
|
// 50 MB — large codebases produce verbose output
|
|
@@ -8540,7 +8878,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8540
8878
|
if (cmd.outputFile && existsSync3(cmd.outputFile)) {
|
|
8541
8879
|
try {
|
|
8542
8880
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8543
|
-
|
|
8881
|
+
resolve8({
|
|
8544
8882
|
scanner,
|
|
8545
8883
|
success: true,
|
|
8546
8884
|
rawOutput: fileOutput,
|
|
@@ -8550,7 +8888,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8550
8888
|
} catch {
|
|
8551
8889
|
}
|
|
8552
8890
|
}
|
|
8553
|
-
|
|
8891
|
+
resolve8({
|
|
8554
8892
|
scanner,
|
|
8555
8893
|
success: false,
|
|
8556
8894
|
rawOutput: "",
|
|
@@ -8563,7 +8901,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8563
8901
|
if (existsSync3(cmd.outputFile)) {
|
|
8564
8902
|
try {
|
|
8565
8903
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8566
|
-
|
|
8904
|
+
resolve8({
|
|
8567
8905
|
scanner,
|
|
8568
8906
|
success: true,
|
|
8569
8907
|
rawOutput: fileOutput,
|
|
@@ -8571,7 +8909,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8571
8909
|
});
|
|
8572
8910
|
return;
|
|
8573
8911
|
} catch (readErr) {
|
|
8574
|
-
|
|
8912
|
+
resolve8({
|
|
8575
8913
|
scanner,
|
|
8576
8914
|
success: false,
|
|
8577
8915
|
rawOutput: "",
|
|
@@ -8581,7 +8919,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8581
8919
|
return;
|
|
8582
8920
|
}
|
|
8583
8921
|
}
|
|
8584
|
-
|
|
8922
|
+
resolve8({
|
|
8585
8923
|
scanner,
|
|
8586
8924
|
success: false,
|
|
8587
8925
|
rawOutput: "",
|
|
@@ -8592,7 +8930,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8592
8930
|
}
|
|
8593
8931
|
const output = stdout.trim();
|
|
8594
8932
|
if (!output) {
|
|
8595
|
-
|
|
8933
|
+
resolve8({
|
|
8596
8934
|
scanner,
|
|
8597
8935
|
success: true,
|
|
8598
8936
|
rawOutput: "[]",
|
|
@@ -8601,7 +8939,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8601
8939
|
});
|
|
8602
8940
|
return;
|
|
8603
8941
|
}
|
|
8604
|
-
|
|
8942
|
+
resolve8({
|
|
8605
8943
|
scanner,
|
|
8606
8944
|
success: true,
|
|
8607
8945
|
rawOutput: output,
|
|
@@ -8613,7 +8951,7 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8613
8951
|
}
|
|
8614
8952
|
|
|
8615
8953
|
// src/scanner/bootstrap.ts
|
|
8616
|
-
import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
|
|
8954
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync as readdirSync2 } from "node:fs";
|
|
8617
8955
|
import { join as join9 } from "node:path";
|
|
8618
8956
|
import { execFile as execFile3 } from "node:child_process";
|
|
8619
8957
|
function detectProjectType(workspaceRoot) {
|
|
@@ -8630,12 +8968,13 @@ function detectProjectType(workspaceRoot) {
|
|
|
8630
8968
|
}
|
|
8631
8969
|
if (has("Directory.Build.props")) return "csharp";
|
|
8632
8970
|
try {
|
|
8633
|
-
const entries =
|
|
8971
|
+
const entries = readdirSync2(workspaceRoot);
|
|
8634
8972
|
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
8635
8973
|
return "csharp";
|
|
8636
8974
|
}
|
|
8637
8975
|
} catch {
|
|
8638
8976
|
}
|
|
8977
|
+
if (has("pubspec.yaml")) return "dart";
|
|
8639
8978
|
return "unknown";
|
|
8640
8979
|
}
|
|
8641
8980
|
function generateEslintConfig(isTypeScript) {
|
|
@@ -8677,7 +9016,7 @@ export default [
|
|
|
8677
9016
|
`;
|
|
8678
9017
|
}
|
|
8679
9018
|
function npmInstall(workspaceRoot, packages) {
|
|
8680
|
-
return new Promise((
|
|
9019
|
+
return new Promise((resolve8) => {
|
|
8681
9020
|
execFile3(
|
|
8682
9021
|
"npm",
|
|
8683
9022
|
["install", "--save-dev", ...packages],
|
|
@@ -8688,14 +9027,14 @@ function npmInstall(workspaceRoot, packages) {
|
|
|
8688
9027
|
},
|
|
8689
9028
|
(err, stdout, stderr) => {
|
|
8690
9029
|
if (err) {
|
|
8691
|
-
|
|
9030
|
+
resolve8({
|
|
8692
9031
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8693
9032
|
success: false,
|
|
8694
9033
|
detail: stderr || err.message
|
|
8695
9034
|
});
|
|
8696
9035
|
return;
|
|
8697
9036
|
}
|
|
8698
|
-
|
|
9037
|
+
resolve8({
|
|
8699
9038
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8700
9039
|
success: true,
|
|
8701
9040
|
detail: `installed ${packages.join(", ")}`
|
|
@@ -8744,12 +9083,23 @@ function getRecommendation(projectType) {
|
|
|
8744
9083
|
installInstructions: "pip install bandit (or: pipx install bandit, poetry add --group dev bandit)"
|
|
8745
9084
|
};
|
|
8746
9085
|
case "java":
|
|
8747
|
-
case "csharp":
|
|
8748
9086
|
return {
|
|
8749
9087
|
scanner: "semgrep",
|
|
8750
9088
|
canAutoInstall: false,
|
|
8751
9089
|
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8752
9090
|
};
|
|
9091
|
+
case "csharp":
|
|
9092
|
+
return {
|
|
9093
|
+
scanner: "dotnet_format",
|
|
9094
|
+
canAutoInstall: false,
|
|
9095
|
+
installInstructions: "Install the .NET SDK: https://dotnet.microsoft.com/download"
|
|
9096
|
+
};
|
|
9097
|
+
case "dart":
|
|
9098
|
+
return {
|
|
9099
|
+
scanner: "dart_analyze",
|
|
9100
|
+
canAutoInstall: false,
|
|
9101
|
+
installInstructions: "Install the Dart SDK: https://dart.dev/get-dart (or Flutter SDK which includes Dart)"
|
|
9102
|
+
};
|
|
8753
9103
|
case "unknown":
|
|
8754
9104
|
return {
|
|
8755
9105
|
scanner: "semgrep",
|
|
@@ -8896,23 +9246,7 @@ function buildResult(projectType, steps, autoScanResult, recommendation) {
|
|
|
8896
9246
|
|
|
8897
9247
|
// src/scanner/complexity-scanner.ts
|
|
8898
9248
|
import { promises as fs7 } from "node:fs";
|
|
8899
|
-
import { join as join10, relative as
|
|
8900
|
-
var SKIP_DIRS3 = /* @__PURE__ */ new Set([
|
|
8901
|
-
"node_modules",
|
|
8902
|
-
".git",
|
|
8903
|
-
"dist",
|
|
8904
|
-
"build",
|
|
8905
|
-
"out",
|
|
8906
|
-
"target",
|
|
8907
|
-
".venv",
|
|
8908
|
-
"venv",
|
|
8909
|
-
"__pycache__",
|
|
8910
|
-
".cache",
|
|
8911
|
-
".next",
|
|
8912
|
-
".nuxt",
|
|
8913
|
-
".claude-crap",
|
|
8914
|
-
".codesight"
|
|
8915
|
-
]);
|
|
9249
|
+
import { join as join10, relative as relative3 } from "node:path";
|
|
8916
9250
|
var MAX_FILES = 2e4;
|
|
8917
9251
|
var RULE_ID = "complexity/cyclomatic-max";
|
|
8918
9252
|
var SOURCE_TOOL = "complexity";
|
|
@@ -8920,7 +9254,8 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
|
|
|
8920
9254
|
const start = Date.now();
|
|
8921
9255
|
const threshold = config.cyclomaticMax;
|
|
8922
9256
|
const errorThreshold = threshold * 2;
|
|
8923
|
-
const
|
|
9257
|
+
const filter = createExclusionFilter(config.exclude);
|
|
9258
|
+
const files = await collectSourceFiles(workspaceRoot, filter);
|
|
8924
9259
|
logger2.info(
|
|
8925
9260
|
{ fileCount: files.length, threshold },
|
|
8926
9261
|
"complexity-scanner: starting analysis"
|
|
@@ -8939,7 +9274,7 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
|
|
|
8939
9274
|
for (const fn of metrics.functions) {
|
|
8940
9275
|
if (fn.cyclomaticComplexity <= threshold) continue;
|
|
8941
9276
|
const level = fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
|
|
8942
|
-
const relPath =
|
|
9277
|
+
const relPath = relative3(workspaceRoot, filePath);
|
|
8943
9278
|
sarifResults.push({
|
|
8944
9279
|
ruleId: RULE_ID,
|
|
8945
9280
|
level,
|
|
@@ -8990,7 +9325,7 @@ async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2
|
|
|
8990
9325
|
);
|
|
8991
9326
|
return { filesScanned, functionsAnalyzed, violations, durationMs };
|
|
8992
9327
|
}
|
|
8993
|
-
async function collectSourceFiles(workspaceRoot) {
|
|
9328
|
+
async function collectSourceFiles(workspaceRoot, filter) {
|
|
8994
9329
|
const files = [];
|
|
8995
9330
|
let truncated = false;
|
|
8996
9331
|
async function walk2(dir) {
|
|
@@ -9003,15 +9338,16 @@ async function collectSourceFiles(workspaceRoot) {
|
|
|
9003
9338
|
}
|
|
9004
9339
|
for (const entry of entries) {
|
|
9005
9340
|
if (truncated) return;
|
|
9006
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
9007
9341
|
const full = join10(dir, entry.name);
|
|
9008
9342
|
if (entry.isDirectory()) {
|
|
9009
|
-
if (
|
|
9343
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
9010
9344
|
await walk2(full);
|
|
9011
9345
|
continue;
|
|
9012
9346
|
}
|
|
9013
9347
|
if (!entry.isFile()) continue;
|
|
9014
9348
|
if (!detectLanguageFromPath(entry.name)) continue;
|
|
9349
|
+
const relPath = relative3(workspaceRoot, full);
|
|
9350
|
+
if (filter.shouldSkipFile(relPath, entry.name)) continue;
|
|
9015
9351
|
files.push(full);
|
|
9016
9352
|
if (files.length >= MAX_FILES) {
|
|
9017
9353
|
truncated = true;
|
|
@@ -9038,10 +9374,18 @@ function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
|
9038
9374
|
async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
9039
9375
|
const start = Date.now();
|
|
9040
9376
|
const detected = await detectScanners(workspaceRoot);
|
|
9377
|
+
const monorepoDetected = await detectMonorepoScanners(workspaceRoot);
|
|
9378
|
+
const rootScannerSet = new Set(detected.filter((d) => d.available).map((d) => d.scanner));
|
|
9379
|
+
for (const md of monorepoDetected) {
|
|
9380
|
+
if (!rootScannerSet.has(md.scanner)) {
|
|
9381
|
+
detected.push(md);
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9041
9384
|
const available = detected.filter((d) => d.available);
|
|
9042
9385
|
logger2.info(
|
|
9043
9386
|
{
|
|
9044
9387
|
detected: detected.map((d) => `${d.scanner}:${d.available}`),
|
|
9388
|
+
monorepo: monorepoDetected.length,
|
|
9045
9389
|
available: available.length
|
|
9046
9390
|
},
|
|
9047
9391
|
"auto-scan: detection complete"
|
|
@@ -9096,7 +9440,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
|
9096
9440
|
};
|
|
9097
9441
|
}
|
|
9098
9442
|
const runResults = await Promise.allSettled(
|
|
9099
|
-
available.map((d) => runScanner(d.scanner, workspaceRoot))
|
|
9443
|
+
available.map((d) => runScanner(d.scanner, workspaceRoot, d.workingDir ? { workingDir: d.workingDir } : void 0))
|
|
9100
9444
|
);
|
|
9101
9445
|
const results = [];
|
|
9102
9446
|
let totalFindings = 0;
|
|
@@ -9177,7 +9521,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
|
9177
9521
|
workspaceRoot,
|
|
9178
9522
|
options.engine,
|
|
9179
9523
|
sarifStore,
|
|
9180
|
-
{ cyclomaticMax: options.cyclomaticMax ?? 15 },
|
|
9524
|
+
{ cyclomaticMax: options.cyclomaticMax ?? 15, ...options.exclude ? { exclude: options.exclude } : {} },
|
|
9181
9525
|
logger2
|
|
9182
9526
|
);
|
|
9183
9527
|
totalFindings += complexityScan.violations;
|
|
@@ -9197,6 +9541,192 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
|
9197
9541
|
};
|
|
9198
9542
|
}
|
|
9199
9543
|
|
|
9544
|
+
// src/monorepo/project-map.ts
|
|
9545
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
9546
|
+
import { promises as fs8 } from "node:fs";
|
|
9547
|
+
import { join as join12, basename as basename2, resolve as resolve7 } from "node:path";
|
|
9548
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
9549
|
+
var MONOREPO_DIRS2 = ["apps", "packages", "libs", "modules", "services"];
|
|
9550
|
+
var SCANNER_FOR_TYPE = {
|
|
9551
|
+
typescript: "eslint",
|
|
9552
|
+
javascript: "eslint",
|
|
9553
|
+
python: "bandit",
|
|
9554
|
+
java: "semgrep",
|
|
9555
|
+
csharp: "dotnet_format",
|
|
9556
|
+
dart: "dart_analyze",
|
|
9557
|
+
unknown: null
|
|
9558
|
+
};
|
|
9559
|
+
var BINARY_FOR_SCANNER = {
|
|
9560
|
+
eslint: "eslint",
|
|
9561
|
+
bandit: "bandit",
|
|
9562
|
+
semgrep: "semgrep",
|
|
9563
|
+
dart_analyze: "dart",
|
|
9564
|
+
dotnet_format: "dotnet"
|
|
9565
|
+
};
|
|
9566
|
+
function probeBinary2(binaryName) {
|
|
9567
|
+
return new Promise((resolve8) => {
|
|
9568
|
+
execFile4("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
9569
|
+
resolve8(err === null);
|
|
9570
|
+
});
|
|
9571
|
+
});
|
|
9572
|
+
}
|
|
9573
|
+
function detectProjectType2(dir) {
|
|
9574
|
+
const has = (file) => existsSync6(join12(dir, file));
|
|
9575
|
+
if (has("pubspec.yaml")) return "dart";
|
|
9576
|
+
if (has("package.json")) {
|
|
9577
|
+
if (has("tsconfig.json")) return "typescript";
|
|
9578
|
+
return "javascript";
|
|
9579
|
+
}
|
|
9580
|
+
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
|
|
9581
|
+
return "python";
|
|
9582
|
+
}
|
|
9583
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
|
|
9584
|
+
return "java";
|
|
9585
|
+
}
|
|
9586
|
+
if (has("Directory.Build.props")) return "csharp";
|
|
9587
|
+
try {
|
|
9588
|
+
const entries = readdirSync3(dir);
|
|
9589
|
+
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
9590
|
+
return "csharp";
|
|
9591
|
+
}
|
|
9592
|
+
} catch {
|
|
9593
|
+
}
|
|
9594
|
+
return "unknown";
|
|
9595
|
+
}
|
|
9596
|
+
function extractWorkspacePatterns(workspaces) {
|
|
9597
|
+
if (Array.isArray(workspaces)) {
|
|
9598
|
+
return workspaces.filter((v) => typeof v === "string");
|
|
9599
|
+
}
|
|
9600
|
+
if (workspaces !== null && typeof workspaces === "object" && "packages" in workspaces && Array.isArray(workspaces.packages)) {
|
|
9601
|
+
return workspaces.packages.filter(
|
|
9602
|
+
(v) => typeof v === "string"
|
|
9603
|
+
);
|
|
9604
|
+
}
|
|
9605
|
+
return [];
|
|
9606
|
+
}
|
|
9607
|
+
function expandWorkspacePattern(workspaceRoot, pattern) {
|
|
9608
|
+
if (pattern.endsWith("/*")) {
|
|
9609
|
+
const parentDir = join12(workspaceRoot, pattern.slice(0, -2));
|
|
9610
|
+
try {
|
|
9611
|
+
const entries = readdirSync3(parentDir, { withFileTypes: true });
|
|
9612
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join12(parentDir, e.name));
|
|
9613
|
+
} catch {
|
|
9614
|
+
return [];
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9617
|
+
const full = resolve7(workspaceRoot, pattern);
|
|
9618
|
+
try {
|
|
9619
|
+
const entries = readdirSync3(full, { withFileTypes: true });
|
|
9620
|
+
void entries;
|
|
9621
|
+
return [full];
|
|
9622
|
+
} catch {
|
|
9623
|
+
return [];
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
function collectSubdirectories(workspaceRoot, extraDirs) {
|
|
9627
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
9628
|
+
const pkgPath = join12(workspaceRoot, "package.json");
|
|
9629
|
+
if (existsSync6(pkgPath)) {
|
|
9630
|
+
try {
|
|
9631
|
+
const raw = readFileSync5(pkgPath, "utf-8");
|
|
9632
|
+
const pkg = JSON.parse(raw);
|
|
9633
|
+
const patterns = extractWorkspacePatterns(pkg["workspaces"]);
|
|
9634
|
+
for (const pattern of patterns) {
|
|
9635
|
+
for (const absPath of expandWorkspacePattern(workspaceRoot, pattern)) {
|
|
9636
|
+
subdirs.add(absPath);
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
} catch {
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
if (extraDirs && extraDirs.length > 0) {
|
|
9643
|
+
for (const dir of extraDirs) {
|
|
9644
|
+
const absDir = resolve7(workspaceRoot, dir);
|
|
9645
|
+
if (!existsSync6(absDir)) continue;
|
|
9646
|
+
const hasMarker = PROJECT_MARKERS.some((m) => existsSync6(join12(absDir, m)));
|
|
9647
|
+
if (hasMarker) {
|
|
9648
|
+
subdirs.add(absDir);
|
|
9649
|
+
continue;
|
|
9650
|
+
}
|
|
9651
|
+
try {
|
|
9652
|
+
const entries = readdirSync3(absDir, { withFileTypes: true });
|
|
9653
|
+
for (const entry of entries) {
|
|
9654
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
9655
|
+
subdirs.add(join12(absDir, entry.name));
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
} catch {
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
const configuredDirNames = new Set(extraDirs?.map((d) => d.split("/")[0]) ?? []);
|
|
9663
|
+
for (const dir of MONOREPO_DIRS2) {
|
|
9664
|
+
if (configuredDirNames.has(dir)) continue;
|
|
9665
|
+
const parentDir = join12(workspaceRoot, dir);
|
|
9666
|
+
try {
|
|
9667
|
+
const entries = readdirSync3(parentDir, { withFileTypes: true });
|
|
9668
|
+
for (const entry of entries) {
|
|
9669
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
9670
|
+
subdirs.add(join12(parentDir, entry.name));
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
} catch {
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9676
|
+
return subdirs;
|
|
9677
|
+
}
|
|
9678
|
+
var PROJECT_MARKERS = [
|
|
9679
|
+
"package.json",
|
|
9680
|
+
"pubspec.yaml",
|
|
9681
|
+
"pyproject.toml",
|
|
9682
|
+
"setup.py",
|
|
9683
|
+
"pom.xml",
|
|
9684
|
+
"build.gradle",
|
|
9685
|
+
"build.gradle.kts",
|
|
9686
|
+
"Directory.Build.props"
|
|
9687
|
+
];
|
|
9688
|
+
async function discoverProjectMap(workspaceRoot, options) {
|
|
9689
|
+
const subdirs = collectSubdirectories(workspaceRoot, options?.projectDirs);
|
|
9690
|
+
const binaryCache = /* @__PURE__ */ new Map();
|
|
9691
|
+
const probeScanner = (scanner) => {
|
|
9692
|
+
const binaryName = BINARY_FOR_SCANNER[scanner];
|
|
9693
|
+
if (binaryName === void 0) return Promise.resolve(false);
|
|
9694
|
+
const cached = binaryCache.get(scanner);
|
|
9695
|
+
if (cached !== void 0) return cached;
|
|
9696
|
+
const probe = probeBinary2(binaryName);
|
|
9697
|
+
binaryCache.set(scanner, probe);
|
|
9698
|
+
return probe;
|
|
9699
|
+
};
|
|
9700
|
+
const projectEntries = await Promise.all(
|
|
9701
|
+
[...subdirs].map(async (absPath) => {
|
|
9702
|
+
const relPath = absPath.replace(workspaceRoot + "/", "");
|
|
9703
|
+
const type = detectProjectType2(absPath);
|
|
9704
|
+
const scanner = SCANNER_FOR_TYPE[type];
|
|
9705
|
+
const scannerAvailable = scanner !== null ? await probeScanner(scanner) : false;
|
|
9706
|
+
return {
|
|
9707
|
+
name: basename2(absPath),
|
|
9708
|
+
path: relPath,
|
|
9709
|
+
type,
|
|
9710
|
+
scanner,
|
|
9711
|
+
scannerAvailable
|
|
9712
|
+
};
|
|
9713
|
+
})
|
|
9714
|
+
);
|
|
9715
|
+
projectEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
9716
|
+
return {
|
|
9717
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9718
|
+
workspaceRoot,
|
|
9719
|
+
isMonorepo: projectEntries.length > 0,
|
|
9720
|
+
projects: projectEntries
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
async function persistProjectMap(map, workspaceRoot) {
|
|
9724
|
+
const dir = join12(workspaceRoot, ".claude-crap");
|
|
9725
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
9726
|
+
const filePath = join12(dir, "projects.json");
|
|
9727
|
+
await fs8.writeFile(filePath, JSON.stringify(map, null, 2) + "\n", "utf-8");
|
|
9728
|
+
}
|
|
9729
|
+
|
|
9200
9730
|
// src/schemas/tool-schemas.ts
|
|
9201
9731
|
var computeCrapSchema = {
|
|
9202
9732
|
type: "object",
|
|
@@ -9287,11 +9817,22 @@ var scoreProjectSchema = {
|
|
|
9287
9817
|
type: "string",
|
|
9288
9818
|
enum: ["markdown", "json", "both"],
|
|
9289
9819
|
description: "Output format. `markdown` returns only the chat summary, `json` returns only the structured snapshot, `both` (default) returns both as separate content blocks."
|
|
9820
|
+
},
|
|
9821
|
+
scope: {
|
|
9822
|
+
type: "string",
|
|
9823
|
+
description: "Optional project name from the project map. When provided, the score is computed only for files within that project's subtree. Omit to score the entire workspace."
|
|
9290
9824
|
}
|
|
9291
9825
|
},
|
|
9292
9826
|
required: [],
|
|
9293
9827
|
additionalProperties: false
|
|
9294
9828
|
};
|
|
9829
|
+
var listProjectsSchema = {
|
|
9830
|
+
type: "object",
|
|
9831
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner. In a single-project workspace, returns an empty list.",
|
|
9832
|
+
properties: {},
|
|
9833
|
+
required: [],
|
|
9834
|
+
additionalProperties: false
|
|
9835
|
+
};
|
|
9295
9836
|
var requireTestHarnessSchema = {
|
|
9296
9837
|
type: "object",
|
|
9297
9838
|
description: "Check whether a production source file has a matching test file. Returns the first existing test path, or the full list of paths the resolver probed when none exists. Use this BEFORE writing any functional code \u2014 the CLAUDE.md Golden Rule requires a test harness to exist first.",
|
|
@@ -9313,7 +9854,7 @@ var ingestScannerOutputSchema = {
|
|
|
9313
9854
|
properties: {
|
|
9314
9855
|
scanner: {
|
|
9315
9856
|
type: "string",
|
|
9316
|
-
enum: ["semgrep", "eslint", "bandit", "stryker"],
|
|
9857
|
+
enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"],
|
|
9317
9858
|
description: "Identifier of the producing scanner."
|
|
9318
9859
|
},
|
|
9319
9860
|
rawOutput: {
|
|
@@ -9373,6 +9914,20 @@ async function main() {
|
|
|
9373
9914
|
{ config: { ...config, pluginRoot: "<redacted>" } },
|
|
9374
9915
|
"claude-crap MCP server starting"
|
|
9375
9916
|
);
|
|
9917
|
+
let userExclusions = [];
|
|
9918
|
+
let userProjectDirs = [];
|
|
9919
|
+
try {
|
|
9920
|
+
const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
|
|
9921
|
+
userExclusions = crapConfig.exclude;
|
|
9922
|
+
userProjectDirs = crapConfig.projectDirs;
|
|
9923
|
+
if (userExclusions.length > 0) {
|
|
9924
|
+
logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
|
|
9925
|
+
}
|
|
9926
|
+
if (userProjectDirs.length > 0) {
|
|
9927
|
+
logger.info({ projectDirs: userProjectDirs }, "user projectDirs loaded from .claude-crap.json");
|
|
9928
|
+
}
|
|
9929
|
+
} catch {
|
|
9930
|
+
}
|
|
9376
9931
|
const astEngine = new TreeSitterEngine();
|
|
9377
9932
|
const sarifStore = new SarifStore({
|
|
9378
9933
|
workspaceRoot: config.pluginRoot,
|
|
@@ -9383,14 +9938,41 @@ async function main() {
|
|
|
9383
9938
|
{ findings: sarifStore.size(), path: sarifStore.consolidatedReportPath },
|
|
9384
9939
|
"SARIF store ready"
|
|
9385
9940
|
);
|
|
9941
|
+
let projectMap = null;
|
|
9942
|
+
try {
|
|
9943
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
9944
|
+
if (projectMap.isMonorepo) {
|
|
9945
|
+
logger.info(
|
|
9946
|
+
{ projects: projectMap.projects.map((p) => `${p.name}(${p.type})`), count: projectMap.projects.length },
|
|
9947
|
+
"monorepo project map discovered"
|
|
9948
|
+
);
|
|
9949
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
9950
|
+
const needsEslint = projectMap.projects.some(
|
|
9951
|
+
(p) => (p.type === "typescript" || p.type === "javascript") && !p.scannerAvailable
|
|
9952
|
+
);
|
|
9953
|
+
if (needsEslint) {
|
|
9954
|
+
logger.info("monorepo: JS/TS projects detected but ESLint not installed \u2014 bootstrapping");
|
|
9955
|
+
try {
|
|
9956
|
+
await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
9957
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
9958
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
9959
|
+
} catch (err) {
|
|
9960
|
+
logger.warn({ err: err.message }, "monorepo ESLint bootstrap failed");
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
}
|
|
9964
|
+
} catch (err) {
|
|
9965
|
+
logger.warn({ err: err.message }, "project map discovery failed");
|
|
9966
|
+
}
|
|
9386
9967
|
let dashboard = null;
|
|
9387
9968
|
try {
|
|
9388
9969
|
dashboard = await startDashboard({
|
|
9389
9970
|
config,
|
|
9390
9971
|
sarifStore,
|
|
9391
|
-
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
|
|
9972
|
+
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions }),
|
|
9392
9973
|
logger,
|
|
9393
|
-
astEngine
|
|
9974
|
+
astEngine,
|
|
9975
|
+
exclude: userExclusions
|
|
9394
9976
|
});
|
|
9395
9977
|
} catch (err) {
|
|
9396
9978
|
logger.warn(
|
|
@@ -9468,12 +10050,20 @@ async function main() {
|
|
|
9468
10050
|
name: "bootstrap_scanner",
|
|
9469
10051
|
description: "Detect project type, install the right scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create minimal config, and run auto_scan to verify.",
|
|
9470
10052
|
inputSchema: bootstrapScannerSchema
|
|
10053
|
+
},
|
|
10054
|
+
{
|
|
10055
|
+
name: "list_projects",
|
|
10056
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner.",
|
|
10057
|
+
inputSchema: listProjectsSchema
|
|
9471
10058
|
}
|
|
9472
10059
|
]
|
|
9473
10060
|
}));
|
|
9474
10061
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
9475
10062
|
const { name, arguments: args } = request.params;
|
|
9476
10063
|
logger.info({ tool: name }, "Tool call received");
|
|
10064
|
+
return handleToolCall(name, args);
|
|
10065
|
+
});
|
|
10066
|
+
async function handleToolCall(name, args) {
|
|
9477
10067
|
switch (name) {
|
|
9478
10068
|
case "compute_crap": {
|
|
9479
10069
|
const typed = args;
|
|
@@ -9559,10 +10149,18 @@ async function main() {
|
|
|
9559
10149
|
case "score_project": {
|
|
9560
10150
|
const typed = args ?? {};
|
|
9561
10151
|
const format = typed.format ?? "both";
|
|
10152
|
+
let scoreRoot = config.pluginRoot;
|
|
10153
|
+
if (typed.scope && projectMap) {
|
|
10154
|
+
const project = projectMap.projects.find((p) => p.name === typed.scope);
|
|
10155
|
+
if (project) {
|
|
10156
|
+
const { join: join13 } = await import("node:path");
|
|
10157
|
+
scoreRoot = join13(config.pluginRoot, project.path);
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
9562
10160
|
try {
|
|
9563
|
-
const workspace = await estimateWorkspaceLoc(
|
|
10161
|
+
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
9564
10162
|
const score = computeProjectScore({
|
|
9565
|
-
workspaceRoot:
|
|
10163
|
+
workspaceRoot: scoreRoot,
|
|
9566
10164
|
minutesPerLoc: config.minutesPerLoc,
|
|
9567
10165
|
tdrMaxRating: config.tdrMaxRating,
|
|
9568
10166
|
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
@@ -9783,7 +10381,8 @@ async function main() {
|
|
|
9783
10381
|
try {
|
|
9784
10382
|
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
9785
10383
|
engine: astEngine,
|
|
9786
|
-
cyclomaticMax: config.cyclomaticMax
|
|
10384
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
10385
|
+
exclude: userExclusions
|
|
9787
10386
|
});
|
|
9788
10387
|
const markdown = renderAutoScanMarkdown(result);
|
|
9789
10388
|
return {
|
|
@@ -9809,10 +10408,28 @@ async function main() {
|
|
|
9809
10408
|
};
|
|
9810
10409
|
}
|
|
9811
10410
|
}
|
|
10411
|
+
case "list_projects": {
|
|
10412
|
+
return {
|
|
10413
|
+
content: [
|
|
10414
|
+
{
|
|
10415
|
+
type: "text",
|
|
10416
|
+
text: JSON.stringify(
|
|
10417
|
+
{
|
|
10418
|
+
tool: "list_projects",
|
|
10419
|
+
isMonorepo: projectMap?.isMonorepo ?? false,
|
|
10420
|
+
projects: projectMap?.projects ?? []
|
|
10421
|
+
},
|
|
10422
|
+
null,
|
|
10423
|
+
2
|
|
10424
|
+
)
|
|
10425
|
+
}
|
|
10426
|
+
]
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
9812
10429
|
default:
|
|
9813
10430
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
9814
10431
|
}
|
|
9815
|
-
}
|
|
10432
|
+
}
|
|
9816
10433
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
9817
10434
|
resources: [
|
|
9818
10435
|
{
|
|
@@ -9862,7 +10479,8 @@ async function main() {
|
|
|
9862
10479
|
logger.info("claude-crap MCP server ready (stdio)");
|
|
9863
10480
|
autoScan(config.pluginRoot, sarifStore, logger, {
|
|
9864
10481
|
engine: astEngine,
|
|
9865
|
-
cyclomaticMax: config.cyclomaticMax
|
|
10482
|
+
cyclomaticMax: config.cyclomaticMax,
|
|
10483
|
+
exclude: userExclusions
|
|
9866
10484
|
}).then((result) => {
|
|
9867
10485
|
const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
|
|
9868
10486
|
logger.info(
|