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,320 @@
1
+ // @ts-check
2
+ /**
3
+ * `claude-crap doctor` — full diagnostic pass.
4
+ *
5
+ * Runs every check the install script runs, plus several deeper probes
6
+ * that make sure the plugin can actually function against the current
7
+ * workspace:
8
+ *
9
+ * - Node.js runtime ≥ 20
10
+ * - Plugin structure sanity (package.json, .claude-plugin/plugin.json,
11
+ * .mcp.json, hooks/hooks.json)
12
+ * - `dist/index.js` exists (built)
13
+ * - Hook scripts are executable
14
+ * - tree-sitter runtime WASM is reachable
15
+ * - tree-sitter language grammars (c_sharp, javascript, typescript,
16
+ * python, java) are all present
17
+ * - Dashboard port is free on 127.0.0.1
18
+ * - SARIF reports directory is writable in the current workspace
19
+ *
20
+ * Exits 0 when all checks pass, 1 when any check fails, and 2 when
21
+ * there are only warnings. This makes it easy to embed in CI:
22
+ * `claude-crap doctor && echo ok`.
23
+ *
24
+ * @module scripts/doctor
25
+ */
26
+
27
+ import { promises as fs, constants as fsConstants } from "node:fs";
28
+ import { createServer } from "node:net";
29
+ import { join, resolve } from "node:path";
30
+
31
+ import { printBanner, printStep, paint, icons } from "./lib/cli-ui.mjs";
32
+
33
+ const SUPPORTED_LANGUAGES = /** @type {const} */ ([
34
+ { id: "c_sharp", wasm: "tree-sitter-c_sharp.wasm" },
35
+ { id: "javascript", wasm: "tree-sitter-javascript.wasm" },
36
+ { id: "typescript", wasm: "tree-sitter-typescript.wasm" },
37
+ { id: "python", wasm: "tree-sitter-python.wasm" },
38
+ { id: "java", wasm: "tree-sitter-java.wasm" },
39
+ ]);
40
+
41
+ /**
42
+ * @typedef {Object} CommandContext
43
+ * @property {string} pluginRoot
44
+ * @property {string[]} argv
45
+ */
46
+
47
+ /**
48
+ * Doctor entrypoint.
49
+ *
50
+ * @param {CommandContext} ctx
51
+ * @returns {Promise<number>}
52
+ */
53
+ export default async function doctor(ctx) {
54
+ printBanner("claude-crap :: doctor");
55
+
56
+ const checks = [];
57
+ checks.push(await checkNodeVersion());
58
+ checks.push(await checkPluginStructure(ctx.pluginRoot));
59
+ checks.push(await checkDist(ctx.pluginRoot));
60
+ checks.push(await checkHooksExecutable(ctx.pluginRoot));
61
+ checks.push(await checkTreeSitterRuntime(ctx.pluginRoot));
62
+ checks.push(...(await checkGrammars(ctx.pluginRoot)));
63
+ checks.push(await checkDashboardPort());
64
+ checks.push(await checkReportsWritable(process.cwd()));
65
+
66
+ for (const step of checks) printStep(step);
67
+
68
+ const fails = checks.filter((c) => c.status === "fail").length;
69
+ const warns = checks.filter((c) => c.status === "warn").length;
70
+
71
+ process.stdout.write(
72
+ `\n${paint.bold("Summary:")} ${checks.length - fails - warns} ok, ${warns} warn, ${fails} fail\n`,
73
+ );
74
+
75
+ if (fails > 0) {
76
+ process.stdout.write(
77
+ `${paint.red(icons.fail)} At least one check failed. Fix the issues above and re-run ${paint.bold("claude-crap doctor")}.\n`,
78
+ );
79
+ return 1;
80
+ }
81
+ if (warns > 0) {
82
+ process.stdout.write(
83
+ `${paint.yellow(icons.warn)} Checks passed with warnings. The plugin should still work.\n`,
84
+ );
85
+ return 2;
86
+ }
87
+ process.stdout.write(`${paint.green(icons.ok)} All checks passed. claude-crap is ready.\n`);
88
+ return 0;
89
+ }
90
+
91
+ /** @returns {Promise<import("./lib/cli-ui.mjs").StepResult>} */
92
+ async function checkNodeVersion() {
93
+ const raw = process.versions.node;
94
+ const major = Number(raw.split(".")[0]);
95
+ if (!Number.isFinite(major) || major < 20) {
96
+ return {
97
+ status: "fail",
98
+ label: `Node.js runtime (${raw})`,
99
+ detail: `claude-crap requires Node.js ≥ 20.0.0`,
100
+ };
101
+ }
102
+ return { status: "ok", label: `Node.js runtime (${raw})` };
103
+ }
104
+
105
+ /**
106
+ * @param {string} pluginRoot
107
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
108
+ */
109
+ async function checkPluginStructure(pluginRoot) {
110
+ const required = [
111
+ "package.json",
112
+ "plugin/.claude-plugin/plugin.json",
113
+ "plugin/.mcp.json",
114
+ "plugin/CLAUDE.md",
115
+ "plugin/hooks/hooks.json",
116
+ "plugin/hooks/pre-tool-use.mjs",
117
+ "plugin/hooks/post-tool-use.mjs",
118
+ "plugin/hooks/stop-quality-gate.mjs",
119
+ "plugin/hooks/session-start.mjs",
120
+ ];
121
+ const missing = [];
122
+ for (const rel of required) {
123
+ try {
124
+ await fs.access(join(pluginRoot, rel));
125
+ } catch {
126
+ missing.push(rel);
127
+ }
128
+ }
129
+ if (missing.length > 0) {
130
+ return {
131
+ status: "fail",
132
+ label: `Plugin files present`,
133
+ detail: `Missing: ${missing.join(", ")}`,
134
+ };
135
+ }
136
+ return { status: "ok", label: `Plugin files present (${required.length} checked)` };
137
+ }
138
+
139
+ /**
140
+ * @param {string} pluginRoot
141
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
142
+ */
143
+ async function checkDist(pluginRoot) {
144
+ const npmEntry = join(pluginRoot, "dist", "index.js");
145
+ const gitEntry = join(pluginRoot, "plugin", "bundle", "mcp-server.mjs");
146
+
147
+ let npmOk = false;
148
+ let gitOk = false;
149
+ let npmAge, gitAge;
150
+
151
+ try {
152
+ const stat = await fs.stat(npmEntry);
153
+ npmAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
154
+ npmOk = true;
155
+ } catch {}
156
+
157
+ try {
158
+ const stat = await fs.stat(gitEntry);
159
+ gitAge = Math.round((Date.now() - stat.mtimeMs) / (1000 * 60 * 60));
160
+ gitOk = true;
161
+ } catch {}
162
+
163
+ const details = [];
164
+ if (npmOk) details.push(`dist/index.js (~${npmAge}h)`);
165
+ if (gitOk) details.push(`plugin/bundle/mcp-server.mjs (~${gitAge}h)`);
166
+
167
+ if (!npmOk && !gitOk) {
168
+ return {
169
+ status: "fail",
170
+ label: `Server entrypoints built`,
171
+ detail: `Both dist/ and plugin/bundle/ are missing. Run \`npm run build && npm run build:plugin\`.`
172
+ };
173
+ }
174
+
175
+ return {
176
+ status: npmOk && gitOk ? "ok" : "warn",
177
+ label: `Server entrypoints built`,
178
+ detail: details.join(", ") + (!npmOk || !gitOk ? " (one is missing)" : "")
179
+ };
180
+ }
181
+
182
+ /**
183
+ * @param {string} pluginRoot
184
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
185
+ */
186
+ async function checkHooksExecutable(pluginRoot) {
187
+ const hooks = ["pre-tool-use.mjs", "post-tool-use.mjs", "stop-quality-gate.mjs", "session-start.mjs"];
188
+ const notExec = [];
189
+ for (const name of hooks) {
190
+ const full = join(pluginRoot, "plugin", "hooks", name);
191
+ try {
192
+ await fs.access(full, fsConstants.X_OK);
193
+ } catch {
194
+ notExec.push(name);
195
+ }
196
+ }
197
+ if (notExec.length > 0) {
198
+ return {
199
+ status: "warn",
200
+ label: `Hooks are executable`,
201
+ detail:
202
+ `${notExec.length} hook(s) lack the executable bit: ${notExec.join(", ")}. ` +
203
+ `Run \`claude-crap install\` to fix.`,
204
+ };
205
+ }
206
+ return { status: "ok", label: `Hooks are executable (4 checked)` };
207
+ }
208
+
209
+ /**
210
+ * @param {string} pluginRoot
211
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
212
+ */
213
+ async function checkTreeSitterRuntime(pluginRoot) {
214
+ const runtime = join(pluginRoot, "node_modules", "web-tree-sitter", "tree-sitter.wasm");
215
+ try {
216
+ const stat = await fs.stat(runtime);
217
+ return {
218
+ status: "ok",
219
+ label: `tree-sitter runtime WASM present`,
220
+ detail: `${runtime} (${stat.size} bytes)`,
221
+ };
222
+ } catch {
223
+ return {
224
+ status: "fail",
225
+ label: `tree-sitter runtime WASM present`,
226
+ detail: `Missing ${runtime}. Run \`npm install\` from ${pluginRoot}.`,
227
+ };
228
+ }
229
+ }
230
+
231
+ /**
232
+ * @param {string} pluginRoot
233
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult[]>}
234
+ */
235
+ async function checkGrammars(pluginRoot) {
236
+ const base = join(pluginRoot, "node_modules", "tree-sitter-wasms", "out");
237
+ /** @type {import("./lib/cli-ui.mjs").StepResult[]} */
238
+ const results = [];
239
+ for (const lang of SUPPORTED_LANGUAGES) {
240
+ const full = join(base, lang.wasm);
241
+ try {
242
+ await fs.access(full);
243
+ results.push({ status: "ok", label: `Grammar: ${lang.id}` });
244
+ } catch {
245
+ results.push({
246
+ status: "fail",
247
+ label: `Grammar: ${lang.id}`,
248
+ detail: `Missing ${full}. Run \`npm install\`.`,
249
+ });
250
+ }
251
+ }
252
+ return results;
253
+ }
254
+
255
+ /**
256
+ * Probe the configured dashboard port on 127.0.0.1. We attempt to
257
+ * open a TCP listener ourselves — if it succeeds the port is free,
258
+ * if it fails with EADDRINUSE then something is already holding it.
259
+ *
260
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
261
+ */
262
+ async function checkDashboardPort() {
263
+ const raw = process.env.CLAUDE_PLUGIN_OPTION_DASHBOARD_PORT;
264
+ const port = Number(raw ?? 5117);
265
+ if (!Number.isFinite(port)) {
266
+ return {
267
+ status: "warn",
268
+ label: `Dashboard port configured`,
269
+ detail: `CLAUDE_PLUGIN_OPTION_DASHBOARD_PORT=${raw} is not a number`,
270
+ };
271
+ }
272
+ return await new Promise((resolvePromise) => {
273
+ const server = createServer();
274
+ server.once("error", (err) => {
275
+ const nodeErr = /** @type {NodeJS.ErrnoException} */ (err);
276
+ if (nodeErr.code === "EADDRINUSE") {
277
+ resolvePromise({
278
+ status: "warn",
279
+ label: `Dashboard port ${port} is free`,
280
+ detail:
281
+ `Port ${port} is already in use. The dashboard will refuse to start ` +
282
+ `but the MCP server will keep running.`,
283
+ });
284
+ } else {
285
+ resolvePromise({
286
+ status: "warn",
287
+ label: `Dashboard port ${port} is free`,
288
+ detail: nodeErr.message,
289
+ });
290
+ }
291
+ });
292
+ server.listen({ port, host: "127.0.0.1" }, () => {
293
+ server.close(() => {
294
+ resolvePromise({ status: "ok", label: `Dashboard port ${port} is free` });
295
+ });
296
+ });
297
+ });
298
+ }
299
+
300
+ /**
301
+ * @param {string} workspace
302
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
303
+ */
304
+ async function checkReportsWritable(workspace) {
305
+ const dir = resolve(workspace, ".claude-crap", "reports");
306
+ try {
307
+ await fs.mkdir(dir, { recursive: true });
308
+ // Try to write a tiny probe file and then immediately unlink it.
309
+ const probe = join(dir, `.doctor-${process.pid}`);
310
+ await fs.writeFile(probe, "ok");
311
+ await fs.unlink(probe);
312
+ return { status: "ok", label: `SARIF reports directory writable`, detail: dir };
313
+ } catch (err) {
314
+ return {
315
+ status: "fail",
316
+ label: `SARIF reports directory writable`,
317
+ detail: `${dir} :: ${/** @type {Error} */ (err).message}`,
318
+ };
319
+ }
320
+ }
@@ -0,0 +1,192 @@
1
+ // @ts-check
2
+ /**
3
+ * `claude-crap install` — prepare the workspace and print the Claude
4
+ * Code registration command.
5
+ *
6
+ * This subcommand does every bit of side-effecty work that the plugin
7
+ * needs to run cleanly AND nothing else. We intentionally do NOT try to
8
+ * edit Claude Code's own settings file — that surface is owned by
9
+ * Claude Code's `/plugin install` command, and manipulating it behind
10
+ * the user's back would be a support nightmare. Instead, we:
11
+ *
12
+ * 1. Verify Node.js and the plugin directory look sane.
13
+ * 2. Ensure `dist/` exists (postinstall should have built it, but we
14
+ * still check so a manual clone also works).
15
+ * 3. `chmod +x` the hook scripts and the bin entrypoint (defensive —
16
+ * npm should handle this but tarballs sometimes lose the bits).
17
+ * 4. Create `.claude-crap/reports/` inside the current workspace so
18
+ * the SARIF store can write without a race on its first ingestion.
19
+ * 5. Print the exact Claude Code command the user needs to run next.
20
+ *
21
+ * Exits 0 on success and 1 on any preparation failure.
22
+ *
23
+ * @module scripts/install
24
+ */
25
+
26
+ import { promises as fs, constants as fsConstants } from "node:fs";
27
+ import { resolve, join } from "node:path";
28
+
29
+ import { printBanner, printStep, paint, icons } from "./lib/cli-ui.mjs";
30
+
31
+ /**
32
+ * @typedef {Object} CommandContext
33
+ * @property {string} pluginRoot Absolute path to the plugin root (this dir).
34
+ * @property {string[]} argv Remaining CLI arguments after the subcommand name.
35
+ */
36
+
37
+ /**
38
+ * Entrypoint invoked by `bin/claude-crap.mjs`.
39
+ *
40
+ * @param {CommandContext} ctx
41
+ * @returns {Promise<number>} Exit code (0 = success, 1 = failure).
42
+ */
43
+ export default async function install(ctx) {
44
+ printBanner("claude-crap :: install");
45
+
46
+ const checks = [];
47
+ checks.push(await checkNodeVersion());
48
+ checks.push(await checkDistBuilt(ctx.pluginRoot));
49
+ checks.push(await chmodHooks(ctx.pluginRoot));
50
+ checks.push(await ensureReportsDir(process.cwd()));
51
+
52
+ for (const step of checks) printStep(step);
53
+
54
+ const hasFailure = checks.some((c) => c.status === "fail");
55
+ if (hasFailure) {
56
+ process.stdout.write(
57
+ `\n${paint.red(icons.fail)} Installation prerequisites failed. ` +
58
+ `Run ${paint.bold("claude-crap doctor")} for details.\n`,
59
+ );
60
+ return 1;
61
+ }
62
+
63
+ // Success — tell the user exactly what to do next. We print the
64
+ // Claude Code native command and also mention the marketplace path
65
+ // for users who cloned the repo from GitHub.
66
+ const pluginDir = join(ctx.pluginRoot, "plugin");
67
+ process.stdout.write(
68
+ [
69
+ "",
70
+ `${paint.green(icons.ok)} claude-crap is ready to register with Claude Code.`,
71
+ "",
72
+ ` Plugin root: ${paint.cyan(pluginDir)}`,
73
+ "",
74
+ paint.bold(" Next steps — pick ONE of the following:"),
75
+ "",
76
+ " 1. Native Claude Code install from this directory:",
77
+ ` ${paint.cyan(`/plugin install ${pluginDir}`)}`,
78
+ "",
79
+ " 2. Marketplace install (if the plugin is published to GitHub):",
80
+ ` ${paint.cyan("/plugin marketplace add ahernandez-developer/claude-crap")}`,
81
+ ` ${paint.cyan("/plugin install claude-crap")}`,
82
+ "",
83
+ paint.dim(" Then open a Claude Code session in this workspace. The"),
84
+ paint.dim(" PreToolUse gatekeeper, PostToolUse verifier, Stop quality"),
85
+ paint.dim(" gate, and the local Vue dashboard will all start on their"),
86
+ paint.dim(" own. Run `claude-crap doctor` any time to re-verify."),
87
+ "",
88
+ ].join("\n"),
89
+ );
90
+ return 0;
91
+ }
92
+
93
+ /**
94
+ * Verify the Node.js major version is at least 20 (matches `engines`
95
+ * in package.json).
96
+ *
97
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
98
+ */
99
+ async function checkNodeVersion() {
100
+ const raw = process.versions.node;
101
+ const major = Number(raw.split(".")[0]);
102
+ if (!Number.isFinite(major) || major < 20) {
103
+ return {
104
+ status: "fail",
105
+ label: `Node.js runtime`,
106
+ detail: `Found ${raw}, claude-crap requires ≥ 20.0.0. Install a newer Node.js and retry.`,
107
+ };
108
+ }
109
+ return { status: "ok", label: `Node.js runtime (${raw})` };
110
+ }
111
+
112
+ /**
113
+ * Check that the plugin bundle exists (`plugin/bundle/mcp-server.mjs`).
114
+ * The postinstall hook should have built it automatically; this is a
115
+ * safety net for users who pulled the repo without running `npm install`.
116
+ *
117
+ * @param {string} pluginRoot
118
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
119
+ */
120
+ async function checkDistBuilt(pluginRoot) {
121
+ const entry = join(pluginRoot, "plugin", "bundle", "mcp-server.mjs");
122
+ try {
123
+ await fs.access(entry);
124
+ return { status: "ok", label: `MCP server bundle is built`, detail: entry };
125
+ } catch {
126
+ return {
127
+ status: "fail",
128
+ label: `MCP server bundle is NOT built`,
129
+ detail: `Expected ${entry}. Run \`npm run build:plugin\` from the plugin root.`,
130
+ };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Ensure every hook script is marked executable. npm usually handles
136
+ * this via the `files` field in package.json, but tarballs extracted
137
+ * manually or pulled over non-UNIX filesystems sometimes lose the bit.
138
+ *
139
+ * @param {string} pluginRoot
140
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
141
+ */
142
+ async function chmodHooks(pluginRoot) {
143
+ const hookDir = join(pluginRoot, "plugin", "hooks");
144
+ const entries = ["pre-tool-use.mjs", "post-tool-use.mjs", "stop-quality-gate.mjs", "session-start.mjs"];
145
+ const fixed = [];
146
+ for (const name of entries) {
147
+ const full = join(hookDir, name);
148
+ try {
149
+ await fs.access(full, fsConstants.F_OK);
150
+ await fs.chmod(full, 0o755);
151
+ fixed.push(name);
152
+ } catch {
153
+ return {
154
+ status: "fail",
155
+ label: `Hook scripts are executable`,
156
+ detail: `Missing ${full}`,
157
+ };
158
+ }
159
+ }
160
+ return {
161
+ status: "ok",
162
+ label: `Hook scripts are executable`,
163
+ detail: fixed.join(", "),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Ensure the SARIF reports directory exists inside the current
169
+ * workspace so the first ingestion doesn't race with a missing dir.
170
+ * This is a separate directory from the plugin root — it lives in the
171
+ * user's project, not the plugin's install location.
172
+ *
173
+ * @param {string} workspace
174
+ * @returns {Promise<import("./lib/cli-ui.mjs").StepResult>}
175
+ */
176
+ async function ensureReportsDir(workspace) {
177
+ const dir = resolve(workspace, ".claude-crap", "reports");
178
+ try {
179
+ await fs.mkdir(dir, { recursive: true });
180
+ return {
181
+ status: "ok",
182
+ label: `SARIF reports directory ready`,
183
+ detail: dir,
184
+ };
185
+ } catch (err) {
186
+ return {
187
+ status: "warn",
188
+ label: `Could not create reports directory`,
189
+ detail: `${dir} :: ${/** @type {Error} */ (err).message}`,
190
+ };
191
+ }
192
+ }
@@ -0,0 +1,122 @@
1
+ // @ts-check
2
+ /**
3
+ * Tiny CLI UI helpers shared by every `claude-crap <cmd>` subcommand.
4
+ *
5
+ * Provides ANSI color wrappers (with a `NO_COLOR` fallback), a unified
6
+ * `printStep` formatter for doctor-style checklists, and a handful of
7
+ * icon constants so the output looks consistent across subcommands.
8
+ *
9
+ * Zero runtime dependencies — uses only Node built-ins so it keeps the
10
+ * CLI startup fast and works in any environment the plugin supports.
11
+ *
12
+ * @module scripts/lib/cli-ui
13
+ */
14
+
15
+ /**
16
+ * Check whether the current terminal supports ANSI color escapes. We
17
+ * follow the `NO_COLOR` env var (https://no-color.org/) and also
18
+ * respect the conventional `FORCE_COLOR` override for CI environments.
19
+ */
20
+ const useColor = (() => {
21
+ if (process.env.NO_COLOR) return false;
22
+ if (process.env.FORCE_COLOR) return true;
23
+ return Boolean(process.stdout.isTTY);
24
+ })();
25
+
26
+ /**
27
+ * Wrap a string in an ANSI color escape when color is enabled. Returns
28
+ * the plain string otherwise.
29
+ *
30
+ * @param {string} code Numeric ANSI color code (e.g. `"32"` for green).
31
+ * @param {string} text Text to wrap.
32
+ * @returns {string}
33
+ */
34
+ function color(code, text) {
35
+ return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
36
+ }
37
+
38
+ /**
39
+ * Color helpers. Each function takes a string and returns a potentially
40
+ * colored version suitable for direct `process.stdout.write()` output.
41
+ */
42
+ export const paint = Object.freeze({
43
+ dim: (s) => color("2", s),
44
+ bold: (s) => color("1", s),
45
+ green: (s) => color("32", s),
46
+ yellow: (s) => color("33", s),
47
+ red: (s) => color("31", s),
48
+ cyan: (s) => color("36", s),
49
+ magenta: (s) => color("35", s),
50
+ });
51
+
52
+ /**
53
+ * Icons used by `printStep`. Kept ASCII-safe so the output renders
54
+ * correctly inside Claude Code's plain-text hook transcript.
55
+ */
56
+ export const icons = Object.freeze({
57
+ ok: "✓",
58
+ warn: "!",
59
+ fail: "✗",
60
+ info: "•",
61
+ step: "▸",
62
+ });
63
+
64
+ /**
65
+ * Print a heading banner to stdout. Used once per subcommand to make
66
+ * the CLI output easy to scan.
67
+ *
68
+ * @param {string} title Short heading text (kept under ~40 chars).
69
+ */
70
+ export function printBanner(title) {
71
+ const line = "─".repeat(Math.max(1, Math.min(title.length + 4, 76)));
72
+ process.stdout.write(`\n${paint.cyan(line)}\n`);
73
+ process.stdout.write(` ${paint.bold(title)}\n`);
74
+ process.stdout.write(`${paint.cyan(line)}\n\n`);
75
+ }
76
+
77
+ /**
78
+ * Structured result from a diagnostic check, suitable for both
79
+ * rendering with `printStep` and aggregating into a summary exit code.
80
+ *
81
+ * @typedef {"ok" | "warn" | "fail" | "info"} StepStatus
82
+ *
83
+ * @typedef {Object} StepResult
84
+ * @property {StepStatus} status
85
+ * @property {string} label
86
+ * @property {string} [detail]
87
+ */
88
+
89
+ /**
90
+ * Print a single checklist step.
91
+ *
92
+ * @param {StepResult} step
93
+ */
94
+ export function printStep(step) {
95
+ const icon =
96
+ step.status === "ok"
97
+ ? paint.green(icons.ok)
98
+ : step.status === "warn"
99
+ ? paint.yellow(icons.warn)
100
+ : step.status === "fail"
101
+ ? paint.red(icons.fail)
102
+ : paint.dim(icons.info);
103
+ process.stdout.write(` ${icon} ${step.label}\n`);
104
+ if (step.detail) {
105
+ for (const line of step.detail.split("\n")) {
106
+ process.stdout.write(` ${paint.dim(line)}\n`);
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Print a key/value pair right-aligned at a fixed label width. Used
113
+ * by `status` to render the "resolved paths" block.
114
+ *
115
+ * @param {string} label
116
+ * @param {string} value
117
+ * @param {number} [width]
118
+ */
119
+ export function printKv(label, value, width = 20) {
120
+ const padded = label.padEnd(width, " ");
121
+ process.stdout.write(` ${paint.dim(padded)} ${value}\n`);
122
+ }