claude-crap 0.3.1 → 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 +52 -0
- package/README.md +1 -1
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +59 -5
- package/dist/dashboard/server.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +24 -0
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +59 -22
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +7 -2
- package/dist/scanner/runner.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 +126 -24
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- 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 +75 -5
- package/src/scanner/auto-scan.ts +28 -0
- package/src/scanner/bootstrap.ts +67 -24
- package/src/scanner/runner.ts +8 -2
|
@@ -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";
|
|
@@ -7414,18 +7415,28 @@ async function startDashboard(options) {
|
|
|
7414
7415
|
});
|
|
7415
7416
|
await fastify.register(fastifyStatic, {
|
|
7416
7417
|
root: publicRoot,
|
|
7417
|
-
prefix: "/"
|
|
7418
|
-
decorateReply: false
|
|
7418
|
+
prefix: "/"
|
|
7419
7419
|
});
|
|
7420
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
7420
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.3.4" }));
|
|
7421
7421
|
fastify.get("/api/score", async () => {
|
|
7422
7422
|
const stats = await workspaceStatsProvider();
|
|
7423
7423
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
7424
7424
|
return score;
|
|
7425
7425
|
});
|
|
7426
7426
|
fastify.get("/api/sarif", async () => sarifStore.toSarifDocument());
|
|
7427
|
-
|
|
7428
|
-
|
|
7427
|
+
fastify.get("/", async (_request, reply) => {
|
|
7428
|
+
return reply.sendFile("index.html");
|
|
7429
|
+
});
|
|
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
7440
|
logger2.info({ url, publicRoot }, "claude-crap dashboard listening");
|
|
7430
7441
|
return {
|
|
7431
7442
|
url,
|
|
@@ -7469,6 +7480,34 @@ function urlOf(fastify, config) {
|
|
|
7469
7480
|
}
|
|
7470
7481
|
return `http://127.0.0.1:${config.dashboardPort}`;
|
|
7471
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
|
+
}
|
|
7472
7511
|
async function buildScore(config, sarifStore, workspace, dashboardUrl) {
|
|
7473
7512
|
return computeProjectScore({
|
|
7474
7513
|
workspaceRoot: config.pluginRoot,
|
|
@@ -8083,6 +8122,10 @@ function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
|
8083
8122
|
return candidate;
|
|
8084
8123
|
}
|
|
8085
8124
|
|
|
8125
|
+
// src/scanner/auto-scan.ts
|
|
8126
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
8127
|
+
import { join as join9 } from "node:path";
|
|
8128
|
+
|
|
8086
8129
|
// src/scanner/detector.ts
|
|
8087
8130
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
8088
8131
|
import { join as join6 } from "node:path";
|
|
@@ -8264,7 +8307,8 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8264
8307
|
},
|
|
8265
8308
|
(err, stdout, stderr) => {
|
|
8266
8309
|
const durationMs = Date.now() - start;
|
|
8267
|
-
|
|
8310
|
+
const isFatalError = cmd.nonZeroIsNormal && err && (!stdout?.trim() || stderr?.includes("Oops!") || stderr?.includes("couldn't find"));
|
|
8311
|
+
if (err && (!cmd.nonZeroIsNormal || isFatalError)) {
|
|
8268
8312
|
if (cmd.outputFile && existsSync2(cmd.outputFile)) {
|
|
8269
8313
|
try {
|
|
8270
8314
|
const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
|
|
@@ -8375,7 +8419,14 @@ export default tseslint.config(
|
|
|
8375
8419
|
js.configs.recommended,
|
|
8376
8420
|
...tseslint.configs.recommended,
|
|
8377
8421
|
{
|
|
8378
|
-
ignores: [
|
|
8422
|
+
ignores: [
|
|
8423
|
+
"dist/",
|
|
8424
|
+
"node_modules/",
|
|
8425
|
+
"coverage/",
|
|
8426
|
+
"**/bundle/",
|
|
8427
|
+
"**/vendor/",
|
|
8428
|
+
"**/*.min.js",
|
|
8429
|
+
],
|
|
8379
8430
|
},
|
|
8380
8431
|
);
|
|
8381
8432
|
`;
|
|
@@ -8385,7 +8436,14 @@ export default tseslint.config(
|
|
|
8385
8436
|
export default [
|
|
8386
8437
|
js.configs.recommended,
|
|
8387
8438
|
{
|
|
8388
|
-
ignores: [
|
|
8439
|
+
ignores: [
|
|
8440
|
+
"dist/",
|
|
8441
|
+
"node_modules/",
|
|
8442
|
+
"coverage/",
|
|
8443
|
+
"**/bundle/",
|
|
8444
|
+
"**/vendor/",
|
|
8445
|
+
"**/*.min.js",
|
|
8446
|
+
],
|
|
8389
8447
|
},
|
|
8390
8448
|
];
|
|
8391
8449
|
`;
|
|
@@ -8475,7 +8533,8 @@ function getRecommendation(projectType) {
|
|
|
8475
8533
|
async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
8476
8534
|
const detections = await detectScanners(workspaceRoot);
|
|
8477
8535
|
const available = detections.filter((d) => d.available);
|
|
8478
|
-
|
|
8536
|
+
const eslintNeedsConfig = available.some((d) => d.scanner === "eslint") && !detections.some((d) => d.scanner === "eslint" && d.configPath);
|
|
8537
|
+
if (available.length > 0 && !eslintNeedsConfig) {
|
|
8479
8538
|
const existingScanners = available.map((d) => d.scanner);
|
|
8480
8539
|
logger2.info(
|
|
8481
8540
|
{ existingScanners },
|
|
@@ -8500,13 +8559,23 @@ async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
|
8500
8559
|
);
|
|
8501
8560
|
if (recommendation.canAutoInstall) {
|
|
8502
8561
|
const isTypeScript = projectType === "typescript";
|
|
8503
|
-
const
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8562
|
+
const eslintAlreadyInstalled = available.some((d) => d.scanner === "eslint");
|
|
8563
|
+
if (!eslintAlreadyInstalled) {
|
|
8564
|
+
const packages = isTypeScript ? ["eslint", "@eslint/js", "typescript-eslint"] : ["eslint", "@eslint/js"];
|
|
8565
|
+
const installStep = await npmInstall(workspaceRoot, packages);
|
|
8566
|
+
steps.push(installStep);
|
|
8567
|
+
if (!installStep.success) {
|
|
8568
|
+
return buildResult(projectType, steps, null);
|
|
8569
|
+
}
|
|
8570
|
+
} else {
|
|
8571
|
+
steps.push({
|
|
8572
|
+
action: "npm install eslint",
|
|
8573
|
+
success: true,
|
|
8574
|
+
detail: "eslint already in package.json \u2014 skipped install"
|
|
8575
|
+
});
|
|
8509
8576
|
}
|
|
8577
|
+
const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
|
|
8578
|
+
steps.push(configStep);
|
|
8510
8579
|
} else {
|
|
8511
8580
|
steps.push({
|
|
8512
8581
|
action: `suggest ${recommendation.scanner} install`,
|
|
@@ -8570,17 +8639,21 @@ async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
|
8570
8639
|
);
|
|
8571
8640
|
}
|
|
8572
8641
|
}
|
|
8642
|
+
return buildResult(projectType, steps, autoScanResult, recommendation);
|
|
8643
|
+
}
|
|
8644
|
+
function buildResult(projectType, steps, autoScanResult, recommendation) {
|
|
8645
|
+
const success = steps.every((s) => s.success);
|
|
8573
8646
|
const findings = autoScanResult?.totalFindings ?? 0;
|
|
8574
|
-
const
|
|
8647
|
+
const scanner = recommendation?.scanner ?? "unknown";
|
|
8575
8648
|
let summary;
|
|
8576
|
-
if (
|
|
8577
|
-
summary = `
|
|
8578
|
-
} else if (
|
|
8579
|
-
summary = `
|
|
8580
|
-
} else if (
|
|
8581
|
-
summary = `
|
|
8649
|
+
if (success && autoScanResult) {
|
|
8650
|
+
summary = `Configured ${scanner} for ${projectType} project. Scan found ${findings} finding(s).`;
|
|
8651
|
+
} else if (success && recommendation && !recommendation.canAutoInstall) {
|
|
8652
|
+
summary = `Detected ${projectType} project. Install ${scanner} manually: ${recommendation.installInstructions}`;
|
|
8653
|
+
} else if (success) {
|
|
8654
|
+
summary = `Configured ${scanner} for ${projectType} project.`;
|
|
8582
8655
|
} else {
|
|
8583
|
-
summary = `Failed to
|
|
8656
|
+
summary = `Failed to configure ${scanner}. Check the error details in the steps.`;
|
|
8584
8657
|
}
|
|
8585
8658
|
return {
|
|
8586
8659
|
projectType,
|
|
@@ -8588,7 +8661,7 @@ async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
|
8588
8661
|
existingScanners: [],
|
|
8589
8662
|
steps,
|
|
8590
8663
|
autoScanResult,
|
|
8591
|
-
success
|
|
8664
|
+
success,
|
|
8592
8665
|
summary
|
|
8593
8666
|
};
|
|
8594
8667
|
}
|
|
@@ -8616,6 +8689,35 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8616
8689
|
},
|
|
8617
8690
|
"auto-scan: detection complete"
|
|
8618
8691
|
);
|
|
8692
|
+
const eslintConfigFiles = [
|
|
8693
|
+
"eslint.config.js",
|
|
8694
|
+
"eslint.config.mjs",
|
|
8695
|
+
"eslint.config.cjs",
|
|
8696
|
+
"eslint.config.ts",
|
|
8697
|
+
"eslint.config.mts",
|
|
8698
|
+
"eslint.config.cts",
|
|
8699
|
+
".eslintrc.js",
|
|
8700
|
+
".eslintrc.cjs",
|
|
8701
|
+
".eslintrc.yaml",
|
|
8702
|
+
".eslintrc.yml",
|
|
8703
|
+
".eslintrc.json"
|
|
8704
|
+
];
|
|
8705
|
+
const eslintDetected = available.some((d) => d.scanner === "eslint");
|
|
8706
|
+
const hasEslintConfig = eslintConfigFiles.some((f) => existsSync4(join9(workspaceRoot, f)));
|
|
8707
|
+
if (eslintDetected && !hasEslintConfig) {
|
|
8708
|
+
logger2.info("auto-scan: ESLint detected but no config \u2014 running bootstrap");
|
|
8709
|
+
try {
|
|
8710
|
+
const bootstrapResult = await bootstrapScanner(workspaceRoot, sarifStore, logger2);
|
|
8711
|
+
if (bootstrapResult.autoScanResult) {
|
|
8712
|
+
return bootstrapResult.autoScanResult;
|
|
8713
|
+
}
|
|
8714
|
+
} catch (err) {
|
|
8715
|
+
logger2.warn(
|
|
8716
|
+
{ err: err.message },
|
|
8717
|
+
"auto-scan: bootstrap config creation failed"
|
|
8718
|
+
);
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8619
8721
|
if (available.length === 0) {
|
|
8620
8722
|
logger2.info("auto-scan: no scanners found, attempting bootstrap");
|
|
8621
8723
|
try {
|