claude-crap 0.3.4 → 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,9 +7235,8 @@ function loadConfig() {
7235
7235
  }
7236
7236
 
7237
7237
  // src/dashboard/server.ts
7238
- import { promises as fs2 } from "node:fs";
7239
- import { createServer as createTcpServer } from "node:net";
7240
- 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";
7241
7240
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7242
7241
  import Fastify from "fastify";
7243
7242
  import fastifyStatic from "@fastify/static";
@@ -7417,7 +7416,7 @@ async function startDashboard(options) {
7417
7416
  root: publicRoot,
7418
7417
  prefix: "/"
7419
7418
  });
7420
- fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.4" }));
7419
+ fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.5" }));
7421
7420
  fastify.get("/api/score", async () => {
7422
7421
  const stats = await workspaceStatsProvider();
7423
7422
  const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
@@ -7427,20 +7426,16 @@ async function startDashboard(options) {
7427
7426
  fastify.get("/", async (_request, reply) => {
7428
7427
  return reply.sendFile("index.html");
7429
7428
  });
7430
- const MAX_PORT_RETRIES = 4;
7431
- const boundPort = await findFreePort(config.dashboardPort, MAX_PORT_RETRIES, logger2);
7432
- await fastify.listen({ port: boundPort, host: "127.0.0.1" });
7433
- const url = `http://127.0.0.1:${boundPort}`;
7434
- if (boundPort !== config.dashboardPort) {
7435
- logger2.warn(
7436
- { url, configuredPort: config.dashboardPort, actualPort: boundPort },
7437
- "claude-crap dashboard bound to fallback port (configured port was in use)"
7438
- );
7439
- }
7429
+ const pidFilePath = resolvePidFilePath(config);
7430
+ await killStaleDashboard(pidFilePath, config.dashboardPort, logger2);
7431
+ await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
7432
+ const url = `http://127.0.0.1:${config.dashboardPort}`;
7440
7433
  logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
7434
+ writePidFile(pidFilePath, config.dashboardPort);
7441
7435
  return {
7442
7436
  url,
7443
7437
  async close() {
7438
+ removePidFile(pidFilePath);
7444
7439
  await fastify.close();
7445
7440
  }
7446
7441
  };
@@ -7480,33 +7475,71 @@ function urlOf(fastify, config) {
7480
7475
  }
7481
7476
  return `http://127.0.0.1:${config.dashboardPort}`;
7482
7477
  }
7483
- async function findFreePort(startPort, maxRetries, logger2) {
7484
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
7485
- const candidatePort = startPort + attempt;
7486
- const isFree = await new Promise((resolvePromise) => {
7487
- const probe = createTcpServer();
7488
- probe.once("error", (err) => {
7489
- if (err.code === "EADDRINUSE") {
7490
- resolvePromise(false);
7491
- } else {
7492
- resolvePromise(false);
7493
- }
7494
- });
7495
- probe.listen({ port: candidatePort, host: "127.0.0.1" }, () => {
7496
- probe.close(() => resolvePromise(true));
7497
- });
7498
- });
7499
- if (isFree) return candidatePort;
7500
- if (attempt < maxRetries) {
7501
- logger2.info(
7502
- { port: candidatePort, nextPort: candidatePort + 1 },
7503
- "dashboard port in use, trying next"
7504
- );
7505
- }
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 {
7506
7490
  }
7507
- throw new Error(
7508
- `[claude-crap] dashboard: all ports ${startPort}\u2013${startPort + maxRetries} are in use`
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"
7509
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));
7510
7543
  }
7511
7544
  async function buildScore(config, sarifStore, workspace, dashboardUrl) {
7512
7545
  return computeProjectScore({
@@ -7551,7 +7584,7 @@ function computeCrap(input, threshold) {
7551
7584
 
7552
7585
  // src/metrics/workspace-walker.ts
7553
7586
  import { promises as fs3 } from "node:fs";
7554
- import { join as join2 } from "node:path";
7587
+ import { join as join3 } from "node:path";
7555
7588
  var SKIP_DIRS = /* @__PURE__ */ new Set([
7556
7589
  "node_modules",
7557
7590
  ".git",
@@ -7606,7 +7639,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7606
7639
  for (const entry of entries) {
7607
7640
  if (truncated) return;
7608
7641
  if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
7609
- const full = join2(dir, entry.name);
7642
+ const full = join3(dir, entry.name);
7610
7643
  if (entry.isDirectory()) {
7611
7644
  if (SKIP_DIRS.has(entry.name)) continue;
7612
7645
  await walk2(full);
@@ -7639,7 +7672,7 @@ async function estimateWorkspaceLoc(workspaceRoot) {
7639
7672
 
7640
7673
  // src/sarif/sarif-store.ts
7641
7674
  import { promises as fs4 } from "node:fs";
7642
- 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";
7643
7676
 
7644
7677
  // src/sarif/sarif-builder.ts
7645
7678
  function buildSarifDocument(tool, findings) {
@@ -7702,7 +7735,7 @@ var SarifStore = class {
7702
7735
  toolInvocations = 0;
7703
7736
  constructor(options) {
7704
7737
  const dir = isAbsolute(options.outputDir) ? options.outputDir : resolve3(options.workspaceRoot, options.outputDir);
7705
- this.filePath = join3(dir, options.fileName ?? "latest.sarif");
7738
+ this.filePath = join4(dir, options.fileName ?? "latest.sarif");
7706
7739
  }
7707
7740
  /**
7708
7741
  * Absolute path to the consolidated SARIF file on disk.
@@ -7773,7 +7806,8 @@ var SarifStore = class {
7773
7806
  return;
7774
7807
  }
7775
7808
  throw new Error(
7776
- `[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 }
7777
7811
  );
7778
7812
  }
7779
7813
  }
@@ -7975,8 +8009,8 @@ function validateSarifDocument(doc) {
7975
8009
  }
7976
8010
 
7977
8011
  // src/crap-config.ts
7978
- import { readFileSync } from "node:fs";
7979
- import { join as join4 } from "node:path";
8012
+ import { readFileSync as readFileSync2 } from "node:fs";
8013
+ import { join as join5 } from "node:path";
7980
8014
  var STRICTNESS_VALUES = ["strict", "warn", "advisory"];
7981
8015
  var DEFAULT_STRICTNESS = "strict";
7982
8016
  var CrapConfigError = class extends Error {
@@ -8001,10 +8035,10 @@ function loadCrapConfig(options) {
8001
8035
  return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
8002
8036
  }
8003
8037
  function readFromFile(workspaceRoot) {
8004
- const filePath = join4(workspaceRoot, ".claude-crap.json");
8038
+ const filePath = join5(workspaceRoot, ".claude-crap.json");
8005
8039
  let raw;
8006
8040
  try {
8007
- raw = readFileSync(filePath, "utf8");
8041
+ raw = readFileSync2(filePath, "utf8");
8008
8042
  } catch (err) {
8009
8043
  const error = err;
8010
8044
  if (error.code === "ENOENT") return null;
@@ -8047,7 +8081,7 @@ function isStrictness(value) {
8047
8081
 
8048
8082
  // src/tools/test-harness.ts
8049
8083
  import { promises as fs5 } from "node:fs";
8050
- 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";
8051
8085
  var TEST_SUFFIX_PATTERN = /\.(test|spec)\./;
8052
8086
  function isTestFile(filePath) {
8053
8087
  const base = basename(filePath);
@@ -8065,21 +8099,21 @@ function candidatePaths(workspaceRoot, filePath) {
8065
8099
  const relFromRoot = relative(absWorkspace, absSource);
8066
8100
  const relDir = dirname4(relFromRoot);
8067
8101
  const candidates = /* @__PURE__ */ new Set();
8068
- candidates.add(join5(dir, `${base}.test${ext}`));
8069
- candidates.add(join5(dir, `${base}.spec${ext}`));
8070
- candidates.add(join5(dir, "__tests__", `${base}.test${ext}`));
8071
- 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}`));
8072
8106
  for (const testRoot of ["tests", "test", "__tests__"]) {
8073
- candidates.add(join5(absWorkspace, testRoot, relDir, `${base}.test${ext}`));
8074
- candidates.add(join5(absWorkspace, testRoot, relDir, `${base}.spec${ext}`));
8075
- 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}`));
8076
8110
  }
8077
8111
  let current = dir;
8078
8112
  while (current.length >= absWorkspace.length) {
8079
8113
  for (const testRoot of ["tests", "test", "__tests__"]) {
8080
- candidates.add(join5(current, testRoot, `${base}.test${ext}`));
8081
- candidates.add(join5(current, testRoot, `${base}.spec${ext}`));
8082
- 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}`));
8083
8117
  }
8084
8118
  if (current === absWorkspace) break;
8085
8119
  const parent = dirname4(current);
@@ -8087,9 +8121,9 @@ function candidatePaths(workspaceRoot, filePath) {
8087
8121
  current = parent;
8088
8122
  }
8089
8123
  if (ext === ".py") {
8090
- candidates.add(join5(dir, `test_${base}.py`));
8091
- candidates.add(join5(absWorkspace, "tests", `test_${base}.py`));
8092
- 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`));
8093
8127
  }
8094
8128
  return Array.from(candidates);
8095
8129
  }
@@ -8123,12 +8157,12 @@ function resolveWithinWorkspace(workspaceRoot, filePath) {
8123
8157
  }
8124
8158
 
8125
8159
  // src/scanner/auto-scan.ts
8126
- import { existsSync as existsSync4 } from "node:fs";
8127
- import { join as join9 } from "node:path";
8160
+ import { existsSync as existsSync5 } from "node:fs";
8161
+ import { join as join10 } from "node:path";
8128
8162
 
8129
8163
  // src/scanner/detector.ts
8130
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
8131
- 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";
8132
8166
  import { execFile } from "node:child_process";
8133
8167
  var SCANNER_SIGNALS = {
8134
8168
  eslint: {
@@ -8182,8 +8216,8 @@ var SCANNER_SIGNALS = {
8182
8216
  function probeConfigFiles(workspaceRoot, scanner) {
8183
8217
  const signals = SCANNER_SIGNALS[scanner];
8184
8218
  for (const file of signals.configFiles) {
8185
- const fullPath = join6(workspaceRoot, file);
8186
- if (existsSync(fullPath)) {
8219
+ const fullPath = join7(workspaceRoot, file);
8220
+ if (existsSync2(fullPath)) {
8187
8221
  return { found: true, path: fullPath };
8188
8222
  }
8189
8223
  }
@@ -8192,10 +8226,10 @@ function probeConfigFiles(workspaceRoot, scanner) {
8192
8226
  function probePackageJson(workspaceRoot, scanner) {
8193
8227
  const signals = SCANNER_SIGNALS[scanner];
8194
8228
  if (signals.packageJsonKeys.length === 0) return false;
8195
- const pkgPath = join6(workspaceRoot, "package.json");
8196
- if (!existsSync(pkgPath)) return false;
8229
+ const pkgPath = join7(workspaceRoot, "package.json");
8230
+ if (!existsSync2(pkgPath)) return false;
8197
8231
  try {
8198
- const raw = readFileSync2(pkgPath, "utf-8");
8232
+ const raw = readFileSync3(pkgPath, "utf-8");
8199
8233
  const pkg = JSON.parse(raw);
8200
8234
  const deps = {
8201
8235
  ...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
@@ -8255,8 +8289,8 @@ async function detectScanners(workspaceRoot) {
8255
8289
 
8256
8290
  // src/scanner/runner.ts
8257
8291
  import { execFile as execFile2 } from "node:child_process";
8258
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
8259
- 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";
8260
8294
  function getScannerCommand(scanner, workspaceRoot) {
8261
8295
  switch (scanner) {
8262
8296
  case "eslint":
@@ -8286,7 +8320,7 @@ function getScannerCommand(scanner, workspaceRoot) {
8286
8320
  args: ["stryker", "run"],
8287
8321
  timeoutMs: 3e5,
8288
8322
  nonZeroIsNormal: false,
8289
- outputFile: join7(workspaceRoot, "reports", "mutation", "mutation.json")
8323
+ outputFile: join8(workspaceRoot, "reports", "mutation", "mutation.json")
8290
8324
  };
8291
8325
  }
8292
8326
  }
@@ -8309,9 +8343,9 @@ function runScanner(scanner, workspaceRoot) {
8309
8343
  const durationMs = Date.now() - start;
8310
8344
  const isFatalError = cmd.nonZeroIsNormal && err && (!stdout?.trim() || stderr?.includes("Oops!") || stderr?.includes("couldn't find"));
8311
8345
  if (err && (!cmd.nonZeroIsNormal || isFatalError)) {
8312
- if (cmd.outputFile && existsSync2(cmd.outputFile)) {
8346
+ if (cmd.outputFile && existsSync3(cmd.outputFile)) {
8313
8347
  try {
8314
- const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8348
+ const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8315
8349
  resolve6({
8316
8350
  scanner,
8317
8351
  success: true,
@@ -8332,9 +8366,9 @@ function runScanner(scanner, workspaceRoot) {
8332
8366
  return;
8333
8367
  }
8334
8368
  if (cmd.outputFile) {
8335
- if (existsSync2(cmd.outputFile)) {
8369
+ if (existsSync3(cmd.outputFile)) {
8336
8370
  try {
8337
- const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8371
+ const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
8338
8372
  resolve6({
8339
8373
  scanner,
8340
8374
  success: true,
@@ -8385,11 +8419,11 @@ function runScanner(scanner, workspaceRoot) {
8385
8419
  }
8386
8420
 
8387
8421
  // src/scanner/bootstrap.ts
8388
- import { existsSync as existsSync3, writeFileSync, readdirSync } from "node:fs";
8389
- 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";
8390
8424
  import { execFile as execFile3 } from "node:child_process";
8391
8425
  function detectProjectType(workspaceRoot) {
8392
- const has = (file) => existsSync3(join8(workspaceRoot, file));
8426
+ const has = (file) => existsSync4(join9(workspaceRoot, file));
8393
8427
  if (has("package.json")) {
8394
8428
  if (has("tsconfig.json")) return "typescript";
8395
8429
  return "javascript";
@@ -8477,8 +8511,8 @@ function npmInstall(workspaceRoot, packages) {
8477
8511
  });
8478
8512
  }
8479
8513
  function writeEslintConfigFile(workspaceRoot, isTypeScript) {
8480
- const configPath = join8(workspaceRoot, "eslint.config.mjs");
8481
- if (existsSync3(configPath)) {
8514
+ const configPath = join9(workspaceRoot, "eslint.config.mjs");
8515
+ if (existsSync4(configPath)) {
8482
8516
  return {
8483
8517
  action: "create eslint.config.mjs",
8484
8518
  success: true,
@@ -8486,7 +8520,7 @@ function writeEslintConfigFile(workspaceRoot, isTypeScript) {
8486
8520
  };
8487
8521
  }
8488
8522
  try {
8489
- writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
8523
+ writeFileSync2(configPath, generateEslintConfig(isTypeScript), "utf-8");
8490
8524
  return {
8491
8525
  action: "create eslint.config.mjs",
8492
8526
  success: true,
@@ -8703,7 +8737,7 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
8703
8737
  ".eslintrc.json"
8704
8738
  ];
8705
8739
  const eslintDetected = available.some((d) => d.scanner === "eslint");
8706
- const hasEslintConfig = eslintConfigFiles.some((f) => existsSync4(join9(workspaceRoot, f)));
8740
+ const hasEslintConfig = eslintConfigFiles.some((f) => existsSync5(join10(workspaceRoot, f)));
8707
8741
  if (eslintDetected && !hasEslintConfig) {
8708
8742
  logger2.info("auto-scan: ESLint detected but no config \u2014 running bootstrap");
8709
8743
  try {