claude-crap 0.3.8 → 0.4.0

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 (64) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +69 -27
  3. package/dist/adapters/common.d.ts +1 -1
  4. package/dist/adapters/common.d.ts.map +1 -1
  5. package/dist/adapters/common.js +1 -1
  6. package/dist/adapters/common.js.map +1 -1
  7. package/dist/adapters/dotnet-format.d.ts +35 -0
  8. package/dist/adapters/dotnet-format.d.ts.map +1 -0
  9. package/dist/adapters/dotnet-format.js +96 -0
  10. package/dist/adapters/dotnet-format.js.map +1 -0
  11. package/dist/adapters/index.d.ts +1 -0
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/index.js +4 -0
  14. package/dist/adapters/index.js.map +1 -1
  15. package/dist/crap-config.d.ts +2 -0
  16. package/dist/crap-config.d.ts.map +1 -1
  17. package/dist/crap-config.js +19 -4
  18. package/dist/crap-config.js.map +1 -1
  19. package/dist/dashboard/server.js +1 -1
  20. package/dist/index.js +74 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/monorepo/project-map.d.ts +112 -0
  23. package/dist/monorepo/project-map.d.ts.map +1 -0
  24. package/dist/monorepo/project-map.js +384 -0
  25. package/dist/monorepo/project-map.js.map +1 -0
  26. package/dist/scanner/bootstrap.d.ts.map +1 -1
  27. package/dist/scanner/bootstrap.js +6 -1
  28. package/dist/scanner/bootstrap.js.map +1 -1
  29. package/dist/scanner/detector.d.ts.map +1 -1
  30. package/dist/scanner/detector.js +7 -2
  31. package/dist/scanner/detector.js.map +1 -1
  32. package/dist/scanner/runner.d.ts.map +1 -1
  33. package/dist/scanner/runner.js +13 -0
  34. package/dist/scanner/runner.js.map +1 -1
  35. package/dist/schemas/tool-schemas.d.ts +16 -1
  36. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  37. package/dist/schemas/tool-schemas.js +16 -1
  38. package/dist/schemas/tool-schemas.js.map +1 -1
  39. package/package.json +1 -1
  40. package/plugin/.claude-plugin/plugin.json +1 -1
  41. package/plugin/CLAUDE.md +37 -0
  42. package/plugin/bundle/mcp-server.mjs +395 -29
  43. package/plugin/bundle/mcp-server.mjs.map +4 -4
  44. package/plugin/package-lock.json +2 -2
  45. package/plugin/package.json +1 -1
  46. package/src/adapters/common.ts +1 -1
  47. package/src/adapters/dotnet-format.ts +125 -0
  48. package/src/adapters/index.ts +4 -0
  49. package/src/crap-config.ts +27 -4
  50. package/src/dashboard/server.ts +1 -1
  51. package/src/index.ts +88 -5
  52. package/src/monorepo/project-map.ts +476 -0
  53. package/src/scanner/bootstrap.ts +7 -1
  54. package/src/scanner/detector.ts +7 -2
  55. package/src/scanner/runner.ts +13 -0
  56. package/src/schemas/tool-schemas.ts +17 -1
  57. package/src/tests/adapters/dispatch.test.ts +1 -1
  58. package/src/tests/auto-scan.test.ts +2 -2
  59. package/src/tests/boot-monorepo.test.ts +804 -0
  60. package/src/tests/boot-scanner-detection.test.ts +692 -0
  61. package/src/tests/boot-single-project.test.ts +780 -0
  62. package/src/tests/integration/mcp-server.integration.test.ts +2 -1
  63. package/src/tests/project-map.test.ts +302 -0
  64. package/src/tests/scanner-detector.test.ts +4 -4
@@ -232,7 +232,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
232
232
  assert.ok(child && !child.killed, "server child should be running");
233
233
  });
234
234
 
