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,328 @@
1
+ // @ts-check
2
+ /**
3
+ * `claude-crap bug-report` — generate a diagnostic bundle for triage.
4
+ *
5
+ * Inspired by claude-mem's `npm run bug-report`. Collects every piece
6
+ * of information a maintainer typically asks for when triaging an
7
+ * issue and writes it to a single Markdown file under the current
8
+ * workspace (or stdout with `--stdout`).
9
+ *
10
+ * The report never contains secrets — we redact every environment
11
+ * variable whose name looks sensitive (`*TOKEN`, `*KEY`, `*PASSWORD`,
12
+ * etc.) before writing it. The user should still skim the file before
13
+ * pasting it into a public issue, but the defaults are safe.
14
+ *
15
+ * Contents of the report:
16
+ *
17
+ * 1. Header — timestamp, plugin version, Node version, OS
18
+ * 2. Plugin file presence (same 9 files the doctor checks)
19
+ * 3. Resolved CLAUDE_PLUGIN_OPTION_* env vars (redacted)
20
+ * 4. Build state — `dist/index.js` mtime and size
21
+ * 5. `doctor` output — we run the doctor subcommand and capture its stdout
22
+ * 6. Recent SARIF report summary if one exists
23
+ * 7. Hook script permission bits
24
+ *
25
+ * Usage:
26
+ *
27
+ * node ./scripts/bug-report.mjs # writes ./claude-crap-bug-report-<ts>.md
28
+ * node ./scripts/bug-report.mjs --stdout # prints to stdout instead
29
+ * node ./scripts/bug-report.mjs -o foo.md # explicit output path
30
+ *
31
+ * Exits 0 on success regardless of individual check outcomes —
32
+ * collecting the report is the goal, not passing every probe.
33
+ *
34
+ * @module scripts/bug-report
35
+ */
36
+
37
+ import { spawnSync } from "node:child_process";
38
+ import { promises as fs } from "node:fs";
39
+ import { createRequire } from "node:module";
40
+ import { arch, platform, release, userInfo } from "node:os";
41
+ import { dirname, join, resolve } from "node:path";
42
+ import { fileURLToPath } from "node:url";
43
+
44
+ const HERE = dirname(fileURLToPath(import.meta.url));
45
+ const PLUGIN_ROOT = resolve(HERE, "..");
46
+
47
+ const REQUIRED_FILES = [
48
+ "package.json",
49
+ "plugin/.claude-plugin/plugin.json",
50
+ "plugin/.mcp.json",
51
+ "plugin/CLAUDE.md",
52
+ "plugin/hooks/hooks.json",
53
+ "plugin/hooks/pre-tool-use.mjs",
54
+ "plugin/hooks/post-tool-use.mjs",
55
+ "plugin/hooks/stop-quality-gate.mjs",
56
+ "plugin/hooks/session-start.mjs",
57
+ "dist/index.js",
58
+ "plugin/bundle/mcp-server.mjs",
59
+ ];
60
+
61
+ /**
62
+ * Regex identifying env var names that should be redacted. We never
63
+ * write the actual value for matches — just the name and `<redacted>`.
64
+ */
65
+ const SENSITIVE_ENV_PATTERN = /(TOKEN|KEY|SECRET|PASSWORD|AUTH|COOKIE|CREDENTIAL|BEARER)/i;
66
+
67
+ /**
68
+ * Entry point. When required as a module, use {@link generateBugReport}
69
+ * directly — this default export is just the CLI wrapper.
70
+ *
71
+ * @param {{pluginRoot: string, argv: string[]}} ctx
72
+ * @returns {Promise<number>}
73
+ */
74
+ export default async function bugReportCommand(ctx) {
75
+ const argv = ctx.argv;
76
+ const useStdout = argv.includes("--stdout");
77
+ const outIdx = argv.indexOf("-o");
78
+ const explicitOut = outIdx >= 0 ? argv[outIdx + 1] : undefined;
79
+
80
+ const report = await generateBugReport(ctx.pluginRoot);
81
+
82
+ if (useStdout) {
83
+ process.stdout.write(report);
84
+ return 0;
85
+ }
86
+
87
+ const outPath =
88
+ explicitOut ??
89
+ join(process.cwd(), `claude-crap-bug-report-${timestampSlug()}.md`);
90
+ await fs.writeFile(outPath, report, "utf8");
91
+ process.stdout.write(`claude-crap: bug report written to ${outPath}\n`);
92
+ return 0;
93
+ }
94
+
95
+ /**
96
+ * Build the full Markdown bug report string. Pure-ish — reads env and
97
+ * filesystem but does not write anywhere. Exported so other scripts
98
+ * (or tests) can embed it without shelling out.
99
+ *
100
+ * @param {string} pluginRoot Absolute plugin root.
101
+ * @returns {Promise<string>}
102
+ */
103
+ export async function generateBugReport(pluginRoot) {
104
+ const lines = [];
105
+
106
+ // --- header ---
107
+ lines.push(`# claude-crap bug report`);
108
+ lines.push("");
109
+ lines.push(`_Generated: ${new Date().toISOString()}_`);
110
+ lines.push("");
111
+
112
+ const version = await readPackageVersion(pluginRoot);
113
+ lines.push(`| Field | Value |`);
114
+ lines.push(`| --- | --- |`);
115
+ lines.push(`| claude-crap version | \`${version}\` |`);
116
+ lines.push(`| Node.js | \`${process.versions.node}\` |`);
117
+ lines.push(`| npm | \`${tryExec("npm", ["-v"])}\` |`);
118
+ lines.push(`| Platform | \`${platform()} ${release()} ${arch()}\` |`);
119
+ lines.push(`| Shell | \`${process.env.SHELL ?? "unknown"}\` |`);
120
+ lines.push(`| User (login) | \`${safeUser()}\` |`);
121
+ lines.push(`| CWD | \`${process.cwd()}\` |`);
122
+ lines.push(`| Plugin root | \`${pluginRoot}\` |`);
123
+ lines.push("");
124
+
125
+ // --- plugin files ---
126
+ lines.push(`## Plugin files`);
127
+ lines.push("");
128
+ lines.push(`| File | Present |`);
129
+ lines.push(`| --- | :---: |`);
130
+ for (const rel of REQUIRED_FILES) {
131
+ const full = join(pluginRoot, rel);
132
+ const present = await exists(full);
133
+ lines.push(`| \`${rel}\` | ${present ? "✅" : "❌"} |`);
134
+ }
135
+ lines.push("");
136
+
137
+ // --- build state ---
138
+ lines.push(`## Build state`);
139
+ lines.push("");
140
+ const distEntry = join(pluginRoot, "dist", "index.js");
141
+ try {
142
+ const stat = await fs.stat(distEntry);
143
+ lines.push(`- npm entry: \`${distEntry}\``);
144
+ lines.push(` - Size: ${stat.size} bytes`);
145
+ lines.push(` - Modified: ${new Date(stat.mtimeMs).toISOString()}`);
146
+ } catch {
147
+ lines.push(`- npm entry: \`${distEntry}\` is missing. Run \`npm run build\`.`);
148
+ }
149
+ const gitEntry = join(pluginRoot, "plugin", "bundle", "mcp-server.mjs");
150
+ try {
151
+ const stat = await fs.stat(gitEntry);
152
+ lines.push(`- git entry: \`${gitEntry}\``);
153
+ lines.push(` - Size: ${stat.size} bytes`);
154
+ lines.push(` - Modified: ${new Date(stat.mtimeMs).toISOString()}`);
155
+ } catch {
156
+ lines.push(`- git entry: \`${gitEntry}\` is missing. Run \`npm run build:plugin\`.`);
157
+ }
158
+ lines.push("");
159
+
160
+ // --- env vars (redacted) ---
161
+ lines.push(`## Environment (CLAUDE_* + CLAUDE_PLUGIN_OPTION_*, redacted)`);
162
+ lines.push("");
163
+ const relevantEnv = Object.entries(process.env)
164
+ .filter(([k]) => k.startsWith("CLAUDE"))
165
+ .sort(([a], [b]) => a.localeCompare(b));
166
+ if (relevantEnv.length === 0) {
167
+ lines.push(`_No claude-crap env vars set._`);
168
+ } else {
169
+ lines.push("```");
170
+ for (const [k, v] of relevantEnv) {
171
+ const value =
172
+ SENSITIVE_ENV_PATTERN.test(k) && v && v.length > 0 ? "<redacted>" : String(v ?? "");
173
+ lines.push(`${k}=${value}`);
174
+ }
175
+ lines.push("```");
176
+ }
177
+ lines.push("");
178
+
179
+ // --- doctor output ---
180
+ lines.push(`## \`claude-crap doctor\` output`);
181
+ lines.push("");
182
+ lines.push("```");
183
+ const doctorBin = join(pluginRoot, "bin", "claude-crap.mjs");
184
+ const doctor = spawnSync(process.execPath, [doctorBin, "doctor"], {
185
+ env: { ...process.env, NO_COLOR: "1" },
186
+ encoding: "utf8",
187
+ });
188
+ lines.push(stripAnsi(doctor.stdout ?? ""));
189
+ if (doctor.stderr) lines.push(stripAnsi(doctor.stderr));
190
+ lines.push(`[exit code: ${doctor.status ?? "null"}]`);
191
+ lines.push("```");
192
+ lines.push("");
193
+
194
+ // --- SARIF report (if any) ---
195
+ lines.push(`## Consolidated SARIF report`);
196
+ lines.push("");
197
+ const sarifPath = join(process.cwd(), ".claude-crap", "reports", "latest.sarif");
198
+ if (await exists(sarifPath)) {
199
+ try {
200
+ const raw = await fs.readFile(sarifPath, "utf8");
201
+ const doc = JSON.parse(raw);
202
+ const findings =
203
+ Array.isArray(doc?.runs) && Array.isArray(doc.runs[0]?.results)
204
+ ? doc.runs[0].results.length
205
+ : 0;
206
+ lines.push(`- Path: \`${sarifPath}\``);
207
+ lines.push(`- SARIF version: \`${doc.version ?? "unknown"}\``);
208
+ lines.push(`- Findings: ${findings}`);
209
+ } catch (err) {
210
+ lines.push(`- Could not parse \`${sarifPath}\`: ${/** @type {Error} */ (err).message}`);
211
+ }
212
+ } else {
213
+ lines.push(`_No consolidated SARIF report at \`${sarifPath}\` yet._`);
214
+ }
215
+ lines.push("");
216
+
217
+ // --- footer ---
218
+ lines.push(`## How to file this`);
219
+ lines.push("");
220
+ lines.push(
221
+ `1. Review this file for anything sensitive that slipped past the redactor.`,
222
+ );
223
+ lines.push(`2. Open a new issue at the plugin repository.`);
224
+ lines.push(`3. Paste this entire file as the issue body, or attach it directly.`);
225
+ lines.push("");
226
+
227
+ return lines.join("\n") + "\n";
228
+ }
229
+
230
+ /**
231
+ * Read the plugin version from `package.json`.
232
+ *
233
+ * @param {string} pluginRoot
234
+ * @returns {Promise<string>}
235
+ */
236
+ async function readPackageVersion(pluginRoot) {
237
+ try {
238
+ const require_ = createRequire(import.meta.url);
239
+ const pkg = require_(join(pluginRoot, "package.json"));
240
+ return String(pkg.version ?? "unknown");
241
+ } catch {
242
+ return "unknown";
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Run a shell command and return its trimmed stdout, or `"unknown"`
248
+ * when it fails. Used for optional diagnostics like `npm -v` that
249
+ * should never fail the report.
250
+ *
251
+ * @param {string} cmd
252
+ * @param {string[]} args
253
+ * @returns {string}
254
+ */
255
+ function tryExec(cmd, args) {
256
+ try {
257
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
258
+ return (r.stdout ?? "unknown").trim();
259
+ } catch {
260
+ return "unknown";
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Return the current login name, defaulting to `"unknown"` when we
266
+ * cannot read it (sandboxed environments, non-interactive runs).
267
+ *
268
+ * @returns {string}
269
+ */
270
+ function safeUser() {
271
+ try {
272
+ return userInfo().username;
273
+ } catch {
274
+ return "unknown";
275
+ }
276
+ }
277
+
278
+ /**
279
+ * `true` when `path` exists on disk.
280
+ *
281
+ * @param {string} path
282
+ * @returns {Promise<boolean>}
283
+ */
284
+ async function exists(path) {
285
+ try {
286
+ await fs.access(path);
287
+ return true;
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Strip ANSI color escapes from a string. The doctor command inherits
295
+ * `NO_COLOR=1` so its output should already be plain, but we defend
296
+ * against pino loggers or npm wrappers that inject colors anyway.
297
+ *
298
+ * @param {string} input
299
+ * @returns {string}
300
+ */
301
+ function stripAnsi(input) {
302
+ // eslint-disable-next-line no-control-regex
303
+ return input.replace(/\x1b\[[0-9;]*m/g, "");
304
+ }
305
+
306
+ /**
307
+ * Filesystem-safe ISO timestamp suitable for a filename (no colons).
308
+ *
309
+ * @returns {string}
310
+ */
311
+ function timestampSlug() {
312
+ return new Date().toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
313
+ }
314
+
315
+ // When run directly via `node ./scripts/bug-report.mjs`, delegate to
316
+ // the CLI wrapper. When imported by bin/claude-crap.mjs, the default
317
+ // export is consumed instead.
318
+ const isDirectInvocation =
319
+ import.meta.url === `file://${process.argv[1]}` ||
320
+ process.argv[1]?.endsWith("bug-report.mjs");
321
+ if (isDirectInvocation) {
322
+ bugReportCommand({ pluginRoot: PLUGIN_ROOT, argv: process.argv.slice(2) })
323
+ .then((code) => process.exit(code ?? 0))
324
+ .catch((err) => {
325
+ process.stderr.write(`claude-crap bug-report: ${err?.message ?? err}\n`);
326
+ process.exit(1);
327
+ });
328
+ }
@@ -0,0 +1,130 @@
1
+ // @ts-check
2
+ /**
3
+ * `npm run build:fast` — fast dev build using esbuild.
4
+ *
5
+ * `tsc` remains the canonical build tool (it produces `.d.ts`
6
+ * declaration files, runs the full type checker, and is the one the
7
+ * CI pipeline + `np release` call) — but for the inner loop it is
8
+ * slow to feel. esbuild compiles the entire `src/` tree to `dist/`
9
+ * in under 100 ms on a modern laptop, which matters when you're
10
+ * rebuilding after every keystroke.
11
+ *
12
+ * Trade-offs vs. `tsc`:
13
+ *
14
+ * - ✅ 10-20x faster
15
+ * - ❌ No type checking (run `npm run typecheck` separately)
16
+ * - ❌ No `.d.ts` files (so `exports` with `types` would not work)
17
+ * - ❌ No declaration maps
18
+ *
19
+ * The script walks `src/` for all `.ts` files (excluding tests and
20
+ * declaration files), then hands them to esbuild as the entry point
21
+ * list so every file gets its own output mirror. That matches what
22
+ * `tsc` does and keeps the `exports` map working.
23
+ *
24
+ * This build mode is strictly for local dev feedback — never use it
25
+ * to publish an npm release. `prepublishOnly` always calls `tsc`.
26
+ *
27
+ * Skip this build with `CLAUDE_CRAP_SKIP_FAST_BUILD=1` to short-circuit.
28
+ *
29
+ * @module scripts/build-fast
30
+ */
31
+
32
+ import { build } from "esbuild";
33
+ import { promises as fs } from "node:fs";
34
+ import { dirname, join, relative, resolve } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+
37
+ const HERE = dirname(fileURLToPath(import.meta.url));
38
+ const PLUGIN_ROOT = resolve(HERE, "..");
39
+ const SRC_DIR = join(PLUGIN_ROOT, "src");
40
+ const DIST_DIR = join(PLUGIN_ROOT, "dist");
41
+
42
+ /**
43
+ * Recursively walk a directory and yield every `.ts` file path that
44
+ * should be compiled. Mirrors the `tsconfig.json` include/exclude
45
+ * lists so the two build modes produce the same output set.
46
+ *
47
+ * @param {string} dir Absolute directory to walk.
48
+ * @returns {AsyncGenerator<string>}
49
+ */
50
+ async function* walkSources(dir) {
51
+ const entries = await fs.readdir(dir, { withFileTypes: true });
52
+ for (const entry of entries) {
53
+ const full = join(dir, entry.name);
54
+ if (entry.isDirectory()) {
55
+ // Tests are compiled by tsc and run via tsx, never by esbuild.
56
+ if (entry.name === "tests") continue;
57
+ yield* walkSources(full);
58
+ continue;
59
+ }
60
+ if (!entry.isFile()) continue;
61
+ if (!entry.name.endsWith(".ts")) continue;
62
+ if (entry.name.endsWith(".d.ts")) continue;
63
+ if (entry.name.endsWith(".test.ts")) continue;
64
+ yield full;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Replace the trailing `.ts` of an absolute source path with `.js`
70
+ * and relocate it from `src/` to `dist/`. Mirrors exactly what `tsc`
71
+ * does with `outDir: "./dist"` and `rootDir: "./src"`.
72
+ *
73
+ * @param {string} srcPath Absolute path inside `src/`.
74
+ * @returns {string}
75
+ */
76
+ function srcToDist(srcPath) {
77
+ const rel = relative(SRC_DIR, srcPath);
78
+ return join(DIST_DIR, rel).replace(/\.ts$/, ".js");
79
+ }
80
+
81
+ async function main() {
82
+ const started = Date.now();
83
+ const entryPoints = [];
84
+ for await (const src of walkSources(SRC_DIR)) {
85
+ entryPoints.push(src);
86
+ }
87
+
88
+ // Clean dist/ so stale output from a previous `tsc` run cannot
89
+ // mask a missing file in the fast build. Full-tree rebuild is
90
+ // still under 200 ms in total.
91
+ await fs.rm(DIST_DIR, { recursive: true, force: true });
92
+ await fs.mkdir(DIST_DIR, { recursive: true });
93
+
94
+ await build({
95
+ entryPoints,
96
+ outdir: DIST_DIR,
97
+ outbase: SRC_DIR,
98
+ bundle: false,
99
+ format: "esm",
100
+ platform: "node",
101
+ target: ["node20"],
102
+ sourcemap: "linked",
103
+ logLevel: "warning",
104
+ // Preserve the existing directory layout in dist/ so the
105
+ // `exports` field in package.json still resolves correctly.
106
+ // `outbase: SRC_DIR` is what makes this work.
107
+ });
108
+
109
+ const durationMs = Date.now() - started;
110
+ const outCount = entryPoints.length;
111
+ process.stdout.write(
112
+ `claude-crap: fast build complete (${outCount} files → ${srcToDist(entryPoints[0] ?? SRC_DIR)
113
+ .split("/")
114
+ .slice(-3, -1)
115
+ .join("/")}, ${durationMs}ms)\n`,
116
+ );
117
+ process.stdout.write(
118
+ `claude-crap: NOTE — this build skipped type checking and declaration files.\n` +
119
+ ` Run \`npm run typecheck\` before committing, and use \`npm run build\` for releases.\n`,
120
+ );
121
+ }
122
+
123
+ if (process.env.CLAUDE_CRAP_SKIP_FAST_BUILD) {
124
+ process.exit(0);
125
+ }
126
+
127
+ main().catch((err) => {
128
+ process.stderr.write(`claude-crap fast build failed: ${err?.message ?? err}\n`);
129
+ process.exit(1);
130
+ });
@@ -0,0 +1,74 @@
1
+ // scripts/bundle-plugin.mjs
2
+ import { build } from "esbuild";
3
+ import { cp, mkdir, rm } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const ROOT = resolve(fileURLToPath(import.meta.url), "../..");
8
+ const BUNDLE_DIR = resolve(ROOT, "plugin/bundle");
9
+
10
+ async function main() {
11
+ await rm(BUNDLE_DIR, { recursive: true, force: true });
12
+ await mkdir(BUNDLE_DIR, { recursive: true });
13
+
14
+ // 1. MCP server entry — bundled, externalizing native/WASM deps
15
+ await build({
16
+ entryPoints: { "mcp-server": resolve(ROOT, "src/index.ts") },
17
+ outdir: BUNDLE_DIR,
18
+ outExtension: { ".js": ".mjs" },
19
+ bundle: true,
20
+ platform: "node",
21
+ format: "esm",
22
+ target: "node20",
23
+ sourcemap: "linked",
24
+ // web-tree-sitter uses Emscripten locateFile() to find .wasm,
25
+ // tree-sitter-wasms ships the grammars — both MUST stay external so
26
+ // the runtime `createRequire(import.meta.url).resolve(...)` finds
27
+ // them in plugin/node_modules/ at runtime.
28
+ // We also externalize pino, fastify and @modelcontextprotocol/sdk due to dynamic require/ESM issues.
29
+ external: [
30
+ "web-tree-sitter",
31
+ "tree-sitter-wasms",
32
+ "pino",
33
+ "fastify",
34
+ "@fastify/static",
35
+ "@modelcontextprotocol/sdk"
36
+ ],
37
+ banner: {
38
+ js: "// Generated by scripts/bundle-plugin.mjs — DO NOT EDIT",
39
+ },
40
+ });
41
+
42
+ // 2. TDR engine — bundled standalone so hooks/lib/quality-gate.mjs
43
+ // has a single entry to import classifyTdr() from.
44
+ await build({
45
+ entryPoints: { "tdr-engine": resolve(ROOT, "src/metrics/tdr.ts") },
46
+ outdir: BUNDLE_DIR,
47
+ outExtension: { ".js": ".mjs" },
48
+ bundle: true,
49
+ platform: "node",
50
+ format: "esm",
51
+ target: "node20",
52
+ sourcemap: "linked",
53
+ // TDR module is self-contained today (no external deps beyond Node
54
+ // builtins). Keep this list empty but explicit so a future import
55
+ // that pulls in a heavy dep is caught by the bundle size diff in CI.
56
+ external: [],
57
+ banner: {
58
+ js: "// Generated by scripts/bundle-plugin.mjs — DO NOT EDIT",
59
+ },
60
+ });
61
+
62
+ // 3. Copy dashboard static assets into the bundle so resolvePublicRoot()
63
+ // can find them relative to the bundle entry.
64
+ await cp(
65
+ resolve(ROOT, "src/dashboard/public"),
66
+ resolve(BUNDLE_DIR, "dashboard/public"),
67
+ { recursive: true },
68
+ );
69
+ }
70
+
71
+ main().catch((err) => {
72
+ process.stderr.write(`bundle-plugin failed: ${err}\n`);
73
+ process.exit(1);
74
+ });