claude-crap 0.4.7 → 0.4.8

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.
@@ -7875,6 +7875,20 @@ async function buildFileDetail(input) {
7875
7875
  // src/dashboard/server.ts
7876
7876
  async function startDashboard(options) {
7877
7877
  const { config, sarifStore, workspaceStatsProvider, logger: logger2 } = options;
7878
+ const pidFilePath = resolvePidFilePath(config);
7879
+ const adoption = await tryAdoptExisting(pidFilePath, config.dashboardPort, logger2);
7880
+ if (adoption) {
7881
+ logger2.info(
7882
+ { url: adoption.url, ownerPid: adoption.pid, port: config.dashboardPort },
7883
+ "adopted existing claude-crap dashboard"
7884
+ );
7885
+ return {
7886
+ url: adoption.url,
7887
+ adopted: true,
7888
+ async close() {
7889
+ }
7890
+ };
7891
+ }
7878
7892
  const publicRoot = await resolvePublicRoot(logger2);
7879
7893
  const fastify = Fastify({
7880
7894
  logger: false,
@@ -7927,14 +7941,35 @@ async function startDashboard(options) {
7927
7941
  fastify.get("/", async (_request, reply) => {
7928
7942
  return reply.sendFile("index.html");
7929
7943
  });
7930
- const pidFilePath = resolvePidFilePath(config);
7931
- await killStaleDashboard(pidFilePath, config.dashboardPort, logger2);
7932
- await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
7944
+ try {
7945
+ await fastify.listen({ port: config.dashboardPort, host: "127.0.0.1" });
7946
+ } catch (err) {
7947
+ const code = err.code;
7948
+ if (code === "EADDRINUSE") {
7949
+ await fastify.close().catch(() => {
7950
+ });
7951
+ const raceAdoption = await tryAdoptExisting(pidFilePath, config.dashboardPort, logger2);
7952
+ if (raceAdoption) {
7953
+ logger2.info(
7954
+ { url: raceAdoption.url, ownerPid: raceAdoption.pid, port: config.dashboardPort },
7955
+ "dashboard bind lost race, adopted concurrent owner"
7956
+ );
7957
+ return {
7958
+ url: raceAdoption.url,
7959
+ adopted: true,
7960
+ async close() {
7961
+ }
7962
+ };
7963
+ }
7964
+ }
7965
+ throw err;
7966
+ }
7933
7967
  const url = `http://127.0.0.1:${config.dashboardPort}`;
7934
7968
  logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
7935
7969
  writePidFile(pidFilePath, config.dashboardPort);
7936
7970
  return {
7937
7971
  url,
7972
+ adopted: false,
7938
7973
  async close() {
7939
7974
  removePidFile(pidFilePath);
7940
7975
  await fastify.close();
@@ -8004,29 +8039,40 @@ function isPidAlive(pid) {
8004
8039
  return false;
8005
8040
  }
8006
8041
  }
8007
- async function killStaleDashboard(pidFilePath, port, logger2) {
8008
- if (!existsSync(pidFilePath)) return;
8042
+ async function tryAdoptExisting(pidFilePath, port, logger2) {
8043
+ if (!existsSync(pidFilePath)) return null;
8009
8044
  let stale;
8010
8045
  try {
8011
8046
  stale = JSON.parse(readFileSync(pidFilePath, "utf8"));
8012
8047
  } catch {
8048
+ logger2.info({ pidFilePath }, "corrupt dashboard pidfile, removing");
8013
8049
  removePidFile(pidFilePath);
8014
- return;
8050
+ return null;
8015
8051
  }
8016
8052
  if (!isPidAlive(stale.pid)) {
8017
- logger2.info({ stalePid: stale.pid }, "stale dashboard PID file found (process dead), removing");
8053
+ logger2.info({ stalePid: stale.pid }, "stale dashboard pidfile (process dead), removing");
8018
8054
  removePidFile(pidFilePath);
8019
- return;
8055
+ return null;
8020
8056
  }
8021
- logger2.info(
8022
- { stalePid: stale.pid, port: stale.port, startedAt: stale.startedAt },
8023
- "killing stale dashboard process from previous session"
8057
+ if (stale.port !== port) {
8058
+ logger2.info(
8059
+ { stalePort: stale.port, wantedPort: port },
8060
+ "dashboard pidfile points at different port, ignoring"
8061
+ );
8062
+ removePidFile(pidFilePath);
8063
+ return null;
8064
+ }
8065
+ const healthy = await probeDashboardHealth(port);
8066
+ if (healthy) {
8067
+ return { url: `http://127.0.0.1:${port}`, pid: stale.pid };
8068
+ }
8069
+ logger2.warn(
8070
+ { stalePid: stale.pid, port },
8071
+ "dashboard pidfile owner is unresponsive, terminating"
8024
8072
  );
8025
8073
  try {
8026
8074
  process.kill(stale.pid, "SIGTERM");
8027
8075
  } catch {
8028
- removePidFile(pidFilePath);
8029
- return;
8030
8076
  }
8031
8077
  for (let i = 0; i < 30; i++) {
8032
8078
  if (!isPidAlive(stale.pid)) break;
@@ -8041,6 +8087,17 @@ async function killStaleDashboard(pidFilePath, port, logger2) {
8041
8087
  }
8042
8088
  removePidFile(pidFilePath);
8043
8089
  await new Promise((r) => setTimeout(r, 300));
8090
+ return null;
8091
+ }
8092
+ async function probeDashboardHealth(port) {
8093
+ try {
8094
+ const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
8095
+ signal: AbortSignal.timeout(500)
8096
+ });
8097
+ return res.ok;
8098
+ } catch {
8099
+ return false;
8100
+ }
8044
8101
  }
8045
8102
  async function buildComplexityReport(config, engine, logger2, exclude) {
8046
8103
  const threshold = config.cyclomaticMax;