claude-crap 0.3.5 → 0.3.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/CHANGELOG.md +13 -0
- package/dist/dashboard/file-detail.d.ts +77 -0
- package/dist/dashboard/file-detail.d.ts.map +1 -0
- package/dist/dashboard/file-detail.js +120 -0
- package/dist/dashboard/file-detail.js.map +1 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +108 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts +8 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -1
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +54 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -0
- package/dist/scanner/complexity-scanner.js +176 -0
- package/dist/scanner/complexity-scanner.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/dashboard/public/index.html +432 -12
- package/plugin/bundle/mcp-server.mjs +429 -71
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/scripts/bundle-plugin.mjs +53 -2
- package/src/dashboard/file-detail.ts +197 -0
- package/src/dashboard/public/index.html +432 -12
- package/src/dashboard/server.ts +141 -1
- package/src/index.ts +20 -2
- package/src/scanner/auto-scan.ts +26 -0
- package/src/scanner/complexity-scanner.ts +233 -0
- package/src/tests/complexity-scanner.test.ts +263 -0
- package/src/tests/file-detail-api.test.ts +258 -0
|
@@ -3585,49 +3585,49 @@ var require_fast_uri = __commonJS({
|
|
|
3585
3585
|
schemelessOptions.skipEscape = true;
|
|
3586
3586
|
return serialize(resolved, schemelessOptions);
|
|
3587
3587
|
}
|
|
3588
|
-
function resolveComponent(base,
|
|
3588
|
+
function resolveComponent(base, relative3, options, skipNormalization) {
|
|
3589
3589
|
const target = {};
|
|
3590
3590
|
if (!skipNormalization) {
|
|
3591
3591
|
base = parse(serialize(base, options), options);
|
|
3592
|
-
|
|
3592
|
+
relative3 = parse(serialize(relative3, 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 && relative3.scheme) {
|
|
3596
|
+
target.scheme = relative3.scheme;
|
|
3597
|
+
target.userinfo = relative3.userinfo;
|
|
3598
|
+
target.host = relative3.host;
|
|
3599
|
+
target.port = relative3.port;
|
|
3600
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3601
|
+
target.query = relative3.query;
|
|
3602
3602
|
} else {
|
|
3603
|
-
if (
|
|
3604
|
-
target.userinfo =
|
|
3605
|
-
target.host =
|
|
3606
|
-
target.port =
|
|
3607
|
-
target.path = removeDotSegments(
|
|
3608
|
-
target.query =
|
|
3603
|
+
if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
|
|
3604
|
+
target.userinfo = relative3.userinfo;
|
|
3605
|
+
target.host = relative3.host;
|
|
3606
|
+
target.port = relative3.port;
|
|
3607
|
+
target.path = removeDotSegments(relative3.path || "");
|
|
3608
|
+
target.query = relative3.query;
|
|
3609
3609
|
} else {
|
|
3610
|
-
if (!
|
|
3610
|
+
if (!relative3.path) {
|
|
3611
3611
|
target.path = base.path;
|
|
3612
|
-
if (
|
|
3613
|
-
target.query =
|
|
3612
|
+
if (relative3.query !== void 0) {
|
|
3613
|
+
target.query = relative3.query;
|
|
3614
3614
|
} else {
|
|
3615
3615
|
target.query = base.query;
|
|
3616
3616
|
}
|
|
3617
3617
|
} else {
|
|
3618
|
-
if (
|
|
3619
|
-
target.path = removeDotSegments(
|
|
3618
|
+
if (relative3.path[0] === "/") {
|
|
3619
|
+
target.path = removeDotSegments(relative3.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 = "/" + relative3.path;
|
|
3623
3623
|
} else if (!base.path) {
|
|
3624
|
-
target.path =
|
|
3624
|
+
target.path = relative3.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) + relative3.path;
|
|
3627
3627
|
}
|
|
3628
3628
|
target.path = removeDotSegments(target.path);
|
|
3629
3629
|
}
|
|
3630
|
-
target.query =
|
|
3630
|
+
target.query = relative3.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 = relative3.fragment;
|
|
3639
3639
|
return target;
|
|
3640
3640
|
}
|
|
3641
3641
|
function equal(uriA, uriB, options) {
|
|
@@ -7041,6 +7041,15 @@ var LANGUAGE_TABLE = {
|
|
|
7041
7041
|
python: PYTHON,
|
|
7042
7042
|
java: JAVA
|
|
7043
7043
|
};
|
|
7044
|
+
function detectLanguageFromPath(filePath) {
|
|
7045
|
+
const lower = filePath.toLowerCase();
|
|
7046
|
+
for (const config of Object.values(LANGUAGE_TABLE)) {
|
|
7047
|
+
for (const ext of config.extensions) {
|
|
7048
|
+
if (lower.endsWith(ext)) return config.id;
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
return null;
|
|
7052
|
+
}
|
|
7044
7053
|
|
|
7045
7054
|
// src/ast/tree-sitter-engine.ts
|
|
7046
7055
|
var TreeSitterEngine = class {
|
|
@@ -7235,8 +7244,8 @@ function loadConfig() {
|
|
|
7235
7244
|
}
|
|
7236
7245
|
|
|
7237
7246
|
// src/dashboard/server.ts
|
|
7238
|
-
import { promises as
|
|
7239
|
-
import { dirname as dirname2, join as join2, resolve as
|
|
7247
|
+
import { promises as fs3, existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
7248
|
+
import { dirname as dirname2, join as join2, resolve as resolve3 } from "node:path";
|
|
7240
7249
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7241
7250
|
import Fastify from "fastify";
|
|
7242
7251
|
import fastifyStatic from "@fastify/static";
|
|
@@ -7403,6 +7412,105 @@ function renderProjectScoreMarkdown(score) {
|
|
|
7403
7412
|
].join("\n");
|
|
7404
7413
|
}
|
|
7405
7414
|
|
|
7415
|
+
// src/dashboard/file-detail.ts
|
|
7416
|
+
import { promises as fs2 } from "node:fs";
|
|
7417
|
+
|
|
7418
|
+
// src/workspace-guard.ts
|
|
7419
|
+
import { isAbsolute, resolve as resolve2, sep } from "node:path";
|
|
7420
|
+
function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
7421
|
+
const workspace = resolve2(workspaceRoot);
|
|
7422
|
+
const candidate = isAbsolute(filePath) ? resolve2(filePath) : resolve2(workspace, filePath);
|
|
7423
|
+
if (candidate !== workspace && !candidate.startsWith(workspace + sep)) {
|
|
7424
|
+
throw new Error(
|
|
7425
|
+
`[claude-crap] Refusing to access '${filePath}' \u2014 path escapes the workspace root`
|
|
7426
|
+
);
|
|
7427
|
+
}
|
|
7428
|
+
return candidate;
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
// src/dashboard/file-detail.ts
|
|
7432
|
+
async function buildFileDetail(input) {
|
|
7433
|
+
const { relativePath, workspaceRoot, astEngine, sarifStore, cyclomaticMax } = input;
|
|
7434
|
+
const absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
|
|
7435
|
+
const source = await fs2.readFile(absolutePath, "utf8");
|
|
7436
|
+
const sourceLines = source.split(/\r?\n/);
|
|
7437
|
+
if (sourceLines.length > 0 && sourceLines[sourceLines.length - 1] === "") {
|
|
7438
|
+
sourceLines.pop();
|
|
7439
|
+
}
|
|
7440
|
+
const physicalLoc = sourceLines.length;
|
|
7441
|
+
let logicalLoc = 0;
|
|
7442
|
+
for (const line of sourceLines) {
|
|
7443
|
+
if (line.trim().length > 0) logicalLoc += 1;
|
|
7444
|
+
}
|
|
7445
|
+
const language = detectLanguageFromPath(relativePath);
|
|
7446
|
+
let functions = [];
|
|
7447
|
+
if (language && astEngine) {
|
|
7448
|
+
try {
|
|
7449
|
+
const metrics = await astEngine.analyzeFile({
|
|
7450
|
+
filePath: absolutePath,
|
|
7451
|
+
language
|
|
7452
|
+
});
|
|
7453
|
+
functions = metrics.functions.map((fn) => ({
|
|
7454
|
+
name: fn.name,
|
|
7455
|
+
startLine: fn.startLine,
|
|
7456
|
+
endLine: fn.endLine,
|
|
7457
|
+
cyclomaticComplexity: fn.cyclomaticComplexity,
|
|
7458
|
+
lineCount: fn.lineCount
|
|
7459
|
+
}));
|
|
7460
|
+
} catch {
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
const allFindings = sarifStore.list();
|
|
7464
|
+
const fileFindings = allFindings.filter(
|
|
7465
|
+
(f) => f.location.uri === relativePath
|
|
7466
|
+
);
|
|
7467
|
+
const findings = fileFindings.map((f) => ({
|
|
7468
|
+
ruleId: f.ruleId,
|
|
7469
|
+
level: f.level,
|
|
7470
|
+
message: f.message,
|
|
7471
|
+
sourceTool: f.sourceTool,
|
|
7472
|
+
startLine: f.location.startLine,
|
|
7473
|
+
startColumn: f.location.startColumn,
|
|
7474
|
+
endLine: f.location.endLine ?? f.location.startLine,
|
|
7475
|
+
endColumn: f.location.endColumn ?? 0,
|
|
7476
|
+
effortMinutes: typeof f.properties?.effortMinutes === "number" ? f.properties.effortMinutes : 0
|
|
7477
|
+
}));
|
|
7478
|
+
let errorCount = 0;
|
|
7479
|
+
let warningCount = 0;
|
|
7480
|
+
let noteCount = 0;
|
|
7481
|
+
let totalEffortMinutes = 0;
|
|
7482
|
+
for (const f of findings) {
|
|
7483
|
+
if (f.level === "error") errorCount += 1;
|
|
7484
|
+
else if (f.level === "warning") warningCount += 1;
|
|
7485
|
+
else if (f.level === "note") noteCount += 1;
|
|
7486
|
+
totalEffortMinutes += f.effortMinutes;
|
|
7487
|
+
}
|
|
7488
|
+
const complexities = functions.map((f) => f.cyclomaticComplexity);
|
|
7489
|
+
const maxComplexity = complexities.length > 0 ? Math.max(...complexities) : 0;
|
|
7490
|
+
const avgComplexity = complexities.length > 0 ? Math.round(
|
|
7491
|
+
complexities.reduce((a, b) => a + b, 0) / complexities.length * 100
|
|
7492
|
+
) / 100 : 0;
|
|
7493
|
+
return {
|
|
7494
|
+
filePath: relativePath,
|
|
7495
|
+
language,
|
|
7496
|
+
physicalLoc,
|
|
7497
|
+
logicalLoc,
|
|
7498
|
+
cyclomaticMax,
|
|
7499
|
+
sourceLines,
|
|
7500
|
+
functions,
|
|
7501
|
+
findings,
|
|
7502
|
+
summary: {
|
|
7503
|
+
totalFindings: findings.length,
|
|
7504
|
+
errorCount,
|
|
7505
|
+
warningCount,
|
|
7506
|
+
noteCount,
|
|
7507
|
+
totalEffortMinutes,
|
|
7508
|
+
avgComplexity,
|
|
7509
|
+
maxComplexity
|
|
7510
|
+
}
|
|
7511
|
+
};
|
|
7512
|
+
}
|
|
7513
|
+
|
|
7406
7514
|
// src/dashboard/server.ts
|
|
7407
7515
|
async function startDashboard(options) {
|
|
7408
7516
|
const { config, sarifStore, workspaceStatsProvider, logger: logger2 } = options;
|
|
@@ -7416,13 +7524,45 @@ async function startDashboard(options) {
|
|
|
7416
7524
|
root: publicRoot,
|
|
7417
7525
|
prefix: "/"
|
|
7418
7526
|
});
|
|
7419
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
7527
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.7" }));
|
|
7420
7528
|
fastify.get("/api/score", async () => {
|
|
7421
7529
|
const stats = await workspaceStatsProvider();
|
|
7422
7530
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
7423
7531
|
return score;
|
|
7424
7532
|
});
|
|
7425
7533
|
fastify.get("/api/sarif", async () => sarifStore.toSarifDocument());
|
|
7534
|
+
fastify.get("/api/complexity", async () => {
|
|
7535
|
+
if (!options.astEngine) {
|
|
7536
|
+
return { threshold: config.cyclomaticMax, totalFunctions: 0, violationCount: 0, topFunctions: [] };
|
|
7537
|
+
}
|
|
7538
|
+
return buildComplexityReport(config, options.astEngine, logger2);
|
|
7539
|
+
});
|
|
7540
|
+
fastify.get("/api/file-detail", async (request, reply) => {
|
|
7541
|
+
const { path: filePath } = request.query;
|
|
7542
|
+
if (!filePath) {
|
|
7543
|
+
return reply.status(400).send({ error: "Missing required query parameter: path" });
|
|
7544
|
+
}
|
|
7545
|
+
try {
|
|
7546
|
+
const detail = await buildFileDetail({
|
|
7547
|
+
relativePath: filePath,
|
|
7548
|
+
workspaceRoot: config.pluginRoot,
|
|
7549
|
+
astEngine: options.astEngine,
|
|
7550
|
+
sarifStore,
|
|
7551
|
+
cyclomaticMax: config.cyclomaticMax
|
|
7552
|
+
});
|
|
7553
|
+
return detail;
|
|
7554
|
+
} catch (err) {
|
|
7555
|
+
const msg = err.message;
|
|
7556
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
7557
|
+
return reply.status(404).send({ error: `File not found: ${filePath}` });
|
|
7558
|
+
}
|
|
7559
|
+
if (msg.includes("escapes the workspace")) {
|
|
7560
|
+
return reply.status(400).send({ error: msg });
|
|
7561
|
+
}
|
|
7562
|
+
logger2.error({ err: msg, filePath }, "file-detail endpoint error");
|
|
7563
|
+
return reply.status(500).send({ error: "Internal server error" });
|
|
7564
|
+
}
|
|
7565
|
+
});
|
|
7426
7566
|
fastify.get("/", async (_request, reply) => {
|
|
7427
7567
|
return reply.sendFile("index.html");
|
|
7428
7568
|
});
|
|
@@ -7444,19 +7584,19 @@ async function resolvePublicRoot(logger2) {
|
|
|
7444
7584
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
7445
7585
|
const candidates = [
|
|
7446
7586
|
// 0. Bundled layout: plugin/bundle/mcp-server.mjs → ./dashboard/public
|
|
7447
|
-
|
|
7587
|
+
resolve3(here, "dashboard", "public"),
|
|
7448
7588
|
// 1. Compiled layout: dist/dashboard/server.js → ./public next to it
|
|
7449
7589
|
// (only present if a build step copies the assets — not used
|
|
7450
7590
|
// today, but accepted so a future copy step does not break us).
|
|
7451
|
-
|
|
7591
|
+
resolve3(here, "public"),
|
|
7452
7592
|
// 2. Source-relative layout: dist/dashboard/server.js → ../../src/dashboard/public
|
|
7453
7593
|
// This is the default — no copy step required because we resolve
|
|
7454
7594
|
// upward from `dist/` into `src/` at runtime.
|
|
7455
|
-
|
|
7595
|
+
resolve3(here, "..", "..", "src", "dashboard", "public")
|
|
7456
7596
|
];
|
|
7457
7597
|
for (const candidate of candidates) {
|
|
7458
7598
|
try {
|
|
7459
|
-
await
|
|
7599
|
+
await fs3.access(resolve3(candidate, "index.html"));
|
|
7460
7600
|
return candidate;
|
|
7461
7601
|
} catch {
|
|
7462
7602
|
}
|
|
@@ -7541,6 +7681,73 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
|
|
|
7541
7681
|
removePidFile(pidFilePath);
|
|
7542
7682
|
await new Promise((r) => setTimeout(r, 300));
|
|
7543
7683
|
}
|
|
7684
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
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) {
|
|
7701
|
+
const threshold = config.cyclomaticMax;
|
|
7702
|
+
const allFunctions = [];
|
|
7703
|
+
let totalFunctions = 0;
|
|
7704
|
+
async function walk2(dir) {
|
|
7705
|
+
let entries;
|
|
7706
|
+
try {
|
|
7707
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7708
|
+
} catch {
|
|
7709
|
+
return;
|
|
7710
|
+
}
|
|
7711
|
+
for (const entry of entries) {
|
|
7712
|
+
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
7713
|
+
const full = join2(dir, entry.name);
|
|
7714
|
+
if (entry.isDirectory()) {
|
|
7715
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
7716
|
+
await walk2(full);
|
|
7717
|
+
continue;
|
|
7718
|
+
}
|
|
7719
|
+
if (!entry.isFile()) continue;
|
|
7720
|
+
const language = detectLanguageFromPath(entry.name);
|
|
7721
|
+
if (!language) continue;
|
|
7722
|
+
try {
|
|
7723
|
+
const metrics = await engine.analyzeFile({ filePath: full, language });
|
|
7724
|
+
for (const fn of metrics.functions) {
|
|
7725
|
+
totalFunctions += 1;
|
|
7726
|
+
allFunctions.push({
|
|
7727
|
+
filePath: full.startsWith(config.pluginRoot) ? full.substring(config.pluginRoot.length + 1) : full,
|
|
7728
|
+
name: fn.name,
|
|
7729
|
+
cyclomaticComplexity: fn.cyclomaticComplexity,
|
|
7730
|
+
startLine: fn.startLine,
|
|
7731
|
+
endLine: fn.endLine,
|
|
7732
|
+
lineCount: fn.lineCount
|
|
7733
|
+
});
|
|
7734
|
+
}
|
|
7735
|
+
} catch (err) {
|
|
7736
|
+
logger2.warn(
|
|
7737
|
+
{ filePath: full, err: err.message },
|
|
7738
|
+
"complexity-report: failed to analyze file"
|
|
7739
|
+
);
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
await walk2(config.pluginRoot);
|
|
7744
|
+
allFunctions.sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
|
|
7745
|
+
const topFunctions = allFunctions.slice(0, 20);
|
|
7746
|
+
const violationCount = allFunctions.filter(
|
|
7747
|
+
(f) => f.cyclomaticComplexity > threshold
|
|
7748
|
+
).length;
|
|
7749
|
+
return { threshold, totalFunctions, violationCount, topFunctions };
|
|
7750
|
+
}
|
|
7544
7751
|
async function buildScore(config, sarifStore, workspace, dashboardUrl) {
|
|
7545
7752
|
return computeProjectScore({
|
|
7546
7753
|
workspaceRoot: config.pluginRoot,
|
|
@@ -7583,9 +7790,9 @@ function computeCrap(input, threshold) {
|
|
|
7583
7790
|
}
|
|
7584
7791
|
|
|
7585
7792
|
// src/metrics/workspace-walker.ts
|
|
7586
|
-
import { promises as
|
|
7793
|
+
import { promises as fs4 } from "node:fs";
|
|
7587
7794
|
import { join as join3 } from "node:path";
|
|
7588
|
-
var
|
|
7795
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
7589
7796
|
"node_modules",
|
|
7590
7797
|
".git",
|
|
7591
7798
|
"dist",
|
|
@@ -7632,7 +7839,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7632
7839
|
if (truncated) return;
|
|
7633
7840
|
let entries;
|
|
7634
7841
|
try {
|
|
7635
|
-
entries = await
|
|
7842
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
7636
7843
|
} catch {
|
|
7637
7844
|
return;
|
|
7638
7845
|
}
|
|
@@ -7641,7 +7848,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7641
7848
|
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
7642
7849
|
const full = join3(dir, entry.name);
|
|
7643
7850
|
if (entry.isDirectory()) {
|
|
7644
|
-
if (
|
|
7851
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
7645
7852
|
await walk2(full);
|
|
7646
7853
|
continue;
|
|
7647
7854
|
}
|
|
@@ -7657,7 +7864,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7657
7864
|
return;
|
|
7658
7865
|
}
|
|
7659
7866
|
try {
|
|
7660
|
-
const content = await
|
|
7867
|
+
const content = await fs4.readFile(full, "utf8");
|
|
7661
7868
|
if (content.length > 0) {
|
|
7662
7869
|
const lines = content.split(/\r?\n/).length;
|
|
7663
7870
|
physicalLoc += content.endsWith("\n") ? lines - 1 : lines;
|
|
@@ -7671,8 +7878,8 @@ async function estimateWorkspaceLoc(workspaceRoot) {
|
|
|
7671
7878
|
}
|
|
7672
7879
|
|
|
7673
7880
|
// src/sarif/sarif-store.ts
|
|
7674
|
-
import { promises as
|
|
7675
|
-
import { dirname as dirname3, isAbsolute, join as join4, resolve as
|
|
7881
|
+
import { promises as fs5 } from "node:fs";
|
|
7882
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
|
|
7676
7883
|
|
|
7677
7884
|
// src/sarif/sarif-builder.ts
|
|
7678
7885
|
function buildSarifDocument(tool, findings) {
|
|
@@ -7734,7 +7941,7 @@ var SarifStore = class {
|
|
|
7734
7941
|
/** Tool invocations we have already ingested, for telemetry. */
|
|
7735
7942
|
toolInvocations = 0;
|
|
7736
7943
|
constructor(options) {
|
|
7737
|
-
const dir =
|
|
7944
|
+
const dir = isAbsolute2(options.outputDir) ? options.outputDir : resolve4(options.workspaceRoot, options.outputDir);
|
|
7738
7945
|
this.filePath = join4(dir, options.fileName ?? "latest.sarif");
|
|
7739
7946
|
}
|
|
7740
7947
|
/**
|
|
@@ -7759,7 +7966,7 @@ var SarifStore = class {
|
|
|
7759
7966
|
*/
|
|
7760
7967
|
async loadLatest() {
|
|
7761
7968
|
try {
|
|
7762
|
-
const raw = await
|
|
7969
|
+
const raw = await fs5.readFile(this.filePath, "utf8");
|
|
7763
7970
|
const parsed = JSON.parse(raw);
|
|
7764
7971
|
if (parsed.version !== "2.1.0") {
|
|
7765
7972
|
throw new Error(`Expected SARIF 2.1.0, got ${parsed.version}`);
|
|
@@ -7861,10 +8068,10 @@ var SarifStore = class {
|
|
|
7861
8068
|
*/
|
|
7862
8069
|
async persist() {
|
|
7863
8070
|
const doc = this.toSarifDocument();
|
|
7864
|
-
await
|
|
8071
|
+
await fs5.mkdir(dirname3(this.filePath), { recursive: true });
|
|
7865
8072
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
7866
|
-
await
|
|
7867
|
-
await
|
|
8073
|
+
await fs5.writeFile(tmp, JSON.stringify(doc, null, 2), "utf8");
|
|
8074
|
+
await fs5.rename(tmp, this.filePath);
|
|
7868
8075
|
}
|
|
7869
8076
|
/**
|
|
7870
8077
|
* Build the current consolidated SARIF document from the in-memory
|
|
@@ -8080,22 +8287,22 @@ function isStrictness(value) {
|
|
|
8080
8287
|
}
|
|
8081
8288
|
|
|
8082
8289
|
// src/tools/test-harness.ts
|
|
8083
|
-
import { promises as
|
|
8084
|
-
import { basename, dirname as dirname4, extname, isAbsolute as
|
|
8290
|
+
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";
|
|
8085
8292
|
var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
|
|
8086
8293
|
function isTestFile(filePath) {
|
|
8087
8294
|
const base = basename(filePath);
|
|
8088
8295
|
if (TEST_SUFFIX_PATTERN.test(base)) return true;
|
|
8089
8296
|
if (base.startsWith("test_") && base.endsWith(".py")) return true;
|
|
8090
|
-
const parts = filePath.split(
|
|
8297
|
+
const parts = filePath.split(sep2);
|
|
8091
8298
|
return parts.includes("__tests__") || parts.includes("tests") || parts.includes("test");
|
|
8092
8299
|
}
|
|
8093
8300
|
function candidatePaths(workspaceRoot, filePath) {
|
|
8094
|
-
const absSource =
|
|
8301
|
+
const absSource = resolve5(filePath);
|
|
8095
8302
|
const ext = extname(absSource);
|
|
8096
8303
|
const base = basename(absSource, ext);
|
|
8097
8304
|
const dir = dirname4(absSource);
|
|
8098
|
-
const absWorkspace =
|
|
8305
|
+
const absWorkspace = resolve5(workspaceRoot);
|
|
8099
8306
|
const relFromRoot = relative(absWorkspace, absSource);
|
|
8100
8307
|
const relDir = dirname4(relFromRoot);
|
|
8101
8308
|
const candidates = /* @__PURE__ */ new Set();
|
|
@@ -8128,14 +8335,14 @@ function candidatePaths(workspaceRoot, filePath) {
|
|
|
8128
8335
|
return Array.from(candidates);
|
|
8129
8336
|
}
|
|
8130
8337
|
async function findTestFile(workspaceRoot, filePath) {
|
|
8131
|
-
const absolute =
|
|
8338
|
+
const absolute = isAbsolute3(filePath) ? filePath : resolve5(workspaceRoot, filePath);
|
|
8132
8339
|
if (isTestFile(absolute)) {
|
|
8133
8340
|
return { testFile: absolute, candidates: [absolute], isTestFile: true };
|
|
8134
8341
|
}
|
|
8135
8342
|
const candidates = candidatePaths(workspaceRoot, absolute);
|
|
8136
8343
|
for (const candidate of candidates) {
|
|
8137
8344
|
try {
|
|
8138
|
-
await
|
|
8345
|
+
await fs6.access(candidate);
|
|
8139
8346
|
return { testFile: candidate, candidates, isTestFile: false };
|
|
8140
8347
|
} catch {
|
|
8141
8348
|
}
|
|
@@ -8143,22 +8350,9 @@ async function findTestFile(workspaceRoot, filePath) {
|
|
|
8143
8350
|
return { testFile: null, candidates, isTestFile: false };
|
|
8144
8351
|
}
|
|
8145
8352
|
|
|
8146
|
-
// src/workspace-guard.ts
|
|
8147
|
-
import { isAbsolute as isAbsolute3, resolve as resolve5, sep as sep2 } from "node:path";
|
|
8148
|
-
function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
8149
|
-
const workspace = resolve5(workspaceRoot);
|
|
8150
|
-
const candidate = isAbsolute3(filePath) ? resolve5(filePath) : resolve5(workspace, filePath);
|
|
8151
|
-
if (candidate !== workspace && !candidate.startsWith(workspace + sep2)) {
|
|
8152
|
-
throw new Error(
|
|
8153
|
-
`[claude-crap] Refusing to access '${filePath}' \u2014 path escapes the workspace root`
|
|
8154
|
-
);
|
|
8155
|
-
}
|
|
8156
|
-
return candidate;
|
|
8157
|
-
}
|
|
8158
|
-
|
|
8159
8353
|
// src/scanner/auto-scan.ts
|
|
8160
8354
|
import { existsSync as existsSync5 } from "node:fs";
|
|
8161
|
-
import { join as
|
|
8355
|
+
import { join as join11 } from "node:path";
|
|
8162
8356
|
|
|
8163
8357
|
// src/scanner/detector.ts
|
|
8164
8358
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -8700,6 +8894,135 @@ function buildResult(projectType, steps, autoScanResult, recommendation) {
|
|
|
8700
8894
|
};
|
|
8701
8895
|
}
|
|
8702
8896
|
|
|
8897
|
+
// src/scanner/complexity-scanner.ts
|
|
8898
|
+
import { promises as fs7 } from "node:fs";
|
|
8899
|
+
import { join as join10, relative as relative2 } from "node:path";
|
|
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
|
+
]);
|
|
8916
|
+
var MAX_FILES = 2e4;
|
|
8917
|
+
var RULE_ID = "complexity/cyclomatic-max";
|
|
8918
|
+
var SOURCE_TOOL = "complexity";
|
|
8919
|
+
async function scanComplexity(workspaceRoot, engine, sarifStore, config, logger2) {
|
|
8920
|
+
const start = Date.now();
|
|
8921
|
+
const threshold = config.cyclomaticMax;
|
|
8922
|
+
const errorThreshold = threshold * 2;
|
|
8923
|
+
const files = await collectSourceFiles(workspaceRoot);
|
|
8924
|
+
logger2.info(
|
|
8925
|
+
{ fileCount: files.length, threshold },
|
|
8926
|
+
"complexity-scanner: starting analysis"
|
|
8927
|
+
);
|
|
8928
|
+
const sarifResults = [];
|
|
8929
|
+
let filesScanned = 0;
|
|
8930
|
+
let functionsAnalyzed = 0;
|
|
8931
|
+
let violations = 0;
|
|
8932
|
+
for (const filePath of files) {
|
|
8933
|
+
const language = detectLanguageFromPath(filePath);
|
|
8934
|
+
if (!language) continue;
|
|
8935
|
+
try {
|
|
8936
|
+
const metrics = await engine.analyzeFile({ filePath, language });
|
|
8937
|
+
filesScanned += 1;
|
|
8938
|
+
functionsAnalyzed += metrics.functions.length;
|
|
8939
|
+
for (const fn of metrics.functions) {
|
|
8940
|
+
if (fn.cyclomaticComplexity <= threshold) continue;
|
|
8941
|
+
const level = fn.cyclomaticComplexity >= errorThreshold ? "error" : "warning";
|
|
8942
|
+
const relPath = relative2(workspaceRoot, filePath);
|
|
8943
|
+
sarifResults.push({
|
|
8944
|
+
ruleId: RULE_ID,
|
|
8945
|
+
level,
|
|
8946
|
+
message: {
|
|
8947
|
+
text: `Function '${fn.name}' has cyclomatic complexity ${fn.cyclomaticComplexity} (threshold: ${threshold})`
|
|
8948
|
+
},
|
|
8949
|
+
locations: [
|
|
8950
|
+
{
|
|
8951
|
+
physicalLocation: {
|
|
8952
|
+
artifactLocation: { uri: relPath },
|
|
8953
|
+
region: {
|
|
8954
|
+
startLine: fn.startLine,
|
|
8955
|
+
startColumn: 1,
|
|
8956
|
+
endLine: fn.endLine,
|
|
8957
|
+
endColumn: 1
|
|
8958
|
+
}
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
],
|
|
8962
|
+
properties: {
|
|
8963
|
+
sourceTool: SOURCE_TOOL,
|
|
8964
|
+
effortMinutes: estimateEffortMinutes(level),
|
|
8965
|
+
cyclomaticComplexity: fn.cyclomaticComplexity
|
|
8966
|
+
}
|
|
8967
|
+
});
|
|
8968
|
+
violations += 1;
|
|
8969
|
+
}
|
|
8970
|
+
} catch (err) {
|
|
8971
|
+
logger2.warn(
|
|
8972
|
+
{ filePath, err: err.message },
|
|
8973
|
+
"complexity-scanner: failed to analyze file, skipping"
|
|
8974
|
+
);
|
|
8975
|
+
}
|
|
8976
|
+
}
|
|
8977
|
+
if (sarifResults.length > 0) {
|
|
8978
|
+
const document = wrapResultsInSarif(
|
|
8979
|
+
SOURCE_TOOL,
|
|
8980
|
+
"0.1.0",
|
|
8981
|
+
sarifResults
|
|
8982
|
+
);
|
|
8983
|
+
sarifStore.ingestRun(document, SOURCE_TOOL);
|
|
8984
|
+
await sarifStore.persist();
|
|
8985
|
+
}
|
|
8986
|
+
const durationMs = Date.now() - start;
|
|
8987
|
+
logger2.info(
|
|
8988
|
+
{ filesScanned, functionsAnalyzed, violations, durationMs },
|
|
8989
|
+
"complexity-scanner: analysis complete"
|
|
8990
|
+
);
|
|
8991
|
+
return { filesScanned, functionsAnalyzed, violations, durationMs };
|
|
8992
|
+
}
|
|
8993
|
+
async function collectSourceFiles(workspaceRoot) {
|
|
8994
|
+
const files = [];
|
|
8995
|
+
let truncated = false;
|
|
8996
|
+
async function walk2(dir) {
|
|
8997
|
+
if (truncated) return;
|
|
8998
|
+
let entries;
|
|
8999
|
+
try {
|
|
9000
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
9001
|
+
} catch {
|
|
9002
|
+
return;
|
|
9003
|
+
}
|
|
9004
|
+
for (const entry of entries) {
|
|
9005
|
+
if (truncated) return;
|
|
9006
|
+
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
9007
|
+
const full = join10(dir, entry.name);
|
|
9008
|
+
if (entry.isDirectory()) {
|
|
9009
|
+
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
9010
|
+
await walk2(full);
|
|
9011
|
+
continue;
|
|
9012
|
+
}
|
|
9013
|
+
if (!entry.isFile()) continue;
|
|
9014
|
+
if (!detectLanguageFromPath(entry.name)) continue;
|
|
9015
|
+
files.push(full);
|
|
9016
|
+
if (files.length >= MAX_FILES) {
|
|
9017
|
+
truncated = true;
|
|
9018
|
+
return;
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
}
|
|
9022
|
+
await walk2(workspaceRoot);
|
|
9023
|
+
return files;
|
|
9024
|
+
}
|
|
9025
|
+
|
|
8703
9026
|
// src/scanner/auto-scan.ts
|
|
8704
9027
|
function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
8705
9028
|
let parsed;
|
|
@@ -8712,7 +9035,7 @@ function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
|
8712
9035
|
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
8713
9036
|
return { accepted: stats.accepted };
|
|
8714
9037
|
}
|
|
8715
|
-
async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
9038
|
+
async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
8716
9039
|
const start = Date.now();
|
|
8717
9040
|
const detected = await detectScanners(workspaceRoot);
|
|
8718
9041
|
const available = detected.filter((d) => d.available);
|
|
@@ -8737,7 +9060,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8737
9060
|
".eslintrc.json"
|
|
8738
9061
|
];
|
|
8739
9062
|
const eslintDetected = available.some((d) => d.scanner === "eslint");
|
|
8740
|
-
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(
|
|
9063
|
+
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(join11(workspaceRoot, f)));
|
|
8741
9064
|
if (eslintDetected && !hasEslintConfig) {
|
|
8742
9065
|
logger2.info("auto-scan: ESLint detected but no config \u2014 running bootstrap");
|
|
8743
9066
|
try {
|
|
@@ -8847,11 +9170,30 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8847
9170
|
if (persistNeeded) {
|
|
8848
9171
|
await sarifStore.persist();
|
|
8849
9172
|
}
|
|
9173
|
+
let complexityScan;
|
|
9174
|
+
if (options?.engine) {
|
|
9175
|
+
try {
|
|
9176
|
+
complexityScan = await scanComplexity(
|
|
9177
|
+
workspaceRoot,
|
|
9178
|
+
options.engine,
|
|
9179
|
+
sarifStore,
|
|
9180
|
+
{ cyclomaticMax: options.cyclomaticMax ?? 15 },
|
|
9181
|
+
logger2
|
|
9182
|
+
);
|
|
9183
|
+
totalFindings += complexityScan.violations;
|
|
9184
|
+
} catch (err) {
|
|
9185
|
+
logger2.warn(
|
|
9186
|
+
{ err: err.message },
|
|
9187
|
+
"auto-scan: complexity scanner failed \u2014 continuing without it"
|
|
9188
|
+
);
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
8850
9191
|
return {
|
|
8851
9192
|
detected,
|
|
8852
9193
|
results,
|
|
8853
9194
|
totalFindings,
|
|
8854
|
-
totalDurationMs: Date.now() - start
|
|
9195
|
+
totalDurationMs: Date.now() - start,
|
|
9196
|
+
...complexityScan ? { complexityScan } : {}
|
|
8855
9197
|
};
|
|
8856
9198
|
}
|
|
8857
9199
|
|
|
@@ -9047,7 +9389,8 @@ async function main() {
|
|
|
9047
9389
|
config,
|
|
9048
9390
|
sarifStore,
|
|
9049
9391
|
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
|
|
9050
|
-
logger
|
|
9392
|
+
logger,
|
|
9393
|
+
astEngine
|
|
9051
9394
|
});
|
|
9052
9395
|
} catch (err) {
|
|
9053
9396
|
logger.warn(
|
|
@@ -9438,7 +9781,10 @@ async function main() {
|
|
|
9438
9781
|
case "auto_scan": {
|
|
9439
9782
|
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
9440
9783
|
try {
|
|
9441
|
-
const result = await autoScan(config.pluginRoot, sarifStore, logger
|
|
9784
|
+
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
9785
|
+
engine: astEngine,
|
|
9786
|
+
cyclomaticMax: config.cyclomaticMax
|
|
9787
|
+
});
|
|
9442
9788
|
const markdown = renderAutoScanMarkdown(result);
|
|
9443
9789
|
return {
|
|
9444
9790
|
content: [
|
|
@@ -9514,7 +9860,10 @@ async function main() {
|
|
|
9514
9860
|
const transport = new StdioServerTransport();
|
|
9515
9861
|
await server.connect(transport);
|
|
9516
9862
|
logger.info("claude-crap MCP server ready (stdio)");
|
|
9517
|
-
autoScan(config.pluginRoot, sarifStore, logger
|
|
9863
|
+
autoScan(config.pluginRoot, sarifStore, logger, {
|
|
9864
|
+
engine: astEngine,
|
|
9865
|
+
cyclomaticMax: config.cyclomaticMax
|
|
9866
|
+
}).then((result) => {
|
|
9518
9867
|
const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
|
|
9519
9868
|
logger.info(
|
|
9520
9869
|
{
|
|
@@ -9581,6 +9930,15 @@ function renderAutoScanMarkdown(result) {
|
|
|
9581
9930
|
}
|
|
9582
9931
|
lines.push("");
|
|
9583
9932
|
}
|
|
9933
|
+
if (result.complexityScan) {
|
|
9934
|
+
const cs = result.complexityScan;
|
|
9935
|
+
lines.push("### Cyclomatic complexity scan\n");
|
|
9936
|
+
lines.push(`- Files scanned: **${cs.filesScanned}**`);
|
|
9937
|
+
lines.push(`- Functions analyzed: **${cs.functionsAnalyzed}**`);
|
|
9938
|
+
lines.push(`- Violations: **${cs.violations}**`);
|
|
9939
|
+
lines.push(`- Duration: ${(cs.durationMs / 1e3).toFixed(1)}s`);
|
|
9940
|
+
lines.push("");
|
|
9941
|
+
}
|
|
9584
9942
|
lines.push(
|
|
9585
9943
|
`**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1e3).toFixed(1)}s`
|
|
9586
9944
|
);
|