claude-crap 0.1.2
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 +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* npm `postinstall` hook for the claude-crap package.
|
|
4
|
+
*
|
|
5
|
+
* Runs automatically after every `npm install` (or
|
|
6
|
+
* `npx claude-crap install` of the package). Its job is to
|
|
7
|
+
* ensure both build surfaces exist:
|
|
8
|
+
*
|
|
9
|
+
* - `dist/` — npm library distribution (tsc)
|
|
10
|
+
* - `plugin/bundle/mcp-server.mjs` — git plugin distribution (esbuild)
|
|
11
|
+
*
|
|
12
|
+
* so the MCP server, the hooks, and the dashboard can all start
|
|
13
|
+
* without a manual build step from the user. We intentionally keep
|
|
14
|
+
* this tiny — heavier validation belongs in `doctor`.
|
|
15
|
+
*
|
|
16
|
+
* Behavior:
|
|
17
|
+
*
|
|
18
|
+
* - If `dist/index.js` already exists, print a one-line welcome and
|
|
19
|
+
* exit 0. This is the common case when the package was installed
|
|
20
|
+
* from a pre-built npm tarball.
|
|
21
|
+
* - Otherwise spawn `tsc -p tsconfig.json` to build `dist/` from the
|
|
22
|
+
* shipped `src/` sources. If that fails, print a warning with the
|
|
23
|
+
* exact command the user can run manually, but still exit 0 so
|
|
24
|
+
* `npm install` is not aborted (the user may still want a
|
|
25
|
+
* source-only install for inspection).
|
|
26
|
+
* - npm sets `INIT_CWD` to the package being installed — we use
|
|
27
|
+
* that to resolve paths so `npm install claude-crap` from a
|
|
28
|
+
* parent project also works.
|
|
29
|
+
*
|
|
30
|
+
* We never write to stdout unless we have something useful to say,
|
|
31
|
+
* to stay friendly inside larger `npm install` output.
|
|
32
|
+
*
|
|
33
|
+
* @module scripts/postinstall
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { spawn } from "node:child_process";
|
|
37
|
+
import { promises as fs } from "node:fs";
|
|
38
|
+
import { dirname, join, resolve } from "node:path";
|
|
39
|
+
import { fileURLToPath } from "node:url";
|
|
40
|
+
|
|
41
|
+
// Skip the whole postinstall during `npm ci` in production-only
|
|
42
|
+
// environments where build tools may be missing. Users can opt out
|
|
43
|
+
// by setting `CLAUDE_CRAP_SKIP_POSTINSTALL=1`.
|
|
44
|
+
if (process.env.CLAUDE_CRAP_SKIP_POSTINSTALL) {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const PLUGIN_ROOT = resolve(HERE, "..");
|
|
50
|
+
|
|
51
|
+
/** @returns {Promise<boolean>} */
|
|
52
|
+
async function distIsBuilt() {
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(join(PLUGIN_ROOT, "dist", "index.js"));
|
|
55
|
+
await fs.access(join(PLUGIN_ROOT, "plugin", "bundle", "mcp-server.mjs"));
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Spawn `tsc` (via npx so it resolves from the package's own devDeps)
|
|
64
|
+
* and pipe its output through to stderr so the user sees build errors
|
|
65
|
+
* in context with the rest of the npm install output.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<number>} Exit code from tsc.
|
|
68
|
+
*/
|
|
69
|
+
function runBuild() {
|
|
70
|
+
return new Promise((resolvePromise) => {
|
|
71
|
+
const child = spawn(
|
|
72
|
+
process.execPath,
|
|
73
|
+
[join(PLUGIN_ROOT, "node_modules", "typescript", "bin", "tsc"), "-p", "tsconfig.json"],
|
|
74
|
+
{ cwd: PLUGIN_ROOT, stdio: ["ignore", "inherit", "inherit"] },
|
|
75
|
+
);
|
|
76
|
+
child.on("exit", (code) => {
|
|
77
|
+
if (code !== 0) return resolvePromise(code ?? 1);
|
|
78
|
+
const bundler = spawn(
|
|
79
|
+
process.execPath,
|
|
80
|
+
[join(PLUGIN_ROOT, "scripts", "bundle-plugin.mjs")],
|
|
81
|
+
{ cwd: PLUGIN_ROOT, stdio: ["ignore", "inherit", "inherit"] }
|
|
82
|
+
);
|
|
83
|
+
bundler.on("exit", (bc) => resolvePromise(bc ?? 1));
|
|
84
|
+
bundler.on("error", () => resolvePromise(1));
|
|
85
|
+
});
|
|
86
|
+
child.on("error", () => resolvePromise(1));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
// Already built? One-line banner and we're done.
|
|
92
|
+
if (await distIsBuilt()) {
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
"claude-crap: ✓ prebuilt dist/ detected. Run `npx claude-crap install` to finish setup.\n",
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Not built yet — try to build with the bundled TypeScript. If
|
|
100
|
+
// TypeScript is missing (production-only install), warn and exit.
|
|
101
|
+
try {
|
|
102
|
+
await fs.access(join(PLUGIN_ROOT, "node_modules", "typescript", "bin", "tsc"));
|
|
103
|
+
} catch {
|
|
104
|
+
process.stderr.write(
|
|
105
|
+
"claude-crap: ! dist/ is missing and TypeScript is not installed. " +
|
|
106
|
+
"Run `npm install` with devDependencies enabled and then `npx claude-crap install`.\n",
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.stderr.write("claude-crap: building entrypoints ...\n");
|
|
112
|
+
const code = await runBuild();
|
|
113
|
+
if (code !== 0) {
|
|
114
|
+
process.stderr.write(
|
|
115
|
+
`claude-crap: ! build failed (exit ${code}). ` +
|
|
116
|
+
`Run \`npm run build && npm run build:plugin\` from ${PLUGIN_ROOT} to see the full error.\n`,
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
process.stderr.write("claude-crap: ✓ build complete. Next: `npx claude-crap install`.\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((err) => {
|
|
124
|
+
process.stderr.write(`claude-crap postinstall: ${err?.message ?? err}\n`);
|
|
125
|
+
// Do not fail the install — the user can still run the plugin.
|
|
126
|
+
process.exit(0);
|
|
127
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* Portable test runner for the `npm test` family of scripts.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: `node --test` on Node 20.x does NOT expand `**`
|
|
7
|
+
* recursive globs. It treats the literal pattern as a file path and
|
|
8
|
+
* fails with `Could not find '.../src/tests/**\/*.test.ts'`. Node
|
|
9
|
+
* 22+ handles the glob, which is why the suite passes locally on a
|
|
10
|
+
* developer machine running Node 22 but fails on the GitHub Actions
|
|
11
|
+
* Node 20 matrix job.
|
|
12
|
+
*
|
|
13
|
+
* Rather than pin Node 22 in CI (which would lock the `engines` floor
|
|
14
|
+
* at 22 and drop Node 20 support), this runner does the glob expansion
|
|
15
|
+
* in userland using `fast-glob` (already a runtime dependency) and
|
|
16
|
+
* hands the discovered file list to `node --test` as explicit paths.
|
|
17
|
+
* The result works on every Node release since 18.x, on every shell
|
|
18
|
+
* (bash / zsh / sh / Windows cmd), and on every OS.
|
|
19
|
+
*
|
|
20
|
+
* Usage from `package.json#scripts`:
|
|
21
|
+
*
|
|
22
|
+
* "test": "node ./scripts/run-tests.mjs \"./src/tests/**\/*.test.ts\"",
|
|
23
|
+
* "test:adapters": "node ./scripts/run-tests.mjs \"./src/tests/adapters/**\/*.test.ts\"",
|
|
24
|
+
* "test:integration": "node ./scripts/run-tests.mjs \"./src/tests/integration/**\/*.test.ts\"",
|
|
25
|
+
*
|
|
26
|
+
* Multiple patterns can be passed as separate arguments. Explicit
|
|
27
|
+
* file paths are accepted too — fast-glob returns them verbatim when
|
|
28
|
+
* no glob characters are present, so mixing files and patterns works.
|
|
29
|
+
*
|
|
30
|
+
* Exits with the subprocess exit code so CI reports test failures
|
|
31
|
+
* correctly. Exits non-zero immediately when no patterns are given
|
|
32
|
+
* or when a pattern matches zero files (both signal a misconfigured
|
|
33
|
+
* script, never a flaky runner).
|
|
34
|
+
*
|
|
35
|
+
* @module scripts/run-tests
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { spawn } from "node:child_process";
|
|
39
|
+
import { dirname, resolve } from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
|
|
42
|
+
import fastGlob from "fast-glob";
|
|
43
|
+
|
|
44
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const PLUGIN_ROOT = resolve(HERE, "..");
|
|
46
|
+
|
|
47
|
+
const patterns = process.argv.slice(2);
|
|
48
|
+
if (patterns.length === 0) {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
"[run-tests] no patterns supplied — usage: node ./scripts/run-tests.mjs <glob> [<glob>...]\n",
|
|
51
|
+
);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// fast-glob returns forward-slash POSIX paths even on Windows, which
|
|
56
|
+
// is exactly what `node --test` wants, so we do not normalize.
|
|
57
|
+
const files = await fastGlob(patterns, {
|
|
58
|
+
cwd: PLUGIN_ROOT,
|
|
59
|
+
absolute: false,
|
|
60
|
+
onlyFiles: true,
|
|
61
|
+
// `**` is fast-glob's default recursive matcher, matching the
|
|
62
|
+
// pattern shape we had in the previous `node --test` invocation.
|
|
63
|
+
// We keep `dot: false` so hidden files are not considered.
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (files.length === 0) {
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
`[run-tests] no test files matched any of: ${patterns.join(", ")}\n` +
|
|
69
|
+
`[run-tests] cwd: ${PLUGIN_ROOT}\n`,
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const child = spawn(
|
|
75
|
+
process.execPath,
|
|
76
|
+
["--import", "tsx", "--test", ...files],
|
|
77
|
+
{
|
|
78
|
+
stdio: "inherit",
|
|
79
|
+
cwd: PLUGIN_ROOT,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
child.on("error", (err) => {
|
|
84
|
+
process.stderr.write(`[run-tests] failed to spawn node --test: ${err.message}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
child.on("exit", (code, signal) => {
|
|
89
|
+
if (signal) {
|
|
90
|
+
process.stderr.write(`[run-tests] node --test killed by signal ${signal}\n`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
process.exit(code ?? 1);
|
|
95
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `claude-crap status` — show resolved paths and runtime state.
|
|
4
|
+
*
|
|
5
|
+
* Designed to be the first thing you run when someone asks
|
|
6
|
+
* "is claude-crap working?". It reports:
|
|
7
|
+
*
|
|
8
|
+
* - Plugin version (from package.json)
|
|
9
|
+
* - Plugin root (where the CLI resolved it to)
|
|
10
|
+
* - Node.js version in use
|
|
11
|
+
* - Whether dist/ is built
|
|
12
|
+
* - Whether the SARIF store has a consolidated report yet
|
|
13
|
+
* - Currently configured thresholds (CRAP, TDR rating, LOC cost)
|
|
14
|
+
*
|
|
15
|
+
* Does not attempt to verify anything — that's what `doctor` is for.
|
|
16
|
+
* Always exits 0.
|
|
17
|
+
*
|
|
18
|
+
* @module scripts/status
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { promises as fs } from "node:fs";
|
|
22
|
+
import { join, resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { printBanner, printKv, paint } from "./lib/cli-ui.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} CommandContext
|
|
28
|
+
* @property {string} pluginRoot
|
|
29
|
+
* @property {string[]} argv
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Status entrypoint.
|
|
34
|
+
*
|
|
35
|
+
* @param {CommandContext} ctx
|
|
36
|
+
* @returns {Promise<number>}
|
|
37
|
+
*/
|
|
38
|
+
export default async function status(ctx) {
|
|
39
|
+
printBanner("claude-crap :: status");
|
|
40
|
+
|
|
41
|
+
// -- plugin version
|
|
42
|
+
const pkg = await readJson(join(ctx.pluginRoot, "package.json"));
|
|
43
|
+
printKv("version", String(pkg.version ?? "unknown"));
|
|
44
|
+
|
|
45
|
+
// -- plugin root
|
|
46
|
+
printKv("plugin root", ctx.pluginRoot);
|
|
47
|
+
printKv("workspace cwd", process.cwd());
|
|
48
|
+
printKv("Node.js", process.versions.node);
|
|
49
|
+
|
|
50
|
+
// -- entrypoints
|
|
51
|
+
const distEntry = join(ctx.pluginRoot, "dist", "index.js");
|
|
52
|
+
const distOk = await exists(distEntry);
|
|
53
|
+
printKv("dist/index.js", distOk ? paint.green("built") : paint.red("MISSING"));
|
|
54
|
+
|
|
55
|
+
const gitEntry = join(ctx.pluginRoot, "plugin", "bundle", "mcp-server.mjs");
|
|
56
|
+
const gitOk = await exists(gitEntry);
|
|
57
|
+
printKv("plugin/bundle/...", gitOk ? paint.green("built") : paint.red("MISSING"));
|
|
58
|
+
|
|
59
|
+
// -- SARIF store
|
|
60
|
+
const sarifDir = resolve(process.cwd(), ".claude-crap", "reports");
|
|
61
|
+
const sarifPath = join(sarifDir, "latest.sarif");
|
|
62
|
+
const sarifOk = await exists(sarifPath);
|
|
63
|
+
printKv("SARIF report", sarifOk ? sarifPath : paint.yellow("<not yet generated>"));
|
|
64
|
+
|
|
65
|
+
// -- dashboard port
|
|
66
|
+
const port = process.env.CLAUDE_PLUGIN_OPTION_DASHBOARD_PORT ?? "5117";
|
|
67
|
+
printKv("dashboard port", port);
|
|
68
|
+
|
|
69
|
+
// -- thresholds
|
|
70
|
+
process.stdout.write(`\n${paint.bold(" Current policy (from env):")}\n`);
|
|
71
|
+
printKv("CRAP threshold", process.env.CLAUDE_PLUGIN_OPTION_CRAP_THRESHOLD ?? "30 (default)");
|
|
72
|
+
printKv("TDR max rating", process.env.CLAUDE_PLUGIN_OPTION_TDR_MAINTAINABILITY_MAX_RATING ?? "C (default)");
|
|
73
|
+
printKv("minutes / LOC", process.env.CLAUDE_PLUGIN_OPTION_MINUTES_PER_LINE_OF_CODE ?? "30 (default)");
|
|
74
|
+
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
`\n${paint.dim("Run `claude-crap doctor` for a full diagnostic pass.")}\n`,
|
|
77
|
+
);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read and parse a JSON file. Returns `{}` when the file is missing
|
|
83
|
+
* so the caller's property accesses always work.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} path
|
|
86
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
87
|
+
*/
|
|
88
|
+
async function readJson(path) {
|
|
89
|
+
try {
|
|
90
|
+
const raw = await fs.readFile(path, "utf8");
|
|
91
|
+
return JSON.parse(raw);
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `true` when `path` exists on disk and is readable.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} path
|
|
101
|
+
* @returns {Promise<boolean>}
|
|
102
|
+
*/
|
|
103
|
+
async function exists(path) {
|
|
104
|
+
try {
|
|
105
|
+
await fs.access(path);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* `claude-crap uninstall` — clean up plugin state and print the
|
|
4
|
+
* Claude Code unregister command.
|
|
5
|
+
*
|
|
6
|
+
* Like `install`, this subcommand does NOT try to edit Claude Code's
|
|
7
|
+
* own settings file. It:
|
|
8
|
+
*
|
|
9
|
+
* 1. Prints the native `/plugin uninstall claude-crap` command the
|
|
10
|
+
* user should run from inside Claude Code.
|
|
11
|
+
* 2. Optionally removes the `.claude-crap/` scratch directory from
|
|
12
|
+
* the current workspace when called with `--purge`. Without the
|
|
13
|
+
* flag we leave the SARIF reports in place so the user can diff
|
|
14
|
+
* them against a re-install.
|
|
15
|
+
*
|
|
16
|
+
* Always exits 0.
|
|
17
|
+
*
|
|
18
|
+
* @module scripts/uninstall
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { promises as fs } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { printBanner, paint, icons } from "./lib/cli-ui.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} CommandContext
|
|
28
|
+
* @property {string} pluginRoot
|
|
29
|
+
* @property {string[]} argv
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Uninstall entrypoint.
|
|
34
|
+
*
|
|
35
|
+
* @param {CommandContext} ctx
|
|
36
|
+
* @returns {Promise<number>}
|
|
37
|
+
*/
|
|
38
|
+
export default async function uninstall(ctx) {
|
|
39
|
+
printBanner("claude-crap :: uninstall");
|
|
40
|
+
|
|
41
|
+
const purge = ctx.argv.includes("--purge");
|
|
42
|
+
const scratch = resolve(process.cwd(), ".claude-crap");
|
|
43
|
+
|
|
44
|
+
if (purge) {
|
|
45
|
+
try {
|
|
46
|
+
await fs.rm(scratch, { recursive: true, force: true });
|
|
47
|
+
process.stdout.write(` ${paint.green(icons.ok)} Removed ${scratch}\n`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
` ${paint.yellow(icons.warn)} Could not remove ${scratch}: ${/** @type {Error} */ (err).message}\n`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
` ${paint.dim(icons.info)} Leaving ${scratch} in place. Use ${paint.bold("claude-crap uninstall --purge")} to remove SARIF reports.\n`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
[
|
|
61
|
+
"",
|
|
62
|
+
paint.bold(" Next step — unregister from Claude Code:"),
|
|
63
|
+
"",
|
|
64
|
+
` ${paint.cyan("/plugin uninstall claude-crap")}`,
|
|
65
|
+
"",
|
|
66
|
+
paint.dim(" If you installed via `npx`, also remove the npm package:"),
|
|
67
|
+
` ${paint.cyan("npm uninstall -g claude-crap")}`,
|
|
68
|
+
"",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bandit adapter.
|
|
3
|
+
*
|
|
4
|
+
* Bandit is a Python security linter. When run with `-f json` it
|
|
5
|
+
* emits a JSON report shaped like this (abbreviated):
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "results": [
|
|
9
|
+
* {
|
|
10
|
+
* "filename": "app.py",
|
|
11
|
+
* "line_number": 42,
|
|
12
|
+
* "col_offset": 5,
|
|
13
|
+
* "test_id": "B608",
|
|
14
|
+
* "test_name": "hardcoded_sql_expressions",
|
|
15
|
+
* "issue_severity": "HIGH", // LOW | MEDIUM | HIGH
|
|
16
|
+
* "issue_confidence": "HIGH", // LOW | MEDIUM | HIGH
|
|
17
|
+
* "issue_text": "Possible SQL injection via string-based query construction.",
|
|
18
|
+
* "issue_cwe": { "id": 89 }
|
|
19
|
+
* }
|
|
20
|
+
* ],
|
|
21
|
+
* "metrics": { ... },
|
|
22
|
+
* "errors": [ ... ]
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* This adapter converts each `results[]` entry into a SARIF 2.1.0
|
|
26
|
+
* `result`, mapping Bandit severity levels to SARIF levels:
|
|
27
|
+
*
|
|
28
|
+
* LOW → "note"
|
|
29
|
+
* MEDIUM → "warning"
|
|
30
|
+
* HIGH → "error"
|
|
31
|
+
*
|
|
32
|
+
* Every finding gets a rule id of `bandit.<test_id>` (e.g.
|
|
33
|
+
* `bandit.B608`) so it is trivial to correlate with Bandit's own docs
|
|
34
|
+
* from inside the claude-crap dashboard.
|
|
35
|
+
*
|
|
36
|
+
* @module adapters/bandit
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
40
|
+
import {
|
|
41
|
+
estimateEffortMinutes,
|
|
42
|
+
wrapResultsInSarif,
|
|
43
|
+
type AdapterResult,
|
|
44
|
+
type KnownScanner,
|
|
45
|
+
} from "./common.js";
|
|
46
|
+
|
|
47
|
+
const BANDIT: KnownScanner = "bandit";
|
|
48
|
+
|
|
49
|
+
interface BanditReport {
|
|
50
|
+
readonly results?: ReadonlyArray<BanditFinding>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BanditFinding {
|
|
54
|
+
readonly filename?: string;
|
|
55
|
+
readonly line_number?: number;
|
|
56
|
+
readonly col_offset?: number;
|
|
57
|
+
readonly test_id?: string;
|
|
58
|
+
readonly test_name?: string;
|
|
59
|
+
readonly issue_severity?: string;
|
|
60
|
+
readonly issue_confidence?: string;
|
|
61
|
+
readonly issue_text?: string;
|
|
62
|
+
readonly issue_cwe?: { readonly id?: number };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Accept a Bandit JSON report and return a normalized
|
|
67
|
+
* `PersistedSarif` document.
|
|
68
|
+
*
|
|
69
|
+
* @param input Raw Bandit JSON (string or parsed object).
|
|
70
|
+
* @returns Adapter result.
|
|
71
|
+
* @throws When the input does not look like a Bandit report.
|
|
72
|
+
*/
|
|
73
|
+
export function adaptBandit(input: unknown): AdapterResult {
|
|
74
|
+
const parsed = typeof input === "string" ? (JSON.parse(input) as unknown) : input;
|
|
75
|
+
if (!parsed || typeof parsed !== "object") {
|
|
76
|
+
throw new Error(`[adapter:bandit] expected a JSON object`);
|
|
77
|
+
}
|
|
78
|
+
const report = parsed as BanditReport;
|
|
79
|
+
if (!Array.isArray(report.results)) {
|
|
80
|
+
throw new Error(`[adapter:bandit] report is missing a results[] array`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const results: Array<ReturnType<typeof buildSarifResult>> = [];
|
|
84
|
+
let totalEffortMinutes = 0;
|
|
85
|
+
|
|
86
|
+
for (const finding of report.results) {
|
|
87
|
+
const filename = finding.filename;
|
|
88
|
+
if (typeof filename !== "string" || !filename) continue;
|
|
89
|
+
const level = mapSeverity(finding.issue_severity);
|
|
90
|
+
// High-severity security findings cost more to fix than the
|
|
91
|
+
// generic default, so we bias the budget toward reality. Bandit
|
|
92
|
+
// is always security-focused, so every finding is treated as a
|
|
93
|
+
// security issue for TDR accounting downstream.
|
|
94
|
+
const effortOverride = level === "error" ? 120 : level === "warning" ? 60 : 20;
|
|
95
|
+
const effort = estimateEffortMinutes(level, effortOverride);
|
|
96
|
+
totalEffortMinutes += effort;
|
|
97
|
+
|
|
98
|
+
const testId = finding.test_id ?? "unknown";
|
|
99
|
+
const ruleId = `bandit.${testId}`;
|
|
100
|
+
const messageText =
|
|
101
|
+
finding.issue_text ??
|
|
102
|
+
`${finding.test_name ?? "Bandit finding"} (${finding.issue_severity ?? "UNKNOWN"})`;
|
|
103
|
+
|
|
104
|
+
const startLine =
|
|
105
|
+
typeof finding.line_number === "number" && finding.line_number > 0
|
|
106
|
+
? finding.line_number
|
|
107
|
+
: 1;
|
|
108
|
+
const startColumn =
|
|
109
|
+
typeof finding.col_offset === "number" && finding.col_offset >= 0
|
|
110
|
+
? finding.col_offset + 1
|
|
111
|
+
: 1;
|
|
112
|
+
|
|
113
|
+
results.push(
|
|
114
|
+
buildSarifResult({
|
|
115
|
+
ruleId,
|
|
116
|
+
level,
|
|
117
|
+
message: messageText,
|
|
118
|
+
uri: filename,
|
|
119
|
+
startLine,
|
|
120
|
+
startColumn,
|
|
121
|
+
effortMinutes: effort,
|
|
122
|
+
cwe: finding.issue_cwe?.id,
|
|
123
|
+
confidence: finding.issue_confidence,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
document: wrapResultsInSarif(BANDIT, "unknown", results),
|
|
130
|
+
sourceTool: BANDIT,
|
|
131
|
+
findingCount: results.length,
|
|
132
|
+
totalEffortMinutes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Map Bandit's `issue_severity` string to a SARIF level. Unknown
|
|
138
|
+
* values default to `"warning"` so findings are still surfaced.
|
|
139
|
+
*/
|
|
140
|
+
function mapSeverity(severity: string | undefined): SarifLevel {
|
|
141
|
+
switch ((severity ?? "").toUpperCase()) {
|
|
142
|
+
case "HIGH":
|
|
143
|
+
return "error";
|
|
144
|
+
case "MEDIUM":
|
|
145
|
+
return "warning";
|
|
146
|
+
case "LOW":
|
|
147
|
+
return "note";
|
|
148
|
+
default:
|
|
149
|
+
return "warning";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the SARIF `result` object for a single Bandit finding. We
|
|
155
|
+
* stash the CWE id and Bandit confidence in the `properties` bag so
|
|
156
|
+
* consumers can surface them in the dashboard hot-spot view.
|
|
157
|
+
*/
|
|
158
|
+
function buildSarifResult(opts: {
|
|
159
|
+
ruleId: string;
|
|
160
|
+
level: SarifLevel;
|
|
161
|
+
message: string;
|
|
162
|
+
uri: string;
|
|
163
|
+
startLine: number;
|
|
164
|
+
startColumn: number;
|
|
165
|
+
effortMinutes: number;
|
|
166
|
+
cwe?: number | undefined;
|
|
167
|
+
confidence?: string | undefined;
|
|
168
|
+
}) {
|
|
169
|
+
return {
|
|
170
|
+
ruleId: opts.ruleId,
|
|
171
|
+
level: opts.level,
|
|
172
|
+
message: { text: opts.message },
|
|
173
|
+
locations: [
|
|
174
|
+
{
|
|
175
|
+
physicalLocation: {
|
|
176
|
+
artifactLocation: { uri: opts.uri },
|
|
177
|
+
region: {
|
|
178
|
+
startLine: opts.startLine,
|
|
179
|
+
startColumn: opts.startColumn,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
properties: {
|
|
185
|
+
sourceTool: BANDIT,
|
|
186
|
+
effortMinutes: opts.effortMinutes,
|
|
187
|
+
...(typeof opts.cwe === "number" ? { cwe: opts.cwe } : {}),
|
|
188
|
+
...(typeof opts.confidence === "string" ? { confidence: opts.confidence } : {}),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|