claude-crap 0.3.3 → 0.3.4
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 +20 -0
- package/README.md +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +52 -3
- package/dist/dashboard/server.js.map +1 -1
- package/package.json +6 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/bundle/launcher.mjs +112 -0
- package/plugin/bundle/mcp-server.mjs +40 -3
- package/plugin/bundle/mcp-server.mjs.map +2 -2
- package/plugin/eslint.config.mjs +15 -0
- package/plugin/launcher.mjs +112 -0
- package/plugin/package-lock.json +2714 -0
- package/plugin/package.json +5 -1
- package/scripts/bundle-plugin.mjs +30 -0
- package/src/dashboard/server.ts +68 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.4] - 2026-04-12
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Out-of-the-box MCP server startup** — added `launcher.mjs` bootstrap
|
|
13
|
+
wrapper that auto-installs runtime dependencies on first run. Fresh
|
|
14
|
+
git-based installs no longer fail with `ERR_MODULE_NOT_FOUND`.
|
|
15
|
+
- **Dashboard port conflict across sessions** — when port 5117 is
|
|
16
|
+
occupied by a stale process, the dashboard now probes up to 4
|
|
17
|
+
consecutive fallback ports (5118–5121) before giving up.
|
|
18
|
+
- **Deterministic installs** — `plugin/package-lock.json` is now
|
|
19
|
+
generated during `build:plugin` so all users resolve identical
|
|
20
|
+
dependency versions.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- MCP server entry point in `.mcp.json` changed from `mcp-server.mjs`
|
|
25
|
+
to `launcher.mjs` (the launcher dynamically imports `mcp-server.mjs`
|
|
26
|
+
after ensuring dependencies exist).
|
|
27
|
+
|
|
8
28
|
## [0.3.3] - 2026-04-12
|
|
9
29
|
|
|
10
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ task close — no further setup required.
|
|
|
81
81
|
> **Two install channels are live:**
|
|
82
82
|
>
|
|
83
83
|
> - **npm** — `npx claude-crap install` (direct, works anywhere `npx` does)
|
|
84
|
-
> - **Claude Code marketplace** — `/plugin marketplace add https://github.com/ahernandez-developer/claude-crap` followed by `/plugin install claude-crap@herz`. Claude Code resolves the marketplace entry's `source` to `claude-crap@0.
|
|
84
|
+
> - **Claude Code marketplace** — `/plugin marketplace add https://github.com/ahernandez-developer/claude-crap` followed by `/plugin install claude-crap@herz`. Claude Code resolves the marketplace entry's `source` to `claude-crap@0.3.4` on the npm registry, so both routes unpack the **same tarball** and get the same SHA.
|
|
85
85
|
|
|
86
86
|
### Marketplace cache troubleshooting
|
|
87
87
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AASH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,2EAA2E;IAC3E,QAAQ,CAAC,sBAAsB,EAAE,sBAAsB,CAAC;IACxD,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CAwE7F"}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @module dashboard/server
|
|
25
25
|
*/
|
|
26
26
|
import { promises as fs } from "node:fs";
|
|
27
|
+
import { createServer as createTcpServer } from "node:net";
|
|
27
28
|
import { dirname, resolve } from "node:path";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import Fastify from "fastify";
|
|
@@ -54,7 +55,7 @@ export async function startDashboard(options) {
|
|
|
54
55
|
// ------------------------------------------------------------------
|
|
55
56
|
// /api/health — liveness probe
|
|
56
57
|
// ------------------------------------------------------------------
|
|
57
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
58
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.4" }));
|
|
58
59
|
// ------------------------------------------------------------------
|
|
59
60
|
// /api/score — live project score
|
|
60
61
|
// ------------------------------------------------------------------
|
|
@@ -76,8 +77,17 @@ export async function startDashboard(options) {
|
|
|
76
77
|
fastify.get("/", async (_request, reply) => {
|
|
77
78
|
return reply.sendFile("index.html");
|
|
78
79
|
});
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// Find a free port. The configured port is tried first, then up to
|
|
81
|
+
// MAX_PORT_RETRIES consecutive neighbors. This handles the common
|
|
82
|
+
// case where a previous Claude Code session left a stale MCP server
|
|
83
|
+
// process holding the default port.
|
|
84
|
+
const MAX_PORT_RETRIES = 4;
|
|
85
|
+
const boundPort = await findFreePort(config.dashboardPort, MAX_PORT_RETRIES, logger);
|
|
86
|
+
await fastify.listen({ port: boundPort, host: "127.0.0.1" });
|
|
87
|
+
const url = `http://127.0.0.1:${boundPort}`;
|
|
88
|
+
if (boundPort !== config.dashboardPort) {
|
|
89
|
+
logger.warn({ url, configuredPort: config.dashboardPort, actualPort: boundPort }, "claude-crap dashboard bound to fallback port (configured port was in use)");
|
|
90
|
+
}
|
|
81
91
|
logger.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
82
92
|
return {
|
|
83
93
|
url,
|
|
@@ -134,6 +144,45 @@ function urlOf(fastify, config) {
|
|
|
134
144
|
}
|
|
135
145
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
136
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Probe a sequence of TCP ports starting at `startPort` and return the
|
|
149
|
+
* first one that is free. Uses a raw `net.createServer()` probe that
|
|
150
|
+
* opens and immediately closes to avoid interfering with Fastify's own
|
|
151
|
+
* listen lifecycle (Fastify instances cannot re-listen after close).
|
|
152
|
+
*
|
|
153
|
+
* @param startPort The preferred port.
|
|
154
|
+
* @param maxRetries How many consecutive ports to try after the first.
|
|
155
|
+
* @param logger Pino logger for diagnostics.
|
|
156
|
+
* @returns The first free port found.
|
|
157
|
+
* @throws When all candidate ports are occupied.
|
|
158
|
+
*/
|
|
159
|
+
async function findFreePort(startPort, maxRetries, logger) {
|
|
160
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
161
|
+
const candidatePort = startPort + attempt;
|
|
162
|
+
const isFree = await new Promise((resolvePromise) => {
|
|
163
|
+
const probe = createTcpServer();
|
|
164
|
+
probe.once("error", (err) => {
|
|
165
|
+
if (err.code === "EADDRINUSE") {
|
|
166
|
+
resolvePromise(false);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Unexpected error (permission denied, etc.) — treat as unavailable.
|
|
170
|
+
resolvePromise(false);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
probe.listen({ port: candidatePort, host: "127.0.0.1" }, () => {
|
|
174
|
+
probe.close(() => resolvePromise(true));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
if (isFree)
|
|
178
|
+
return candidatePort;
|
|
179
|
+
if (attempt < maxRetries) {
|
|
180
|
+
logger.info({ port: candidatePort, nextPort: candidatePort + 1 }, "dashboard port in use, trying next");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// All ports exhausted — throw so startDashboard rejects gracefully.
|
|
184
|
+
throw new Error(`[claude-crap] dashboard: all ports ${startPort}–${startPort + maxRetries} are in use`);
|
|
185
|
+
}
|
|
137
186
|
/**
|
|
138
187
|
* Wrap {@link computeProjectScore} so the dashboard endpoint can call
|
|
139
188
|
* it with the live store and provide consistent location metadata.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,OAAiC,MAAM,SAAS,CAAC;AACxD,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAI5C,OAAO,EACL,mBAAmB,GAGpB,MAAM,qBAAqB,CAAC;AAiC7B;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAA8B;IACjE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAEvE,oEAAoE;IACpE,qEAAqE;IACrE,qEAAqE;IACrE,kEAAkE;IAClE,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAEnD,MAAM,OAAO,GAAoB,OAAO,CAAC;QACvC,MAAM,EAAE,KAAK,EAAE,uDAAuD;QACtE,qBAAqB,EAAE,IAAI;KAC5B,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE;QACpC,IAAI,EAAE,UAAU;QAChB,MAAM,EAAE,GAAG;KACZ,CAAC,CAAC;IAEH,qEAAqE;IACrE,+BAA+B;IAC/B,qEAAqE;IACrE,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAEpG,qEAAqE;IACrE,kCAAkC;IAClC,qEAAqE;IACrE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE;QACnC,MAAM,KAAK,GAAG,MAAM,sBAAsB,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QAClF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,qEAAqE;IACrE,iDAAiD;IACjD,qEAAqE;IACrE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC;IAEpE,qEAAqE;IACrE,2CAA2C;IAC3C,qEAAqE;IACrE,mEAAmE;IACnE,kEAAkE;IAClE,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACzC,OAAO,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,MAAM,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Deterministic QA plugin for Claude Code — CRAP index, Technical Debt Ratio, tree-sitter AST, SARIF 2.1.0, hooks, and a local Vue dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -128,10 +128,14 @@
|
|
|
128
128
|
"web-tree-sitter": "^0.24.4"
|
|
129
129
|
},
|
|
130
130
|
"devDependencies": {
|
|
131
|
+
"@eslint/js": "^10.0.1",
|
|
131
132
|
"@types/node": "^22.10.2",
|
|
132
133
|
"esbuild": "^0.28.0",
|
|
134
|
+
"eslint": "^10.2.0",
|
|
135
|
+
"globals": "^17.5.0",
|
|
133
136
|
"np": "^11.0.2",
|
|
134
137
|
"tsx": "^4.19.2",
|
|
135
|
-
"typescript": "^5.7.2"
|
|
138
|
+
"typescript": "^5.7.2",
|
|
139
|
+
"typescript-eslint": "^8.58.1"
|
|
136
140
|
}
|
|
137
141
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://code.claude.com/schemas/plugin.json",
|
|
3
3
|
"name": "claude-crap",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"description": "Deterministic Quality Assurance plugin for Claude Code. Wraps every Write / Edit / Bash tool call with a PreToolUse gatekeeper, a PostToolUse verifier, and a Stop quality gate backed by CRAP index, Technical Debt Ratio, tree-sitter AST metrics, and SARIF 2.1.0 reports. Forbids the agent from writing functional code before a test safety net exists.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Alan Hernandez",
|
package/plugin/.mcp.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* claude-crap :: MCP server launcher — zero-dependency bootstrap wrapper.
|
|
5
|
+
*
|
|
6
|
+
* This file is the actual entry point declared in `.mcp.json`. It
|
|
7
|
+
* ensures the MCP server's runtime dependencies (fastify, pino,
|
|
8
|
+
* tree-sitter, etc.) are installed before the server's static ESM
|
|
9
|
+
* `import` statements fire. Without this guard, a clean install from
|
|
10
|
+
* git would fail with ERR_MODULE_NOT_FOUND because `node_modules/`
|
|
11
|
+
* is not committed to the repository.
|
|
12
|
+
*
|
|
13
|
+
* Design constraints:
|
|
14
|
+
*
|
|
15
|
+
* - ZERO external dependencies — only Node.js builtins.
|
|
16
|
+
* - Synchronous check + install so the control flow is linear.
|
|
17
|
+
* - All output goes to stderr (fd 2) to preserve the MCP JSON-RPC
|
|
18
|
+
* channel on stdout.
|
|
19
|
+
* - Must work on macOS, Linux, and Windows.
|
|
20
|
+
*
|
|
21
|
+
* After dependencies are guaranteed to exist, this module dynamically
|
|
22
|
+
* imports `./mcp-server.mjs` which handles the rest of the startup.
|
|
23
|
+
*
|
|
24
|
+
* @module launcher
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
29
|
+
import { dirname, join, resolve } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The plugin root is one level above `bundle/`. This is where
|
|
36
|
+
* `package.json` lives and where `npm install` must run.
|
|
37
|
+
*
|
|
38
|
+
* In the deployed layout:
|
|
39
|
+
* ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/
|
|
40
|
+
* ├── package.json ← PLUGIN_ROOT
|
|
41
|
+
* ├── node_modules/ ← created by ensureDependencies()
|
|
42
|
+
* └── bundle/
|
|
43
|
+
* ├── launcher.mjs ← this file
|
|
44
|
+
* └── mcp-server.mjs ← the real entry point
|
|
45
|
+
*/
|
|
46
|
+
const PLUGIN_ROOT = resolve(__dirname, "..");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check whether all runtime dependencies are installed. When they are
|
|
50
|
+
* not, run `npm install --omit=dev` synchronously and verify success.
|
|
51
|
+
*
|
|
52
|
+
* The check is deliberately simple: if the `node_modules/` directory
|
|
53
|
+
* exists we assume all packages are present. A more thorough check
|
|
54
|
+
* would resolve every external, but the cost of a false-positive skip
|
|
55
|
+
* is a cryptic ERR_MODULE_NOT_FOUND — vs. a ~5s npm install on first
|
|
56
|
+
* run. Speed wins.
|
|
57
|
+
*/
|
|
58
|
+
function ensureDependencies() {
|
|
59
|
+
const nodeModulesPath = join(PLUGIN_ROOT, "node_modules");
|
|
60
|
+
if (existsSync(nodeModulesPath)) return; // fast path — already installed
|
|
61
|
+
|
|
62
|
+
// ── Report what we're about to install ────────────────────────────
|
|
63
|
+
let depsSummary = "(unable to read package.json)";
|
|
64
|
+
try {
|
|
65
|
+
const pkgPath = join(PLUGIN_ROOT, "package.json");
|
|
66
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
67
|
+
const deps = Object.entries(pkg.dependencies || {});
|
|
68
|
+
depsSummary = deps.map(([n, v]) => `${n}@${v}`).join(", ");
|
|
69
|
+
} catch {
|
|
70
|
+
/* proceed anyway — the install itself will surface any real error */
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[claude-crap] node_modules/ not found — installing runtime dependencies...\n` +
|
|
75
|
+
`[claude-crap] deps: ${depsSummary}\n`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ── Run npm install synchronously ─────────────────────────────────
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("npm", [
|
|
81
|
+
"install",
|
|
82
|
+
"--omit=dev",
|
|
83
|
+
"--no-audit",
|
|
84
|
+
"--no-fund",
|
|
85
|
+
"--no-progress",
|
|
86
|
+
"--loglevel=error",
|
|
87
|
+
], {
|
|
88
|
+
cwd: PLUGIN_ROOT,
|
|
89
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
90
|
+
timeout: 120_000, // 2 minutes — generous for slow networks
|
|
91
|
+
env: { ...process.env, NODE_ENV: "production" },
|
|
92
|
+
});
|
|
93
|
+
process.stderr.write("[claude-crap] dependencies installed successfully.\n");
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const stderr = /** @type {any} */ (err).stderr?.toString?.() ?? "";
|
|
96
|
+
const lastLines = stderr.split("\n").filter(Boolean).slice(-20).join("\n");
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`[claude-crap] FATAL: npm install failed (exit ${/** @type {any} */ (err).status ?? "unknown"}).\n` +
|
|
99
|
+
(lastLines ? `${lastLines}\n` : "") +
|
|
100
|
+
`[claude-crap] Try manually: cd "${PLUGIN_ROOT}" && npm install --omit=dev\n`,
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Bootstrap ─────────────────────────────────────────────────────────
|
|
107
|
+
ensureDependencies();
|
|
108
|
+
|
|
109
|
+
// Now that node_modules/ exists, the static ESM imports inside
|
|
110
|
+
// mcp-server.mjs can resolve. Dynamic import avoids top-level
|
|
111
|
+
// resolution failures.
|
|
112
|
+
await import("./mcp-server.mjs");
|
|
@@ -7236,6 +7236,7 @@ function loadConfig() {
|
|
|
7236
7236
|
|
|
7237
7237
|
// src/dashboard/server.ts
|
|
7238
7238
|
import { promises as fs2 } from "node:fs";
|
|
7239
|
+
import { createServer as createTcpServer } from "node:net";
|
|
7239
7240
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
7240
7241
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7241
7242
|
import Fastify from "fastify";
|
|
@@ -7416,7 +7417,7 @@ async function startDashboard(options) {
|
|
|
7416
7417
|
root: publicRoot,
|
|
7417
7418
|
prefix: "/"
|
|
7418
7419
|
});
|
|
7419
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
7420
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.4" }));
|
|
7420
7421
|
fastify.get("/api/score", async () => {
|
|
7421
7422
|
const stats = await workspaceStatsProvider();
|
|
7422
7423
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
@@ -7426,8 +7427,16 @@ async function startDashboard(options) {
|
|
|
7426
7427
|
fastify.get("/", async (_request, reply) => {
|
|
7427
7428
|
return reply.sendFile("index.html");
|
|
7428
7429
|
});
|
|
7429
|
-
|
|
7430
|
-
const
|
|
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
|
+
}
|
|
7431
7440
|
logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
7432
7441
|
return {
|
|
7433
7442
|
url,
|
|
@@ -7471,6 +7480,34 @@ function urlOf(fastify, config) {
|
|
|
7471
7480
|
}
|
|
7472
7481
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
7473
7482
|
}
|
|
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
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
throw new Error(
|
|
7508
|
+
`[claude-crap] dashboard: all ports ${startPort}\u2013${startPort + maxRetries} are in use`
|
|
7509
|
+
);
|
|
7510
|
+
}
|
|
7474
7511
|
async function buildScore(config, sarifStore, workspace, dashboardUrl) {
|
|
7475
7512
|
return computeProjectScore({
|
|
7476
7513
|
workspaceRoot: config.pluginRoot,
|