claude-crap 0.3.3 → 0.3.5

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.
@@ -7235,8 +7235,8 @@ function loadConfig() {
7235
7235
  }
7236
7236
 
7237
7237
  // src/dashboard/server.ts
7238
- import { promises as fs2 } from "node:fs";
7239
- import { dirname as dirname2, resolve as resolve2 } from "node:path";
7238
+ import { promises as fs2, existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
7239
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
7240
7240
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7241
7241
  import Fastify from "fastify";
7242
7242
  import fastifyStatic from "@fastify/static";
@@ -7416,7 +7416,7 @@ async function startDashboard(options) {
7416
7416
  root: publicRoot,
7417
7417
  prefix: "/"
7418
7418
  });
7419
- fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.2" }));
7419
+ fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.5" }));
7420
7420
  fastify.get("/api/score", async () => {
7421
7421
  const stats = await workspaceStatsProvider();
7422
7422
  const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
@@ -7426,12 +7426,16 @@ async function startDashboard(options) {
7426
7426
  fastify.get("/", async (_request, reply) => {
7427
7427
  return reply.sendFile("index.html");
7428
7428
  });
7429
+ const pidFilePath = resolvePidFilePath(config);
7430
+ await killStaleDashboard(pidFilePath, config.dashboardPort, logger2);
7429
7431
  await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
7430
7432
  const url = `http://127.0.0.1:${config.dashboardPort}`;
7431
7433
  logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
7434
+ writePidFile(pidFilePath, config.dashboardPort);
7432
7435
  return {
7433
7436
  url,
7434
7437
  async close() {
7438
+ removePidFile(pidFilePath);
7435
7439
  await fastify.close();
7436
7440
  }
7437
7441
  };
@@ -7471,6 +7475,72 @@ function urlOf(fastify, config) {
7471
7475
  }
7472
7476
  return `http://127.0.0.1:${config.dashboardPort}`;
7473
7477
  }
7478
+ function resolvePidFilePath(config) {
7479
+ return join2(config.pluginRoot, ".claude-crap", "dashboard.pid");
7480
+ }
7481
+ function writePidFile(path, port) {
7482
+ const data = {
7483
+ pid: process.pid,
7484
+ port,
7485
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
7486
+ };
7487
+ try {
7488
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
7489
+ } catch {
7490
+ }
7491
+ }
7492
+ function removePidFile(path) {
7493
+ try {
7494
+ unlinkSync(path);
7495
+ } catch {
7496
+ }
7497
+ }
7498
+ function isPidAlive(pid) {
7499
+ try {
7500
+ process.kill(pid, 0);
7501
+ return true;
7502
+ } catch {
7503
+ return false;
7504
+ }
7505
+ }
7506
+ async function killStaleDashboard(pidFilePath, port, logger2) {
7507
+ if (!existsSync(pidFilePath)) return;
7508
+ let stale;
7509
+ try {
7510
+ stale = JSON.parse(readFileSync(pidFilePath, "utf8"));
7511
+ } catch {
7512
+ removePidFile(pidFilePath);
7513
+ return;
7514
+ }
7515
+ if (!isPidAlive(stale.pid)) {
7516
+ logger2.info({ stalePid: stale.pid }, "stale dashboard PID file found (process dead), removing");
7517
+ removePidFile(pidFilePath);
7518
+ return;
7519
+ }
7520
+ logger2.info(
7521
+ { stalePid: stale.pid, port: stale.port, startedAt: stale.startedAt },
7522
+ "killing stale dashboard process from previous session"
7523
+ );
7524
+ try {
7525
+ process.kill(stale.pid, "SIGTERM");
7526
+ } catch {
7527
+ removePidFile(pidFilePath);
7528
+ return;
7529
+ }
7530
+ for (let i = 0; i < 30; i++) {
7531
+ if (!isPidAlive(stale.pid)) break;
7532
+ await new Promise((r) => setTimeout(r, 100));
7533
+ }
7534
+ if (isPidAlive(stale.pid)) {
7535
+ try {
7536
+ process.kill(stale.pid, "SIGKILL");
7537
+ } catch {
7538
+ }
7539
+ await new Promise((r) => setTimeout(r, 200));
7540
+ }
7541
+ removePidFile(pidFilePath);
7542
+ await new Promise((r) => setTimeout(r, 300));
7543
+ }
7474
7544
  async function buildScore(config, sarifStore, workspace, dashboardUrl) {
7475
7545
  return computeProjectScore({
7476
7546
  workspaceRoot: config.pluginRoot,
@@ -7514,7 +7584,7 @@ function computeCrap(input, threshold) {
7514
7584
 
7515
7585
  // src/metrics/workspace-walker.ts
7516
7586
  import { promises as fs3 } from "node:fs";
7517
- import { join as join2 } from "node:path";
7587
+ import { join as join3 } from "node:path";
7518
7588
  var SKIP_DIRS = /* @__PURE__ */ new Set([
7519
7589
  "node_modules",
7520
7590
  ".git",
@@ -7569,7 +7639,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7569
7639
  for (const entry of entries) {
7570
7640
  if (truncated) return;
7571
7641
  if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
7572
- const full = join2(dir, entry.name);
7642
+ const full = join3(dir, entry.name);
7573
7643
  if (entry.isDirectory()) {
7574
7644
  if (SKIP_DIRS.has(entry.name)) continue;
7575
7645
  await walk2(full);
@@ -7602,7 +7672,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7602
7672
 
7603
7673
  // src/sarif/sarif-store.ts
7604
7674
  import { promises as fs4 } from "node:fs";
7605
- import { dirname as dirname3, isAbsolute, join as join3, resolve as resolve3 } from "node:path";
7675
+ import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
7606
7676
 
7607
7677
  // src/sarif/sarif-builder.ts
7608
7678
  function buildSarifDocument(tool, findings) {
@@ -7665,7 +7735,7 @@ var SarifStore = class {
7665
7735
  toolInvocations = 0;
7666
7736
  constructor(options) {
7667
7737
  const dir = isAbsolute(options.outputDir) ? options.outputDir : resolve3(options.workspaceRoot, options.outputDir);
7668
- this.filePath = join3(dir, options.fileName ?? "latest.sarif");
7738
+ this.filePath = join4(dir, options.fileName ?? "latest.sarif");
7669
7739
  }
7670
7740
  /**
7671
7741
  * Absolute path to the consolidated SARIF file on disk.
@@ -7736,7 +7806,8 @@ var SarifStore = class {
7736
7806
  return;
7737
7807
  }
7738
7808
  throw new Error(
7739
- `[sarif-store] Failed to load consolidated report at ${this.filePath}: ${error.message}`
7809
+ `[sarif-store] Failed to load consolidated report at ${this.filePath}: ${error.message}`,
7810
+ { cause: err }
7740
7811
  );
7741
7812
  }
7742
7813
  }
@@ -7938,8 +8009,8 @@ function validateSarifDocument(doc) {
7938
8009
  }
7939
8010
 
7940
8011
  // src/crap-config.ts
7941
- import { readFileSync } from "node:fs";
7942
- import { join as join4 } from "node:path";
8012
+ import { readFileSync as readFileSync2 } from "node:fs";
8013
+ import { join as join5 } from "node:path";
7943
8014
  var STRICTNESS_VALUES = ["strict", "warn", "advisory"];
7944
8015
  var DEFAULT_STRICTNESS = "strict";
7945
8016
  var CrapConfigError = class extends Error {
@@ -7964,10 +8035,10 @@ function loadCrapConfig(options) {
7964
8035
  return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
7965
8036
  }
7966
8037
  function readFromFile(workspaceRoot) {
7967
- const filePath = join4(workspaceRoot, ".claude-crap.json");
8038
+ const filePath = join5(workspaceRoot, ".claude-crap.json");
7968
8039
  let raw;
7969
8040
  try {
7970
- raw = readFileSync(filePath, "utf8");
8041
+ raw = readFileSync2(filePath, "utf8");
7971
8042
  } catch (err) {
7972
8043
  const error = err;
7973
8044
  if (error.code === "ENOENT") return null;
@@ -8010,7 +8081,7 @@ function isStrictness(value) {
8010
8081
 
8011
8082
  // src/tools/test-harness.ts
8012
8083
  import { promises as fs5 } from "node:fs";
8013
- import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve4, sep } from "node:path";
8084
+ import { basename, dirname as dirname4, extname, isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve4, sep } from "node:path";
8014
8085
  var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
8015
8086
  function isTestFile(filePath) {
8016
8087
  const base = basename(filePath);
@@ -8028,21 +8099,21 @@ function candidatePaths(workspaceRoot, filePath) {
8028
8099
  const relFromRoot = relative(absWorkspace, absSource);
8029
8100
  const relDir = dirname4(relFromRoot);
8030
8101
  const candidates = /* @__PURE__ */ new Set();
8031
- candidates.add(join5(dir, `${base}.test${ext}`));
8032
- candidates.add(join5(dir, `${base}.spec${ext}`));
8033
- candidates.add(join5(dir, "__tests__", `${base}.test${ext}`));
8034
- candidates.add(join5(dir, "__tests__", `${base}.spec${ext}`));
8102
+ candidates.add(join6(dir, `${base}.test${ext}`));
8103
+ candidates.add(join6(dir, `${base}.spec${ext}`));
8104
+ candidates.add(join6(dir, "__tests__", `${base}.test${ext}`));
8105
+ candidates.add(join6(dir, "__tests__", `${base}.spec${ext}`));
8035
8106
  for (const testRoot of ["tests", "test", "__tests__"]) {
8036
- candidates.add(join5(absWorkspace, testRoot, relDir, `${base}.test${ext}`));
8037
- candidates.add(join5(absWorkspace, testRoot, relDir, `${base}.spec${ext}`));
8038
- candidates.add(join5(absWorkspace, testRoot, relDir, `${base}${ext}`));
8107
+ candidates.add(join6(absWorkspace, testRoot, relDir, `${base}.test${ext}`));
8108
+ candidates.add(join6(absWorkspace, testRoot, relDir, `${base}.spec${ext}`));
8109
+ candidates.add(join6(absWorkspace, testRoot, relDir, `${base}${ext}`));
8039
8110
  }
8040
8111
  let current = dir;
8041
8112
  while (current.length >= absWorkspace.length) {
8042
8113
  for (const testRoot of ["tests", "test", "__tests__"]) {
8043
- candidates.add(join5(current, testRoot, `${base}.test${ext}`));
8044
- candidates.add(join5(current, testRoot, `${base}.spec${ext}`));
8045
- candidates.add(join5(current, testRoot, `${base}${ext}`));
8114
+ candidates.add(join6(current, testRoot, `${base}.test${ext}`));
8115
+ candidates.add(join6(current, testRoot, `${base}.spec${ext}`));
8116
+ candidates.add(join6(current, testRoot, `${base}${ext}`));
8046
8117
  }
8047
8118
  if (current === absWorkspace) break;
8048
8119
  const parent = dirname4(current);
@@ -8050,9 +8121,9 @@ function candidatePaths(workspaceRoot, filePath) {
8050
8121
  current = parent;
8051
8122
  }
8052
8123
  if (ext === ".py") {
8053
- candidates.add(join5(dir, `test_${base}.py`));
8054
- candidates.add(join5(absWorkspace, "tests", `test_${base}.py`));
8055
- candidates.add(join5(absWorkspace, "tests", relDir, `test_${base}.py`));
8124
+ candidates.add(join6(dir, `test_${base}.py`));
8125
+ candidates.add(join6(absWorkspace, "tests", `test_${base}.py`));
8126
+ candidates.add(join6(absWorkspace, "tests", relDir, `test_${base}.py`));
8056
8127
  }
8057
8128
  return Array.from(candidates);
8058
8129
  }
@@ -8086,12 +8157,12 @@ function resolveWithinWorkspace(workspaceRoot, filePath) {
8086
8157
  }
8087
8158
 
8088
8159
  // src/scanner/auto-scan.ts
8089
- import { existsSync as existsSync4 } from "node:fs";
8090
- import { join as join9 } from "node:path";
8160
+ import { existsSync as existsSync5 } from "node:fs";
8161
+ import { join as join10 } from "node:path";
8091
8162
 
8092
8163
  // src/scanner/detector.ts
8093
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
8094
- import { join as join6 } from "node:path";
8164
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8165
+ import { join as join7 } from "node:path";
8095
8166
  import { execFile } from "node:child_process";
8096
8167
  var SCANNER_SIGNALS = {
8097
8168
  eslint: {
@@ -8145,8 +8216,8 @@ var SCANNER_SIGNALS = {
8145
8216
  function probeConfigFiles(workspaceRoot, scanner) {
8146
8217
  const signals = SCANNER_SIGNALS[scanner];
8147
8218
  for (const file of signals.configFiles) {
8148
- const fullPath = join6(workspaceRoot, file);
8149
- if (existsSync(fullPath)) {
8219
+ const fullPath = join7(workspaceRoot, file);
8220
+ if (existsSync2(fullPath)) {
8150
8221
  return { found: true, path: fullPath };
8151
8222
  }
8152
8223
  }
@@ -8155,10 +8226,10 @@ function probeConfigFiles(workspaceRoot, scanner) {
8155
8226
  function probePackageJson(workspaceRoot, scanner) {
8156
8227
  const signals = SCANNER_SIGNALS[scanner];
8157
8228
  if (signals.packageJsonKeys.length === 0) return false;
8158
- const pkgPath = join6(workspaceRoot, "package.json");
8159
- if (!existsSync(pkgPath)) return false;
8229
+ const pkgPath = join7(workspaceRoot, "package.json");
8230
+ if (!existsSync2(pkgPath)) return false;
8160
8231
  try {
8161
- const raw = readFileSync2(pkgPath, "utf-8");
8232
+ const raw = readFileSync3(pkgPath, "utf-8");
8162
8233
  const pkg = JSON.parse(raw);
8163
8234
  const deps = {
8164
8235
  ...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
@@ -8218,8 +8289,8 @@ async function detectScanners(workspaceRoot) {
8218
8289
 
8219
8290
  // src/scanner/runner.ts
8220
8291
  import { execFile as execFile2 } from "node:child_process";
8221
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
8222
- import { join as join7 } from "node:path";
8292
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
8293
+ import { join as join8 } from "node:path";
8223
8294
  function getScannerCommand(scanner, workspaceRoot) {
8224
8295
  switch (scanner) {
8225
8296
  case "eslint":
@@ -8249,7 +8320,7 @@ function getScannerCommand(scanner, workspaceRoot) {
8249
8320
  args: ["stryker", "run"],
8250
8321
  timeoutMs: 3e5,
8251
8322
  nonZeroIsNormal: false,
8252
- outputFile: join7(workspaceRoot, "reports", "mutation", "mutation.json")
8323
+ outputFile: join8(workspaceRoot, "reports", "mutation", "mutation.json")
8253
8324
  };
8254
8325
  }
8255
8326
  }
@@ -8272,9 +8343,9 @@ function runScanner(scanner, workspaceRoot) {
8272
8343
  const durationMs = Date.now() - start;
8273
8344
  const isFatalError = cmd.nonZeroIsNormal && err && (!stdout?.trim() || stderr?.includes("Oops!") || stderr?.includes("couldn't find"));
8274
8345
  if (err && (!cmd.nonZeroIsNormal || isFatalError)) {
8275
- if (cmd.outputFile && existsSync2(cmd.outputFile)) {
8346
+ if (cmd.outputFile && existsSync3(cmd.outputFile)) {
8276
8347
  try {
8277
- const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8348
+ const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8278
8349
  resolve6({
8279
8350
  scanner,
8280
8351
  success: true,
@@ -8295,9 +8366,9 @@ function runScanner(scanner, workspaceRoot) {
8295
8366
  return;
8296
8367
  }
8297
8368
  if (cmd.outputFile) {
8298
- if (existsSync2(cmd.outputFile)) {
8369
+ if (existsSync3(cmd.outputFile)) {
8299
8370
  try {
8300
- const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8371
+ const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8301
8372
  resolve6({
8302
8373
  scanner,
8303
8374
  success: true,
@@ -8348,11 +8419,11 @@ function runScanner(scanner, workspaceRoot) {
8348
8419
  }
8349
8420
 
8350
8421
  // src/scanner/bootstrap.ts
8351
- import { existsSync as existsSync3, writeFileSync, readdirSync } from "node:fs";
8352
- import { join as join8 } from "node:path";
8422
+ import { existsSync as existsSync4, writeFileSync as writeFileSync2, readdirSync } from "node:fs";
8423
+ import { join as join9 } from "node:path";
8353
8424
  import { execFile as execFile3 } from "node:child_process";
8354
8425
  function detectProjectType(workspaceRoot) {
8355
- const has = (file) => existsSync3(join8(workspaceRoot, file));
8426
+ const has = (file) => existsSync4(join9(workspaceRoot, file));
8356
8427
  if (has("package.json")) {
8357
8428
  if (has("tsconfig.json")) return "typescript";
8358
8429
  return "javascript";
@@ -8440,8 +8511,8 @@ function npmInstall(workspaceRoot, packages) {
8440
8511
  });
8441
8512
  }
8442
8513
  function writeEslintConfigFile(workspaceRoot, isTypeScript) {
8443
- const configPath = join8(workspaceRoot, "eslint.config.mjs");
8444
- if (existsSync3(configPath)) {
8514
+ const configPath = join9(workspaceRoot, "eslint.config.mjs");
8515
+ if (existsSync4(configPath)) {
8445
8516
  return {
8446
8517
  action: "create eslint.config.mjs",
8447
8518
  success: true,
@@ -8449,7 +8520,7 @@ function writeEslintConfigFile(workspaceRoot, isTypeScript) {
8449
8520
  };
8450
8521
  }
8451
8522
  try {
8452
- writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
8523
+ writeFileSync2(configPath, generateEslintConfig(isTypeScript), "utf-8");
8453
8524
  return {
8454
8525
  action: "create eslint.config.mjs",
8455
8526
  success: true,
@@ -8666,7 +8737,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
8666
8737
  ".eslintrc.json"
8667
8738
  ];
8668
8739
  const eslintDetected = available.some((d) => d.scanner === "eslint");
8669
- const hasEslintConfig = eslintConfigFiles.some((f) => existsSync4(join9(workspaceRoot, f)));
8740
+ const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(join10(workspaceRoot, f)));
8670
8741
  if (eslintDetected && !hasEslintConfig) {
8671
8742
  logger2.info("auto-scan: ESLint detected but no config \u2014 running bootstrap");
8672
8743
  try {