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.
Files changed (202) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/LICENSE +21 -0
  3. package/README.md +550 -0
  4. package/bin/claude-crap.mjs +141 -0
  5. package/dist/adapters/bandit.d.ts +48 -0
  6. package/dist/adapters/bandit.d.ts.map +1 -0
  7. package/dist/adapters/bandit.js +145 -0
  8. package/dist/adapters/bandit.js.map +1 -0
  9. package/dist/adapters/common.d.ts +73 -0
  10. package/dist/adapters/common.d.ts.map +1 -0
  11. package/dist/adapters/common.js +78 -0
  12. package/dist/adapters/common.js.map +1 -0
  13. package/dist/adapters/eslint.d.ts +52 -0
  14. package/dist/adapters/eslint.d.ts.map +1 -0
  15. package/dist/adapters/eslint.js +142 -0
  16. package/dist/adapters/eslint.js.map +1 -0
  17. package/dist/adapters/index.d.ts +47 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +64 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/semgrep.d.ts +30 -0
  22. package/dist/adapters/semgrep.d.ts.map +1 -0
  23. package/dist/adapters/semgrep.js +130 -0
  24. package/dist/adapters/semgrep.js.map +1 -0
  25. package/dist/adapters/stryker.d.ts +55 -0
  26. package/dist/adapters/stryker.d.ts.map +1 -0
  27. package/dist/adapters/stryker.js +165 -0
  28. package/dist/adapters/stryker.js.map +1 -0
  29. package/dist/ast/cyclomatic.d.ts +48 -0
  30. package/dist/ast/cyclomatic.d.ts.map +1 -0
  31. package/dist/ast/cyclomatic.js +106 -0
  32. package/dist/ast/cyclomatic.js.map +1 -0
  33. package/dist/ast/index.d.ts +26 -0
  34. package/dist/ast/index.d.ts.map +1 -0
  35. package/dist/ast/index.js +23 -0
  36. package/dist/ast/index.js.map +1 -0
  37. package/dist/ast/language-config.d.ts +70 -0
  38. package/dist/ast/language-config.d.ts.map +1 -0
  39. package/dist/ast/language-config.js +192 -0
  40. package/dist/ast/language-config.js.map +1 -0
  41. package/dist/ast/tree-sitter-engine.d.ts +133 -0
  42. package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
  43. package/dist/ast/tree-sitter-engine.js +270 -0
  44. package/dist/ast/tree-sitter-engine.js.map +1 -0
  45. package/dist/config.d.ts +57 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +78 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/crap-config.d.ts +97 -0
  50. package/dist/crap-config.d.ts.map +1 -0
  51. package/dist/crap-config.js +144 -0
  52. package/dist/crap-config.js.map +1 -0
  53. package/dist/dashboard/server.d.ts +65 -0
  54. package/dist/dashboard/server.d.ts.map +1 -0
  55. package/dist/dashboard/server.js +147 -0
  56. package/dist/dashboard/server.js.map +1 -0
  57. package/dist/index.d.ts +32 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +574 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/metrics/crap.d.ts +71 -0
  62. package/dist/metrics/crap.d.ts.map +1 -0
  63. package/dist/metrics/crap.js +67 -0
  64. package/dist/metrics/crap.js.map +1 -0
  65. package/dist/metrics/index.d.ts +31 -0
  66. package/dist/metrics/index.d.ts.map +1 -0
  67. package/dist/metrics/index.js +27 -0
  68. package/dist/metrics/index.js.map +1 -0
  69. package/dist/metrics/score.d.ts +143 -0
  70. package/dist/metrics/score.d.ts.map +1 -0
  71. package/dist/metrics/score.js +224 -0
  72. package/dist/metrics/score.js.map +1 -0
  73. package/dist/metrics/tdr.d.ts +106 -0
  74. package/dist/metrics/tdr.d.ts.map +1 -0
  75. package/dist/metrics/tdr.js +117 -0
  76. package/dist/metrics/tdr.js.map +1 -0
  77. package/dist/metrics/workspace-walker.d.ts +43 -0
  78. package/dist/metrics/workspace-walker.d.ts.map +1 -0
  79. package/dist/metrics/workspace-walker.js +137 -0
  80. package/dist/metrics/workspace-walker.js.map +1 -0
  81. package/dist/sarif/index.d.ts +21 -0
  82. package/dist/sarif/index.d.ts.map +1 -0
  83. package/dist/sarif/index.js +19 -0
  84. package/dist/sarif/index.js.map +1 -0
  85. package/dist/sarif/sarif-builder.d.ts +128 -0
  86. package/dist/sarif/sarif-builder.d.ts.map +1 -0
  87. package/dist/sarif/sarif-builder.js +79 -0
  88. package/dist/sarif/sarif-builder.js.map +1 -0
  89. package/dist/sarif/sarif-store.d.ts +205 -0
  90. package/dist/sarif/sarif-store.d.ts.map +1 -0
  91. package/dist/sarif/sarif-store.js +246 -0
  92. package/dist/sarif/sarif-store.js.map +1 -0
  93. package/dist/sarif/sarif-validator.d.ts +45 -0
  94. package/dist/sarif/sarif-validator.d.ts.map +1 -0
  95. package/dist/sarif/sarif-validator.js +138 -0
  96. package/dist/sarif/sarif-validator.js.map +1 -0
  97. package/dist/schemas/tool-schemas.d.ts +216 -0
  98. package/dist/schemas/tool-schemas.d.ts.map +1 -0
  99. package/dist/schemas/tool-schemas.js +208 -0
  100. package/dist/schemas/tool-schemas.js.map +1 -0
  101. package/dist/sdk.d.ts +45 -0
  102. package/dist/sdk.d.ts.map +1 -0
  103. package/dist/sdk.js +44 -0
  104. package/dist/sdk.js.map +1 -0
  105. package/dist/tools/index.d.ts +24 -0
  106. package/dist/tools/index.d.ts.map +1 -0
  107. package/dist/tools/index.js +23 -0
  108. package/dist/tools/index.js.map +1 -0
  109. package/dist/tools/test-harness.d.ts +75 -0
  110. package/dist/tools/test-harness.d.ts.map +1 -0
  111. package/dist/tools/test-harness.js +137 -0
  112. package/dist/tools/test-harness.js.map +1 -0
  113. package/dist/workspace-guard.d.ts +53 -0
  114. package/dist/workspace-guard.d.ts.map +1 -0
  115. package/dist/workspace-guard.js +61 -0
  116. package/dist/workspace-guard.js.map +1 -0
  117. package/package.json +133 -0
  118. package/plugin/.claude-plugin/plugin.json +29 -0
  119. package/plugin/.mcp.json +18 -0
  120. package/plugin/CLAUDE.md +143 -0
  121. package/plugin/bundle/dashboard/public/index.html +368 -0
  122. package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
  123. package/plugin/bundle/mcp-server.mjs +8718 -0
  124. package/plugin/bundle/mcp-server.mjs.map +7 -0
  125. package/plugin/bundle/tdr-engine.mjs +50 -0
  126. package/plugin/bundle/tdr-engine.mjs.map +7 -0
  127. package/plugin/hooks/hooks.json +62 -0
  128. package/plugin/hooks/lib/crap-config.mjs +152 -0
  129. package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
  130. package/plugin/hooks/lib/hook-io.mjs +151 -0
  131. package/plugin/hooks/lib/quality-gate.mjs +329 -0
  132. package/plugin/hooks/lib/test-harness.mjs +152 -0
  133. package/plugin/hooks/post-tool-use.mjs +245 -0
  134. package/plugin/hooks/pre-tool-use.mjs +290 -0
  135. package/plugin/hooks/session-start.mjs +109 -0
  136. package/plugin/hooks/stop-quality-gate.mjs +226 -0
  137. package/plugin/package.json +18 -0
  138. package/plugin/skills/adopt/SKILL.md +74 -0
  139. package/plugin/skills/analyze/SKILL.md +77 -0
  140. package/plugin/skills/check-test/SKILL.md +50 -0
  141. package/plugin/skills/score/SKILL.md +31 -0
  142. package/scripts/bug-report.mjs +328 -0
  143. package/scripts/build-fast.mjs +130 -0
  144. package/scripts/bundle-plugin.mjs +74 -0
  145. package/scripts/doctor.mjs +320 -0
  146. package/scripts/install.mjs +192 -0
  147. package/scripts/lib/cli-ui.mjs +122 -0
  148. package/scripts/postinstall.mjs +127 -0
  149. package/scripts/run-tests.mjs +95 -0
  150. package/scripts/status.mjs +110 -0
  151. package/scripts/uninstall.mjs +72 -0
  152. package/src/adapters/bandit.ts +191 -0
  153. package/src/adapters/common.ts +133 -0
  154. package/src/adapters/eslint.ts +187 -0
  155. package/src/adapters/index.ts +78 -0
  156. package/src/adapters/semgrep.ts +150 -0
  157. package/src/adapters/stryker.ts +218 -0
  158. package/src/ast/cyclomatic.ts +131 -0
  159. package/src/ast/index.ts +33 -0
  160. package/src/ast/language-config.ts +231 -0
  161. package/src/ast/tree-sitter-engine.ts +385 -0
  162. package/src/config.ts +109 -0
  163. package/src/crap-config.ts +196 -0
  164. package/src/dashboard/public/index.html +368 -0
  165. package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
  166. package/src/dashboard/server.ts +205 -0
  167. package/src/index.ts +696 -0
  168. package/src/metrics/crap.ts +101 -0
  169. package/src/metrics/index.ts +51 -0
  170. package/src/metrics/score.ts +329 -0
  171. package/src/metrics/tdr.ts +155 -0
  172. package/src/metrics/workspace-walker.ts +146 -0
  173. package/src/sarif/index.ts +31 -0
  174. package/src/sarif/sarif-builder.ts +139 -0
  175. package/src/sarif/sarif-store.ts +347 -0
  176. package/src/sarif/sarif-validator.ts +145 -0
  177. package/src/schemas/tool-schemas.ts +225 -0
  178. package/src/sdk.ts +110 -0
  179. package/src/tests/adapters/bandit.test.ts +111 -0
  180. package/src/tests/adapters/dispatch.test.ts +100 -0
  181. package/src/tests/adapters/eslint.test.ts +138 -0
  182. package/src/tests/adapters/semgrep.test.ts +125 -0
  183. package/src/tests/adapters/stryker.test.ts +103 -0
  184. package/src/tests/crap-config.test.ts +228 -0
  185. package/src/tests/crap.test.ts +59 -0
  186. package/src/tests/cyclomatic.test.ts +87 -0
  187. package/src/tests/dashboard-http.test.ts +108 -0
  188. package/src/tests/dashboard-integrity.test.ts +128 -0
  189. package/src/tests/integration/mcp-server.integration.test.ts +352 -0
  190. package/src/tests/pre-tool-use-hook.test.ts +178 -0
  191. package/src/tests/sarif-store.test.ts +241 -0
  192. package/src/tests/sarif-validator.test.ts +164 -0
  193. package/src/tests/score.test.ts +260 -0
  194. package/src/tests/skills-frontmatter.test.ts +172 -0
  195. package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
  196. package/src/tests/tdr.test.ts +86 -0
  197. package/src/tests/test-harness.test.ts +153 -0
  198. package/src/tests/workspace-guard.test.ts +111 -0
  199. package/src/tools/index.ts +24 -0
  200. package/src/tools/test-harness.ts +158 -0
  201. package/src/workspace-guard.ts +64 -0
  202. 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
+ }