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/plugin/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Runtime dependencies for the claude-crap plugin bundle",
|
|
6
6
|
"type": "module",
|
|
@@ -14,5 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=20.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^10.0.1",
|
|
20
|
+
"eslint": "^10.2.0"
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// scripts/bundle-plugin.mjs
|
|
2
2
|
import { build } from "esbuild";
|
|
3
3
|
import { cp, mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
@@ -66,6 +67,35 @@ async function main() {
|
|
|
66
67
|
resolve(BUNDLE_DIR, "dashboard/public"),
|
|
67
68
|
{ recursive: true },
|
|
68
69
|
);
|
|
70
|
+
|
|
71
|
+
// 4. Copy the launcher wrapper into the bundle directory.
|
|
72
|
+
// launcher.mjs is the .mcp.json entry point and is hand-written
|
|
73
|
+
// (not bundled by esbuild). It ensures node_modules/ exist before
|
|
74
|
+
// dynamically importing the real MCP server.
|
|
75
|
+
await cp(
|
|
76
|
+
resolve(ROOT, "plugin/launcher.mjs"),
|
|
77
|
+
resolve(BUNDLE_DIR, "launcher.mjs"),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 5. Generate plugin/package-lock.json for deterministic installs.
|
|
81
|
+
// This lockfile ships with the plugin so that the launcher's
|
|
82
|
+
// `npm install --omit=dev` resolves the exact same versions on
|
|
83
|
+
// every developer's machine. `--package-lock-only` does NOT
|
|
84
|
+
// create node_modules/, keeping the repo light.
|
|
85
|
+
try {
|
|
86
|
+
execFileSync("npm", [
|
|
87
|
+
"install",
|
|
88
|
+
"--package-lock-only",
|
|
89
|
+
"--omit=dev",
|
|
90
|
+
], {
|
|
91
|
+
cwd: resolve(ROOT, "plugin"),
|
|
92
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`warning: could not generate plugin/package-lock.json: ${err.message}\n`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
main().catch((err) => {
|
package/src/dashboard/server.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { promises as fs } from "node:fs";
|
|
28
|
+
import { createServer as createTcpServer } from "node:net";
|
|
28
29
|
import { dirname, resolve } from "node:path";
|
|
29
30
|
import { fileURLToPath } from "node:url";
|
|
30
31
|
|
|
@@ -99,7 +100,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
99
100
|
// ------------------------------------------------------------------
|
|
100
101
|
// /api/health — liveness probe
|
|
101
102
|
// ------------------------------------------------------------------
|
|
102
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.
|
|
103
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.4" }));
|
|
103
104
|
|
|
104
105
|
// ------------------------------------------------------------------
|
|
105
106
|
// /api/score — live project score
|
|
@@ -125,8 +126,22 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
125
126
|
return reply.sendFile("index.html");
|
|
126
127
|
});
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
// Find a free port. The configured port is tried first, then up to
|
|
130
|
+
// MAX_PORT_RETRIES consecutive neighbors. This handles the common
|
|
131
|
+
// case where a previous Claude Code session left a stale MCP server
|
|
132
|
+
// process holding the default port.
|
|
133
|
+
const MAX_PORT_RETRIES = 4;
|
|
134
|
+
const boundPort = await findFreePort(config.dashboardPort, MAX_PORT_RETRIES, logger);
|
|
135
|
+
|
|
136
|
+
await fastify.listen({ port: boundPort, host: "127.0.0.1" });
|
|
137
|
+
|
|
138
|
+
const url = `http://127.0.0.1:${boundPort}`;
|
|
139
|
+
if (boundPort !== config.dashboardPort) {
|
|
140
|
+
logger.warn(
|
|
141
|
+
{ url, configuredPort: config.dashboardPort, actualPort: boundPort },
|
|
142
|
+
"claude-crap dashboard bound to fallback port (configured port was in use)",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
130
145
|
logger.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
131
146
|
|
|
132
147
|
return {
|
|
@@ -188,6 +203,56 @@ function urlOf(fastify: FastifyInstance, config: CrapConfig): string {
|
|
|
188
203
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
189
204
|
}
|
|
190
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Probe a sequence of TCP ports starting at `startPort` and return the
|
|
208
|
+
* first one that is free. Uses a raw `net.createServer()` probe that
|
|
209
|
+
* opens and immediately closes to avoid interfering with Fastify's own
|
|
210
|
+
* listen lifecycle (Fastify instances cannot re-listen after close).
|
|
211
|
+
*
|
|
212
|
+
* @param startPort The preferred port.
|
|
213
|
+
* @param maxRetries How many consecutive ports to try after the first.
|
|
214
|
+
* @param logger Pino logger for diagnostics.
|
|
215
|
+
* @returns The first free port found.
|
|
216
|
+
* @throws When all candidate ports are occupied.
|
|
217
|
+
*/
|
|
218
|
+
async function findFreePort(
|
|
219
|
+
startPort: number,
|
|
220
|
+
maxRetries: number,
|
|
221
|
+
logger: Logger,
|
|
222
|
+
): Promise<number> {
|
|
223
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
224
|
+
const candidatePort = startPort + attempt;
|
|
225
|
+
const isFree = await new Promise<boolean>((resolvePromise) => {
|
|
226
|
+
const probe = createTcpServer();
|
|
227
|
+
probe.once("error", (err: NodeJS.ErrnoException) => {
|
|
228
|
+
if (err.code === "EADDRINUSE") {
|
|
229
|
+
resolvePromise(false);
|
|
230
|
+
} else {
|
|
231
|
+
// Unexpected error (permission denied, etc.) — treat as unavailable.
|
|
232
|
+
resolvePromise(false);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
probe.listen({ port: candidatePort, host: "127.0.0.1" }, () => {
|
|
236
|
+
probe.close(() => resolvePromise(true));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (isFree) return candidatePort;
|
|
241
|
+
|
|
242
|
+
if (attempt < maxRetries) {
|
|
243
|
+
logger.info(
|
|
244
|
+
{ port: candidatePort, nextPort: candidatePort + 1 },
|
|
245
|
+
"dashboard port in use, trying next",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// All ports exhausted — throw so startDashboard rejects gracefully.
|
|
251
|
+
throw new Error(
|
|
252
|
+
`[claude-crap] dashboard: all ports ${startPort}–${startPort + maxRetries} are in use`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
191
256
|
/**
|
|
192
257
|
* Wrap {@link computeProjectScore} so the dashboard endpoint can call
|
|
193
258
|
* it with the live store and provide consistent location metadata.
|