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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
@@ -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.1.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
- // / — static SPA fallback (Fastify-static handles index.html)
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}`;
@@ -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: [],
@@ -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 { autoScan, type AutoScanResult } from "./auto-scan.js";
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: ["dist/", "node_modules/", "coverage/"],
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: ["dist/", "node_modules/", "coverage/"],
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 (available.length > 0) {
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 packages = isTypeScript
325
- ? ["eslint", "@eslint/js", "typescript-eslint"]
326
- : ["eslint", "@eslint/js"];
327
-
328
- const installStep = await npmInstall(workspaceRoot, packages);
329
- steps.push(installStep);
330
-
331
- if (installStep.success) {
332
- const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
333
- steps.push(configStep);
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 auto_scan if installation succeeded
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
- autoScanResult = await autoScan(workspaceRoot, sarifStore, logger);
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: auto_scan after install failed",
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 scannerInstalled = recommendation.canAutoInstall && installSucceeded;
456
+ const scanner = recommendation?.scanner ?? "unknown";
362
457
 
363
458
  let summary: string;
364
- if (scannerInstalled && autoScanResult) {
365
- summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan found ${findings} finding(s).`;
366
- } else if (scannerInstalled) {
367
- summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan did not run.`;
368
- } else if (!recommendation.canAutoInstall) {
369
- summary = `Detected ${projectType} project. Install ${recommendation.scanner} manually: ${recommendation.installInstructions}`;
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 install ${recommendation.scanner}. Check the error details in the steps.`;
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: installSucceeded,
475
+ success,
381
476
  summary,
382
477
  };
383
478
  }
@@ -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
- if (err && !cmd.nonZeroIsNormal) {
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 {