235
- it("exposes all nine tools via tools/list", async () => {
235
+ it("exposes all eleven tools via tools/list", async () => {
236
236
  const response = await client!.request<{ result?: { tools?: Array<{ name: string }> } }>(
237
237
  "tools/list",
238
238
  );
@@ -245,6 +245,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
245
245
  "compute_tdr",
246
246
  "ingest_sarif",
247
247
  "ingest_scanner_output",
248
+ "list_projects",
248
249
  "require_test_harness",
249
250
  "score_project",
250
251
  ]);
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Unit tests for the project-map discovery module.
3
+ *
4
+ * Covers workspace classification (single-project vs monorepo),
5
+ * per-project language detection across all supported project types,
6
+ * deduplication when a workspace appears in both npm workspaces and
7
+ * directory scan results, and the persist/load round-trip.
8
+ *
9
+ * Each test creates a fresh temporary directory tree and removes it in
10
+ * a `finally` block so failures leave no artefacts on disk.
11
+ *
12
+ * @module tests/project-map.test
13
+ */
14
+
15
+ import { describe, it } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { tmpdir } from "node:os";
20
+
21
+ import {
22
+ discoverProjectMap,
23
+ persistProjectMap,
24
+ loadProjectMap,
25
+ type ProjectMap,
26
+ type ProjectEntry,
27
+ } from "../monorepo/project-map.js";
28
+
29
+ // ── Helpers ──────────────────────────────────────────────────────────
30
+
31
+ function makeTmpDir(): string {
32
+ return mkdtempSync(join(tmpdir(), "crap-projmap-"));
33
+ }
34
+
35
+ /**
36
+ * Write a file at `absPath`, creating all parent directories first.
37
+ */
38
+ function touch(absPath: string, content = ""): void {
39
+ mkdirSync(join(absPath, ".."), { recursive: true });
40
+ writeFileSync(absPath, content, "utf8");
41
+ }
42
+
43
+ /**
44
+ * Return the ProjectEntry for the given relative path, or throw if absent.
45
+ */
46
+ function findProject(map: ProjectMap, relPath: string): ProjectEntry {
47
+ const entry = map.projects.find((p) => p.path === relPath);
48
+ assert.ok(entry, `expected project at path "${relPath}" — found: ${map.projects.map((p) => p.path).join(", ")}`);
49
+ return entry;
50
+ }
51
+
52
+ // ── discoverProjectMap ────────────────────────────────────────────────
53
+
54
+ describe("discoverProjectMap", () => {
55
+ it("single-project workspace is not a monorepo and has no sub-projects", async () => {
56
+ const dir = makeTmpDir();
57
+ try {
58
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "my-app" }));
59
+ writeFileSync(join(dir, "tsconfig.json"), "{}");
60
+
61
+ const map = await discoverProjectMap(dir);
62
+
63
+ assert.equal(map.isMonorepo, false);
64
+ assert.deepEqual(map.projects, []);
65
+ assert.equal(map.workspaceRoot, dir);
66
+ assert.equal(typeof map.generatedAt, "string");
67
+ } finally {
68
+ rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ it("npm workspaces monorepo detects both sub-projects with correct types", async () => {
73
+ const dir = makeTmpDir();
74
+ try {
75
+ // Root manifest declares workspaces
76
+ writeFileSync(
77
+ join(dir, "package.json"),
78
+ JSON.stringify({ name: "root", workspaces: ["apps/web", "apps/api"] }),
79
+ );
80
+
81
+ // apps/web — TypeScript project
82
+ mkdirSync(join(dir, "apps", "web"), { recursive: true });
83
+ writeFileSync(join(dir, "apps", "web", "package.json"), JSON.stringify({ name: "web" }));
84
+ writeFileSync(join(dir, "apps", "web", "tsconfig.json"), "{}");
85
+
86
+ // apps/api — plain JavaScript project (no tsconfig)
87
+ mkdirSync(join(dir, "apps", "api"), { recursive: true });
88
+ writeFileSync(join(dir, "apps", "api", "package.json"), JSON.stringify({ name: "api" }));
89
+
90
+ const map = await discoverProjectMap(dir);
91
+
92
+ assert.equal(map.isMonorepo, true);
93
+ assert.equal(map.projects.length, 2);
94
+
95
+ const web = findProject(map, "apps/web");
96
+ assert.equal(web.type, "typescript");
97
+ assert.equal(web.name, "web");
98
+
99
+ const api = findProject(map, "apps/api");
100
+ assert.equal(api.type, "javascript");
101
+ assert.equal(api.name, "api");
102
+ } finally {
103
+ rmSync(dir, { recursive: true, force: true });
104
+ }
105
+ });
106
+
107
+ it("mixed monorepo discovers both an npm workspace and a Dart project", async () => {
108
+ const dir = makeTmpDir();
109
+ try {
110
+ // Root declares only apps/web in workspaces; apps/mobile is found by scan
111
+ writeFileSync(
112
+ join(dir, "package.json"),
113
+ JSON.stringify({ name: "root", workspaces: ["apps/web"] }),
114
+ );
115
+
116
+ // apps/web — TypeScript
117
+ mkdirSync(join(dir, "apps", "web"), { recursive: true });
118
+ writeFileSync(join(dir, "apps", "web", "package.json"), JSON.stringify({ name: "web" }));
119
+ writeFileSync(join(dir, "apps", "web", "tsconfig.json"), "{}");
120
+
121
+ // apps/mobile — Dart / Flutter (discovered via directory scan, not workspaces)
122
+ mkdirSync(join(dir, "apps", "mobile"), { recursive: true });
123
+ writeFileSync(
124
+ join(dir, "apps", "mobile", "pubspec.yaml"),
125
+ "name: mobile\nenvironment:\n sdk: '>=3.0.0 <4.0.0'\n",
126
+ );
127
+
128
+ const map = await discoverProjectMap(dir);
129
+
130
+ assert.equal(map.isMonorepo, true);
131
+
132
+ const web = findProject(map, "apps/web");
133
+ assert.equal(web.type, "typescript");
134
+
135
+ const mobile = findProject(map, "apps/mobile");
136
+ assert.equal(mobile.type, "dart");
137
+ } finally {
138
+ rmSync(dir, { recursive: true, force: true });
139
+ }
140
+ });
141
+
142
+ it("Python project is detected with type 'python' and scanner 'bandit'", async () => {
143
+ const dir = makeTmpDir();
144
+ try {
145
+ // Root with no workspaces — but has a sub-directory with Python signals
146
+ writeFileSync(
147
+ join(dir, "package.json"),
148
+ JSON.stringify({ name: "root", workspaces: ["apps/ml"] }),
149
+ );
150
+
151
+ mkdirSync(join(dir, "apps", "ml"), { recursive: true });
152
+ writeFileSync(
153
+ join(dir, "apps", "ml", "pyproject.toml"),
154
+ "[project]\nname = \"ml\"\n",
155
+ );
156
+
157
+ const map = await discoverProjectMap(dir);
158
+
159
+ const ml = findProject(map, "apps/ml");
160
+ assert.equal(ml.type, "python");
161
+ assert.equal(ml.scanner, "bandit");
162
+ } finally {
163
+ rmSync(dir, { recursive: true, force: true });
164
+ }
165
+ });
166
+
167
+ it("Java project is detected with type 'java' and scanner 'dotnet_format'", async () => {
168
+ const dir = makeTmpDir();
169
+ try {
170
+ writeFileSync(
171
+ join(dir, "package.json"),
172
+ JSON.stringify({ name: "root", workspaces: ["apps/backend"] }),
173
+ );
174
+
175
+ mkdirSync(join(dir, "apps", "backend"), { recursive: true });
176
+ writeFileSync(
177
+ join(dir, "apps", "backend", "pom.xml"),
178
+ "<project><modelVersion>4.0.0</modelVersion></project>",
179
+ );
180
+
181
+ const map = await discoverProjectMap(dir);
182
+
183
+ const backend = findProject(map, "apps/backend");
184
+ assert.equal(backend.type, "java");
185
+ assert.equal(backend.scanner, "semgrep");
186
+ } finally {
187
+ rmSync(dir, { recursive: true, force: true });
188
+ }
189
+ });
190
+
191
+ it("C# project is detected with type 'csharp' and scanner 'dotnet_format'", async () => {
192
+ const dir = makeTmpDir();
193
+ try {
194
+ writeFileSync(
195
+ join(dir, "package.json"),
196
+ JSON.stringify({ name: "root", workspaces: ["apps/api"] }),
197
+ );
198
+
199
+ mkdirSync(join(dir, "apps", "api"), { recursive: true });
200
+ writeFileSync(
201
+ join(dir, "apps", "api", "MyApp.csproj"),
202
+ "<Project Sdk=\"Microsoft.NET.Sdk\"></Project>",
203
+ );
204
+
205
+ const map = await discoverProjectMap(dir);
206
+
207
+ const api = findProject(map, "apps/api");
208
+ assert.equal(api.type, "csharp");
209
+ assert.equal(api.scanner, "dotnet_format");
210
+ } finally {
211
+ rmSync(dir, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ it("empty workspace returns isMonorepo false and an empty projects array", async () => {
216
+ const dir = makeTmpDir();
217
+ try {
218
+ const map = await discoverProjectMap(dir);
219
+
220
+ assert.equal(map.isMonorepo, false);
221
+ assert.deepEqual(map.projects, []);
222
+ } finally {
223
+ rmSync(dir, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ it("project listed in npm workspaces AND found by directory scan appears only once", async () => {
228
+ const dir = makeTmpDir();
229
+ try {
230
+ // apps/shared is declared in workspaces; it also lives inside apps/
231
+ // which the directory scanner would naturally traverse
232
+ writeFileSync(
233
+ join(dir, "package.json"),
234
+ JSON.stringify({ name: "root", workspaces: ["apps/shared"] }),
235
+ );
236
+
237
+ mkdirSync(join(dir, "apps", "shared"), { recursive: true });
238
+ writeFileSync(join(dir, "apps", "shared", "package.json"), JSON.stringify({ name: "shared" }));
239
+ writeFileSync(join(dir, "apps", "shared", "tsconfig.json"), "{}");
240
+
241
+ const map = await discoverProjectMap(dir);
242
+
243
+ const matches = map.projects.filter((p) => p.path === "apps/shared");
244
+ assert.equal(
245
+ matches.length,
246
+ 1,
247
+ `expected exactly 1 entry for "apps/shared", got ${matches.length}`,
248
+ );
249
+ } finally {
250
+ rmSync(dir, { recursive: true, force: true });
251
+ }
252
+ });
253
+ });
254
+
255
+ // ── persistProjectMap / loadProjectMap ───────────────────────────────
256
+
257
+ describe("persistProjectMap / loadProjectMap", () => {
258
+ it("persisted map round-trips through loadProjectMap with deep equality", async () => {
259
+ const dir = makeTmpDir();
260
+ try {
261
+ const original: ProjectMap = {
262
+ generatedAt: new Date().toISOString(),
263
+ workspaceRoot: dir,
264
+ isMonorepo: true,
265
+ projects: [
266
+ {
267
+ name: "web",
268
+ path: "apps/web",
269
+ type: "typescript",
270
+ scanner: "eslint",
271
+ scannerAvailable: true,
272
+ },
273
+ {
274
+ name: "ml",
275
+ path: "apps/ml",
276
+ type: "python",
277
+ scanner: "bandit",
278
+ scannerAvailable: false,
279
+ },
280
+ ],
281
+ };
282
+
283
+ await persistProjectMap(original, dir);
284
+ const loaded = loadProjectMap(dir);
285
+
286
+ assert.ok(loaded !== null, "loadProjectMap returned null after persist");
287
+ assert.deepEqual(loaded, original);
288
+ } finally {
289
+ rmSync(dir, { recursive: true, force: true });
290
+ }
291
+ });
292
+
293
+ it("loadProjectMap returns null when no persisted file exists", () => {
294
+ const dir = makeTmpDir();
295
+ try {
296
+ const result = loadProjectMap(dir);
297
+ assert.equal(result, null);
298
+ } finally {
299
+ rmSync(dir, { recursive: true, force: true });
300
+ }
301
+ });
302
+ });
@@ -153,9 +153,9 @@ describe("detectScanners", () => {
153
153
  // Config and package.json probes will all fail.
154
154
  // Binary probe results depend on the host — don't assert on those,
155
155
  // but do assert the structure is correct.
156
- assert.equal(results.length, 5);
156
+ assert.equal(results.length, 6);
157
157
  for (const r of results) {
158
- assert.ok(["eslint", "semgrep", "bandit", "stryker", "dart_analyze"].includes(r.scanner));
158
+ assert.ok(["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"].includes(r.scanner));
159
159
  assert.equal(typeof r.available, "boolean");
160
160
  assert.equal(typeof r.reason, "string");
161
161
  }
@@ -170,7 +170,7 @@ describe("detectScanners", () => {
170
170
  writeFileSync(join(dir, "package.json"), "not json at all");
171
171
  // Should not throw — just skip the package.json probe
172
172
  const results = await detectScanners(dir);
173
- assert.equal(results.length, 5);
173
+ assert.equal(results.length, 6);
174
174
  } finally {
175
175
  rmSync(dir, { recursive: true, force: true });
176
176
  }
@@ -195,7 +195,7 @@ describe("detectScanners", () => {
195
195
  it("SCANNER_SIGNALS covers all supported scanners", () => {
196
196
  assert.deepEqual(
197
197
  Object.keys(SCANNER_SIGNALS).sort(),
198
- ["bandit", "dart_analyze", "eslint", "semgrep", "stryker"],
198
+ ["bandit", "dart_analyze", "dotnet_format", "eslint", "semgrep", "stryker"],
199
199
  );
200
200
  });
201
201
  });