claude-crap 0.3.0 → 0.3.3
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 +45 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +8 -3
- package/dist/dashboard/server.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +36 -0
- 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 +111 -26
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +7 -2
- package/dist/scanner/runner.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/mcp-server.mjs +259 -138
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package.json +1 -1
- package/src/dashboard/server.ts +8 -3
- package/src/scanner/auto-scan.ts +43 -0
- package/src/scanner/bootstrap.ts +123 -28
- package/src/scanner/runner.ts +8 -2
package/plugin/package.json
CHANGED
package/src/dashboard/server.ts
CHANGED
|
@@ -94,13 +94,12 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
94
94
|
await fastify.register(fastifyStatic, {
|
|
95
95
|
root: publicRoot,
|
|
96
96
|
prefix: "/",
|
|
97
|
-
decorateReply: false,
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
// ------------------------------------------------------------------
|
|
101
100
|
// /api/health — liveness probe
|
|
102
101
|
// ------------------------------------------------------------------
|
|
103
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
102
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.2" }));
|
|
104
103
|
|
|
105
104
|
// ------------------------------------------------------------------
|
|
106
105
|
// /api/score — live project score
|
|
@@ -117,8 +116,14 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
117
116
|
fastify.get("/api/sarif", async () => sarifStore.toSarifDocument());
|
|
118
117
|
|
|
119
118
|
// ------------------------------------------------------------------
|
|
120
|
-
// / —
|
|
119
|
+
// / — explicit SPA fallback for index.html
|
|
121
120
|
// ------------------------------------------------------------------
|
|
121
|
+
// @fastify/static sometimes doesn't serve index.html on GET / when
|
|
122
|
+
// API routes are registered on the same prefix. Explicit fallback
|
|
123
|
+
// ensures the dashboard always loads.
|
|
124
|
+
fastify.get("/", async (_request, reply) => {
|
|
125
|
+
return reply.sendFile("index.html");
|
|
126
|
+
});
|
|
122
127
|
|
|
123
128
|
await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
|
|
124
129
|
const url = `http://127.0.0.1:${config.dashboardPort}`;
|
package/src/scanner/auto-scan.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* @module scanner/auto-scan
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
22
24
|
import type { Logger } from "pino";
|
|
23
25
|
import { detectScanners, type ScannerDetection } from "./detector.js";
|
|
24
26
|
import { runScanner, type ScannerRunResult } from "./runner.js";
|
|
27
|
+
import { bootstrapScanner } from "./bootstrap.js";
|
|
25
28
|
import { adaptScannerOutput, type KnownScanner } from "../adapters/index.js";
|
|
26
29
|
import type { SarifStore } from "../sarif/sarif-store.js";
|
|
27
30
|
|
|
@@ -105,7 +108,47 @@ export async function autoScan(
|
|
|
105
108
|
"auto-scan: detection complete",
|
|
106
109
|
);
|
|
107
110
|
|
|
111
|
+
// If ESLint is detected (e.g. in package.json) but has no config file,
|
|
112
|
+
// bootstrap will create one before we try to scan.
|
|
113
|
+
const eslintConfigFiles = [
|
|
114
|
+
"eslint.config.js", "eslint.config.mjs", "eslint.config.cjs",
|
|
115
|
+
"eslint.config.ts", "eslint.config.mts", "eslint.config.cts",
|
|
116
|
+
".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml",
|
|
117
|
+
".eslintrc.yml", ".eslintrc.json",
|
|
118
|
+
];
|
|
119
|
+
const eslintDetected = available.some((d) => d.scanner === "eslint");
|
|
120
|
+
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync(join(workspaceRoot, f)));
|
|
121
|
+
|
|
122
|
+
if (eslintDetected && !hasEslintConfig) {
|
|
123
|
+
logger.info("auto-scan: ESLint detected but no config — running bootstrap");
|
|
124
|
+
try {
|
|
125
|
+
const bootstrapResult = await bootstrapScanner(workspaceRoot, sarifStore, logger);
|
|
126
|
+
if (bootstrapResult.autoScanResult) {
|
|
127
|
+
return bootstrapResult.autoScanResult;
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger.warn(
|
|
131
|
+
{ err: (err as Error).message },
|
|
132
|
+
"auto-scan: bootstrap config creation failed",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
if (available.length === 0) {
|
|
138
|
+
// No scanners configured — try to bootstrap one automatically.
|
|
139
|
+
logger.info("auto-scan: no scanners found, attempting bootstrap");
|
|
140
|
+
try {
|
|
141
|
+
const bootstrapResult = await bootstrapScanner(workspaceRoot, sarifStore, logger);
|
|
142
|
+
if (bootstrapResult.autoScanResult) {
|
|
143
|
+
return bootstrapResult.autoScanResult;
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
logger.warn(
|
|
147
|
+
{ err: (err as Error).message },
|
|
148
|
+
"auto-scan: bootstrap failed — continuing with empty results",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
return {
|
|
110
153
|
detected,
|
|
111
154
|
results: [],
|
package/src/scanner/bootstrap.ts
CHANGED
|
@@ -27,8 +27,10 @@ import { join } from "node:path";
|
|
|
27
27
|
import { execFile } from "node:child_process";
|
|
28
28
|
import type { Logger } from "pino";
|
|
29
29
|
import type { KnownScanner } from "../adapters/common.js";
|
|
30
|
+
import { adaptScannerOutput } from "../adapters/index.js";
|
|
30
31
|
import { detectScanners } from "./detector.js";
|
|
31
|
-
import {
|
|
32
|
+
import { runScanner } from "./runner.js";
|
|
33
|
+
import type { AutoScanResult, ScannerResult } from "./auto-scan.js";
|
|
32
34
|
import type { SarifStore } from "../sarif/sarif-store.js";
|
|
33
35
|
|
|
34
36
|
// ── Types ──────────────────────────────────────────────────────────
|
|
@@ -135,7 +137,14 @@ export default tseslint.config(
|
|
|
135
137
|
js.configs.recommended,
|
|
136
138
|
...tseslint.configs.recommended,
|
|
137
139
|
{
|
|
138
|
-
ignores: [
|
|
140
|
+
ignores: [
|
|
141
|
+
"dist/",
|
|
142
|
+
"node_modules/",
|
|
143
|
+
"coverage/",
|
|
144
|
+
"**/bundle/",
|
|
145
|
+
"**/vendor/",
|
|
146
|
+
"**/*.min.js",
|
|
147
|
+
],
|
|
139
148
|
},
|
|
140
149
|
);
|
|
141
150
|
`;
|
|
@@ -146,7 +155,14 @@ export default tseslint.config(
|
|
|
146
155
|
export default [
|
|
147
156
|
js.configs.recommended,
|
|
148
157
|
{
|
|
149
|
-
ignores: [
|
|
158
|
+
ignores: [
|
|
159
|
+
"dist/",
|
|
160
|
+
"node_modules/",
|
|
161
|
+
"coverage/",
|
|
162
|
+
"**/bundle/",
|
|
163
|
+
"**/vendor/",
|
|
164
|
+
"**/*.min.js",
|
|
165
|
+
],
|
|
150
166
|
},
|
|
151
167
|
];
|
|
152
168
|
`;
|
|
@@ -290,7 +306,12 @@ export async function bootstrapScanner(
|
|
|
290
306
|
const detections = await detectScanners(workspaceRoot);
|
|
291
307
|
const available = detections.filter((d) => d.available);
|
|
292
308
|
|
|
293
|
-
if
|
|
309
|
+
// A scanner is truly "configured" only if it also has a config
|
|
310
|
+
// file. ESLint in package.json without eslint.config.mjs will crash.
|
|
311
|
+
const eslintNeedsConfig = available.some((d) => d.scanner === "eslint")
|
|
312
|
+
&& !detections.some((d) => d.scanner === "eslint" && d.configPath);
|
|
313
|
+
|
|
314
|
+
if (available.length > 0 && !eslintNeedsConfig) {
|
|
294
315
|
const existingScanners = available.map((d) => d.scanner);
|
|
295
316
|
logger.info(
|
|
296
317
|
{ existingScanners },
|
|
@@ -317,21 +338,32 @@ export async function bootstrapScanner(
|
|
|
317
338
|
"bootstrap: detected project type",
|
|
318
339
|
);
|
|
319
340
|
|
|
320
|
-
// 3. Install scanner
|
|
341
|
+
// 3. Install scanner (skip npm install if already in package.json)
|
|
321
342
|
if (recommendation.canAutoInstall) {
|
|
322
|
-
// JS/TS: auto-install ESLint
|
|
323
343
|
const isTypeScript = projectType === "typescript";
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
344
|
+
const eslintAlreadyInstalled = available.some((d) => d.scanner === "eslint");
|
|
345
|
+
|
|
346
|
+
if (!eslintAlreadyInstalled) {
|
|
347
|
+
const packages = isTypeScript
|
|
348
|
+
? ["eslint", "@eslint/js", "typescript-eslint"]
|
|
349
|
+
: ["eslint", "@eslint/js"];
|
|
350
|
+
const installStep = await npmInstall(workspaceRoot, packages);
|
|
351
|
+
steps.push(installStep);
|
|
352
|
+
if (!installStep.success) {
|
|
353
|
+
// npm install failed — skip config creation, fall through to result
|
|
354
|
+
return buildResult(projectType, steps, null);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
steps.push({
|
|
358
|
+
action: "npm install eslint",
|
|
359
|
+
success: true,
|
|
360
|
+
detail: "eslint already in package.json — skipped install",
|
|
361
|
+
});
|
|
334
362
|
}
|
|
363
|
+
|
|
364
|
+
// Always create config if missing
|
|
365
|
+
const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
|
|
366
|
+
steps.push(configStep);
|
|
335
367
|
} else {
|
|
336
368
|
// Python / Java / C# / Unknown: return instructions
|
|
337
369
|
steps.push({
|
|
@@ -341,34 +373,97 @@ export async function bootstrapScanner(
|
|
|
341
373
|
});
|
|
342
374
|
}
|
|
343
375
|
|
|
344
|
-
// 4. Run
|
|
376
|
+
// 4. Run scanner directly if installation succeeded (inline scan
|
|
377
|
+
// to avoid circular dependency — autoScan calls bootstrapScanner)
|
|
345
378
|
const installSucceeded = steps.every((s) => s.success);
|
|
346
379
|
let autoScanResult: AutoScanResult | null = null;
|
|
347
380
|
|
|
348
381
|
if (installSucceeded && recommendation.canAutoInstall) {
|
|
349
382
|
try {
|
|
350
|
-
|
|
383
|
+
const scanStart = Date.now();
|
|
384
|
+
const postDetections = await detectScanners(workspaceRoot);
|
|
385
|
+
const postAvailable = postDetections.filter((d) => d.available);
|
|
386
|
+
const scanResults: ScannerResult[] = [];
|
|
387
|
+
let scanFindings = 0;
|
|
388
|
+
|
|
389
|
+
const settled = await Promise.allSettled(
|
|
390
|
+
postAvailable.map((d) => runScanner(d.scanner, workspaceRoot)),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < postAvailable.length; i++) {
|
|
394
|
+
const det = postAvailable[i]!;
|
|
395
|
+
const res = settled[i]!;
|
|
396
|
+
|
|
397
|
+
if (res.status === "rejected" || !res.value.success) {
|
|
398
|
+
scanResults.push({
|
|
399
|
+
scanner: det.scanner,
|
|
400
|
+
success: false,
|
|
401
|
+
findingsIngested: 0,
|
|
402
|
+
durationMs: res.status === "fulfilled" ? res.value.durationMs : 0,
|
|
403
|
+
error: res.status === "rejected"
|
|
404
|
+
? String(res.reason)
|
|
405
|
+
: res.value.error ?? "unknown error",
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const runResult = res.value;
|
|
411
|
+
let parsed: unknown;
|
|
412
|
+
try { parsed = JSON.parse(runResult.rawOutput); } catch { parsed = runResult.rawOutput; }
|
|
413
|
+
const adapted = adaptScannerOutput(runResult.scanner, parsed);
|
|
414
|
+
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
415
|
+
scanFindings += stats.accepted;
|
|
416
|
+
|
|
417
|
+
scanResults.push({
|
|
418
|
+
scanner: runResult.scanner,
|
|
419
|
+
success: true,
|
|
420
|
+
findingsIngested: stats.accepted,
|
|
421
|
+
durationMs: runResult.durationMs,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (scanFindings > 0) await sarifStore.persist();
|
|
426
|
+
|
|
427
|
+
autoScanResult = {
|
|
428
|
+
detected: postDetections,
|
|
429
|
+
results: scanResults,
|
|
430
|
+
totalFindings: scanFindings,
|
|
431
|
+
totalDurationMs: Date.now() - scanStart,
|
|
432
|
+
};
|
|
351
433
|
} catch (err) {
|
|
352
434
|
logger.warn(
|
|
353
435
|
{ err: (err as Error).message },
|
|
354
|
-
"bootstrap:
|
|
436
|
+
"bootstrap: scan after install failed",
|
|
355
437
|
);
|
|
356
438
|
}
|
|
357
439
|
}
|
|
358
440
|
|
|
359
441
|
// 5. Build result
|
|
442
|
+
return buildResult(projectType, steps, autoScanResult, recommendation);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Build a BootstrapResult from the collected steps and optional scan result.
|
|
447
|
+
*/
|
|
448
|
+
function buildResult(
|
|
449
|
+
projectType: ProjectType,
|
|
450
|
+
steps: BootstrapStep[],
|
|
451
|
+
autoScanResult: AutoScanResult | null,
|
|
452
|
+
recommendation?: { scanner: KnownScanner; canAutoInstall: boolean; installInstructions: string },
|
|
453
|
+
): BootstrapResult {
|
|
454
|
+
const success = steps.every((s) => s.success);
|
|
360
455
|
const findings = autoScanResult?.totalFindings ?? 0;
|
|
361
|
-
const
|
|
456
|
+
const scanner = recommendation?.scanner ?? "unknown";
|
|
362
457
|
|
|
363
458
|
let summary: string;
|
|
364
|
-
if (
|
|
365
|
-
summary = `
|
|
366
|
-
} else if (
|
|
367
|
-
summary = `
|
|
368
|
-
} else if (
|
|
369
|
-
summary = `
|
|
459
|
+
if (success && autoScanResult) {
|
|
460
|
+
summary = `Configured ${scanner} for ${projectType} project. Scan found ${findings} finding(s).`;
|
|
461
|
+
} else if (success && recommendation && !recommendation.canAutoInstall) {
|
|
462
|
+
summary = `Detected ${projectType} project. Install ${scanner} manually: ${recommendation.installInstructions}`;
|
|
463
|
+
} else if (success) {
|
|
464
|
+
summary = `Configured ${scanner} for ${projectType} project.`;
|
|
370
465
|
} else {
|
|
371
|
-
summary = `Failed to
|
|
466
|
+
summary = `Failed to configure ${scanner}. Check the error details in the steps.`;
|
|
372
467
|
}
|
|
373
468
|
|
|
374
469
|
return {
|
|
@@ -377,7 +472,7 @@ export async function bootstrapScanner(
|
|
|
377
472
|
existingScanners: [],
|
|
378
473
|
steps,
|
|
379
474
|
autoScanResult,
|
|
380
|
-
success
|
|
475
|
+
success,
|
|
381
476
|
summary,
|
|
382
477
|
};
|
|
383
478
|
}
|
package/src/scanner/runner.ts
CHANGED
|
@@ -124,8 +124,14 @@ export function runScanner(
|
|
|
124
124
|
const durationMs = Date.now() - start;
|
|
125
125
|
|
|
126
126
|
// For scanners where non-zero exit means "findings exist",
|
|
127
|
-
// we still have valid output in stdout.
|
|
128
|
-
|
|
127
|
+
// we still have valid output in stdout. But if the scanner
|
|
128
|
+
// crashed (e.g. ESLint with no config file), treat it as a
|
|
129
|
+
// real failure even when nonZeroIsNormal is set.
|
|
130
|
+
const isFatalError = cmd.nonZeroIsNormal
|
|
131
|
+
&& err
|
|
132
|
+
&& (!stdout?.trim() || stderr?.includes("Oops!") || stderr?.includes("couldn't find"));
|
|
133
|
+
|
|
134
|
+
if (err && (!cmd.nonZeroIsNormal || isFatalError)) {
|
|
129
135
|
// Stryker: check if the output file was written despite the error
|
|
130
136
|
if (cmd.outputFile && existsSync(cmd.outputFile)) {
|
|
131
137
|
try {
|