@mainahq/core 1.0.0 → 1.0.1

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.
@@ -0,0 +1,303 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { detectTools, getToolsForLanguages, TOOL_REGISTRY } from "../detect";
3
+
4
+ // ─── TOOL_REGISTRY metadata ────────────────────────────────────────────────
5
+
6
+ describe("TOOL_REGISTRY metadata", () => {
7
+ test("every tool entry has a languages array", () => {
8
+ for (const [_name, entry] of Object.entries(TOOL_REGISTRY)) {
9
+ expect(entry.languages).toBeDefined();
10
+ expect(Array.isArray(entry.languages)).toBe(true);
11
+ expect(entry.languages.length).toBeGreaterThan(0);
12
+ }
13
+ });
14
+
15
+ test("every tool entry has a tier field", () => {
16
+ for (const [_name, entry] of Object.entries(TOOL_REGISTRY)) {
17
+ expect(entry.tier).toBeDefined();
18
+ expect(["essential", "recommended", "optional"]).toContain(entry.tier);
19
+ }
20
+ });
21
+
22
+ test("biome is essential for typescript/javascript", () => {
23
+ expect(TOOL_REGISTRY.biome.languages).toContain("typescript");
24
+ expect(TOOL_REGISTRY.biome.languages).toContain("javascript");
25
+ expect(TOOL_REGISTRY.biome.tier).toBe("essential");
26
+ });
27
+
28
+ test("semgrep is universal and recommended", () => {
29
+ expect(TOOL_REGISTRY.semgrep.languages).toContain("*");
30
+ expect(TOOL_REGISTRY.semgrep.tier).toBe("recommended");
31
+ });
32
+
33
+ test("trivy is universal and recommended", () => {
34
+ expect(TOOL_REGISTRY.trivy.languages).toContain("*");
35
+ expect(TOOL_REGISTRY.trivy.tier).toBe("recommended");
36
+ });
37
+
38
+ test("secretlint is universal and recommended", () => {
39
+ expect(TOOL_REGISTRY.secretlint.languages).toContain("*");
40
+ expect(TOOL_REGISTRY.secretlint.tier).toBe("recommended");
41
+ });
42
+
43
+ test("ruff is essential for python", () => {
44
+ expect(TOOL_REGISTRY.ruff.languages).toContain("python");
45
+ expect(TOOL_REGISTRY.ruff.tier).toBe("essential");
46
+ });
47
+
48
+ test("golangci-lint is essential for go", () => {
49
+ expect(TOOL_REGISTRY["golangci-lint"].languages).toContain("go");
50
+ expect(TOOL_REGISTRY["golangci-lint"].tier).toBe("essential");
51
+ });
52
+
53
+ test("cargo-clippy is essential for rust", () => {
54
+ expect(TOOL_REGISTRY["cargo-clippy"].languages).toContain("rust");
55
+ expect(TOOL_REGISTRY["cargo-clippy"].tier).toBe("essential");
56
+ });
57
+
58
+ test("cargo-audit is recommended for rust", () => {
59
+ expect(TOOL_REGISTRY["cargo-audit"].languages).toContain("rust");
60
+ expect(TOOL_REGISTRY["cargo-audit"].tier).toBe("recommended");
61
+ });
62
+
63
+ test("checkstyle is essential for java", () => {
64
+ expect(TOOL_REGISTRY.checkstyle.languages).toContain("java");
65
+ expect(TOOL_REGISTRY.checkstyle.tier).toBe("essential");
66
+ });
67
+
68
+ test("spotbugs is recommended for java", () => {
69
+ expect(TOOL_REGISTRY.spotbugs.languages).toContain("java");
70
+ expect(TOOL_REGISTRY.spotbugs.tier).toBe("recommended");
71
+ });
72
+
73
+ test("pmd is recommended for java", () => {
74
+ expect(TOOL_REGISTRY.pmd.languages).toContain("java");
75
+ expect(TOOL_REGISTRY.pmd.tier).toBe("recommended");
76
+ });
77
+
78
+ test("dotnet-format is essential for dotnet", () => {
79
+ expect(TOOL_REGISTRY["dotnet-format"].languages).toContain("dotnet");
80
+ expect(TOOL_REGISTRY["dotnet-format"].tier).toBe("essential");
81
+ });
82
+
83
+ test("stryker is optional for typescript/javascript", () => {
84
+ expect(TOOL_REGISTRY.stryker.languages).toContain("typescript");
85
+ expect(TOOL_REGISTRY.stryker.languages).toContain("javascript");
86
+ expect(TOOL_REGISTRY.stryker.tier).toBe("optional");
87
+ });
88
+
89
+ test("playwright is optional for typescript/javascript", () => {
90
+ expect(TOOL_REGISTRY.playwright.languages).toContain("typescript");
91
+ expect(TOOL_REGISTRY.playwright.languages).toContain("javascript");
92
+ expect(TOOL_REGISTRY.playwright.tier).toBe("optional");
93
+ });
94
+
95
+ test("lighthouse is optional for typescript/javascript", () => {
96
+ expect(TOOL_REGISTRY.lighthouse.languages).toContain("typescript");
97
+ expect(TOOL_REGISTRY.lighthouse.languages).toContain("javascript");
98
+ expect(TOOL_REGISTRY.lighthouse.tier).toBe("optional");
99
+ });
100
+
101
+ test("sonarqube is universal and optional", () => {
102
+ expect(TOOL_REGISTRY.sonarqube.languages).toContain("*");
103
+ expect(TOOL_REGISTRY.sonarqube.tier).toBe("optional");
104
+ });
105
+
106
+ test("diff-cover is universal and optional", () => {
107
+ expect(TOOL_REGISTRY["diff-cover"].languages).toContain("*");
108
+ expect(TOOL_REGISTRY["diff-cover"].tier).toBe("optional");
109
+ });
110
+
111
+ test("zap is universal and optional", () => {
112
+ expect(TOOL_REGISTRY.zap.languages).toContain("*");
113
+ expect(TOOL_REGISTRY.zap.tier).toBe("optional");
114
+ });
115
+ });
116
+
117
+ // ─── getToolsForLanguages ──────────────────────────────────────────────────
118
+
119
+ describe("getToolsForLanguages", () => {
120
+ test("returns only typescript-relevant tools for ['typescript']", () => {
121
+ const tools = getToolsForLanguages(["typescript"]);
122
+ const names = tools.map((t) => t.name);
123
+
124
+ // Must include TS-specific tools
125
+ expect(names).toContain("biome");
126
+ expect(names).toContain("stryker");
127
+ expect(names).toContain("playwright");
128
+ expect(names).toContain("lighthouse");
129
+
130
+ // Must include universal tools
131
+ expect(names).toContain("semgrep");
132
+ expect(names).toContain("trivy");
133
+ expect(names).toContain("secretlint");
134
+ expect(names).toContain("sonarqube");
135
+ expect(names).toContain("diff-cover");
136
+ expect(names).toContain("zap");
137
+
138
+ // Must NOT include language-specific tools for other languages
139
+ expect(names).not.toContain("ruff");
140
+ expect(names).not.toContain("golangci-lint");
141
+ expect(names).not.toContain("cargo-clippy");
142
+ expect(names).not.toContain("cargo-audit");
143
+ expect(names).not.toContain("checkstyle");
144
+ expect(names).not.toContain("spotbugs");
145
+ expect(names).not.toContain("pmd");
146
+ expect(names).not.toContain("dotnet-format");
147
+ });
148
+
149
+ test("returns only python-relevant tools for ['python']", () => {
150
+ const tools = getToolsForLanguages(["python"]);
151
+ const names = tools.map((t) => t.name);
152
+
153
+ expect(names).toContain("ruff");
154
+ expect(names).toContain("semgrep"); // universal
155
+ expect(names).toContain("trivy"); // universal
156
+
157
+ expect(names).not.toContain("biome");
158
+ expect(names).not.toContain("golangci-lint");
159
+ expect(names).not.toContain("cargo-clippy");
160
+ });
161
+
162
+ test("returns only go-relevant tools for ['go']", () => {
163
+ const tools = getToolsForLanguages(["go"]);
164
+ const names = tools.map((t) => t.name);
165
+
166
+ expect(names).toContain("golangci-lint");
167
+ expect(names).toContain("semgrep"); // universal
168
+
169
+ expect(names).not.toContain("biome");
170
+ expect(names).not.toContain("ruff");
171
+ });
172
+
173
+ test("returns only rust-relevant tools for ['rust']", () => {
174
+ const tools = getToolsForLanguages(["rust"]);
175
+ const names = tools.map((t) => t.name);
176
+
177
+ expect(names).toContain("cargo-clippy");
178
+ expect(names).toContain("cargo-audit");
179
+ expect(names).toContain("semgrep"); // universal
180
+
181
+ expect(names).not.toContain("biome");
182
+ expect(names).not.toContain("ruff");
183
+ });
184
+
185
+ test("returns only java-relevant tools for ['java']", () => {
186
+ const tools = getToolsForLanguages(["java"]);
187
+ const names = tools.map((t) => t.name);
188
+
189
+ expect(names).toContain("checkstyle");
190
+ expect(names).toContain("spotbugs");
191
+ expect(names).toContain("pmd");
192
+ expect(names).toContain("semgrep"); // universal
193
+
194
+ expect(names).not.toContain("biome");
195
+ expect(names).not.toContain("ruff");
196
+ });
197
+
198
+ test("returns only dotnet-relevant tools for ['dotnet']", () => {
199
+ const tools = getToolsForLanguages(["dotnet"]);
200
+ const names = tools.map((t) => t.name);
201
+
202
+ expect(names).toContain("dotnet-format");
203
+ expect(names).toContain("semgrep"); // universal
204
+
205
+ expect(names).not.toContain("biome");
206
+ expect(names).not.toContain("ruff");
207
+ });
208
+
209
+ test("multi-language returns union of tools", () => {
210
+ const tools = getToolsForLanguages(["typescript", "python"]);
211
+ const names = tools.map((t) => t.name);
212
+
213
+ // TS tools
214
+ expect(names).toContain("biome");
215
+ expect(names).toContain("stryker");
216
+ // Python tools
217
+ expect(names).toContain("ruff");
218
+ // Universal
219
+ expect(names).toContain("semgrep");
220
+
221
+ // Not Go/Rust/Java/Dotnet
222
+ expect(names).not.toContain("golangci-lint");
223
+ expect(names).not.toContain("cargo-clippy");
224
+ });
225
+
226
+ test("['unknown'] returns all tools (no filtering)", () => {
227
+ const allTools = getToolsForLanguages(["unknown"]);
228
+ const allNames: string[] = allTools.map((t) => t.name);
229
+ const registryNames = Object.keys(TOOL_REGISTRY);
230
+
231
+ expect(allNames.length).toBe(registryNames.length);
232
+ for (const name of registryNames) {
233
+ expect(allNames).toContain(name);
234
+ }
235
+ });
236
+
237
+ test("returns ToolRegistryEntry objects with name, languages, tier", () => {
238
+ const tools = getToolsForLanguages(["typescript"]);
239
+ for (const tool of tools) {
240
+ expect(tool.name).toBeDefined();
241
+ expect(tool.command).toBeDefined();
242
+ expect(tool.versionFlag).toBeDefined();
243
+ expect(tool.languages).toBeDefined();
244
+ expect(tool.tier).toBeDefined();
245
+ }
246
+ });
247
+
248
+ test("no duplicate tools when languages overlap in universal", () => {
249
+ const tools = getToolsForLanguages(["typescript", "python", "go"]);
250
+ const names = tools.map((t) => t.name);
251
+ const unique = [...new Set(names)];
252
+ expect(names.length).toBe(unique.length);
253
+ });
254
+ });
255
+
256
+ // ─── detectTools with language filter ──────────────────────────────────────
257
+
258
+ describe("detectTools with language filter", () => {
259
+ test("without languages parameter returns all tools (backward compatible)", async () => {
260
+ const results = await detectTools();
261
+ expect(results.length).toBe(Object.keys(TOOL_REGISTRY).length);
262
+ });
263
+
264
+ test("with ['typescript'] only detects relevant tools", async () => {
265
+ const results = await detectTools(["typescript"]);
266
+ const names = results.map((t) => t.name);
267
+
268
+ // Should include TS and universal tools
269
+ expect(names).toContain("biome");
270
+ expect(names).toContain("semgrep");
271
+
272
+ // Should NOT include Python/Go/Rust/Java/Dotnet-specific tools
273
+ expect(names).not.toContain("ruff");
274
+ expect(names).not.toContain("golangci-lint");
275
+ expect(names).not.toContain("cargo-clippy");
276
+ expect(names).not.toContain("checkstyle");
277
+ expect(names).not.toContain("dotnet-format");
278
+ });
279
+
280
+ test("with ['python'] only detects relevant tools", async () => {
281
+ const results = await detectTools(["python"]);
282
+ const names = results.map((t) => t.name);
283
+
284
+ expect(names).toContain("ruff");
285
+ expect(names).toContain("semgrep");
286
+ expect(names).not.toContain("biome");
287
+ expect(names).not.toContain("golangci-lint");
288
+ });
289
+
290
+ test("with ['unknown'] detects all tools", async () => {
291
+ const results = await detectTools(["unknown"]);
292
+ expect(results.length).toBe(Object.keys(TOOL_REGISTRY).length);
293
+ });
294
+
295
+ test("each filtered result has correct DetectedTool shape", async () => {
296
+ const results = await detectTools(["go"]);
297
+ for (const tool of results) {
298
+ expect(typeof tool.name).toBe("string");
299
+ expect(typeof tool.command).toBe("string");
300
+ expect(typeof tool.available).toBe("boolean");
301
+ }
302
+ });
303
+ });
@@ -37,30 +37,155 @@ export interface DetectedTool {
37
37
  available: boolean;
38
38
  }
39
39
 
40
- export const TOOL_REGISTRY: Record<
41
- ToolName,
42
- { command: string; versionFlag: string }
43
- > = {
44
- biome: { command: "biome", versionFlag: "--version" },
45
- semgrep: { command: "semgrep", versionFlag: "--version" },
46
- trivy: { command: "trivy", versionFlag: "--version" },
47
- secretlint: { command: "secretlint", versionFlag: "--version" },
48
- sonarqube: { command: "sonar-scanner", versionFlag: "--version" },
49
- stryker: { command: "stryker", versionFlag: "--version" },
50
- "diff-cover": { command: "diff-cover", versionFlag: "--version" },
51
- ruff: { command: "ruff", versionFlag: "--version" },
52
- "golangci-lint": { command: "golangci-lint", versionFlag: "--version" },
53
- "cargo-clippy": { command: "cargo", versionFlag: "clippy --version" },
54
- "cargo-audit": { command: "cargo-audit", versionFlag: "--version" },
55
- playwright: { command: "npx", versionFlag: "playwright --version" },
56
- "dotnet-format": { command: "dotnet", versionFlag: "format --version" },
57
- checkstyle: { command: "checkstyle", versionFlag: "--version" },
58
- spotbugs: { command: "spotbugs", versionFlag: "-version" },
59
- pmd: { command: "pmd", versionFlag: "--version" },
60
- zap: { command: "docker", versionFlag: "--version" },
61
- lighthouse: { command: "lighthouse", versionFlag: "--version" },
40
+ export type ToolTier = "essential" | "recommended" | "optional";
41
+
42
+ export interface ToolRegistryEntry {
43
+ command: string;
44
+ versionFlag: string;
45
+ languages: string[];
46
+ tier: ToolTier;
47
+ }
48
+
49
+ export const TOOL_REGISTRY: Record<ToolName, ToolRegistryEntry> = {
50
+ biome: {
51
+ command: "biome",
52
+ versionFlag: "--version",
53
+ languages: ["typescript", "javascript"],
54
+ tier: "essential",
55
+ },
56
+ semgrep: {
57
+ command: "semgrep",
58
+ versionFlag: "--version",
59
+ languages: ["*"],
60
+ tier: "recommended",
61
+ },
62
+ trivy: {
63
+ command: "trivy",
64
+ versionFlag: "--version",
65
+ languages: ["*"],
66
+ tier: "recommended",
67
+ },
68
+ secretlint: {
69
+ command: "secretlint",
70
+ versionFlag: "--version",
71
+ languages: ["*"],
72
+ tier: "recommended",
73
+ },
74
+ sonarqube: {
75
+ command: "sonar-scanner",
76
+ versionFlag: "--version",
77
+ languages: ["*"],
78
+ tier: "optional",
79
+ },
80
+ stryker: {
81
+ command: "stryker",
82
+ versionFlag: "--version",
83
+ languages: ["typescript", "javascript"],
84
+ tier: "optional",
85
+ },
86
+ "diff-cover": {
87
+ command: "diff-cover",
88
+ versionFlag: "--version",
89
+ languages: ["*"],
90
+ tier: "optional",
91
+ },
92
+ ruff: {
93
+ command: "ruff",
94
+ versionFlag: "--version",
95
+ languages: ["python"],
96
+ tier: "essential",
97
+ },
98
+ "golangci-lint": {
99
+ command: "golangci-lint",
100
+ versionFlag: "--version",
101
+ languages: ["go"],
102
+ tier: "essential",
103
+ },
104
+ "cargo-clippy": {
105
+ command: "cargo",
106
+ versionFlag: "clippy --version",
107
+ languages: ["rust"],
108
+ tier: "essential",
109
+ },
110
+ "cargo-audit": {
111
+ command: "cargo-audit",
112
+ versionFlag: "--version",
113
+ languages: ["rust"],
114
+ tier: "recommended",
115
+ },
116
+ playwright: {
117
+ command: "npx",
118
+ versionFlag: "playwright --version",
119
+ languages: ["typescript", "javascript"],
120
+ tier: "optional",
121
+ },
122
+ "dotnet-format": {
123
+ command: "dotnet",
124
+ versionFlag: "format --version",
125
+ languages: ["dotnet"],
126
+ tier: "essential",
127
+ },
128
+ checkstyle: {
129
+ command: "checkstyle",
130
+ versionFlag: "--version",
131
+ languages: ["java"],
132
+ tier: "essential",
133
+ },
134
+ spotbugs: {
135
+ command: "spotbugs",
136
+ versionFlag: "-version",
137
+ languages: ["java"],
138
+ tier: "recommended",
139
+ },
140
+ pmd: {
141
+ command: "pmd",
142
+ versionFlag: "--version",
143
+ languages: ["java"],
144
+ tier: "recommended",
145
+ },
146
+ zap: {
147
+ command: "docker",
148
+ versionFlag: "--version",
149
+ languages: ["*"],
150
+ tier: "optional",
151
+ },
152
+ lighthouse: {
153
+ command: "lighthouse",
154
+ versionFlag: "--version",
155
+ languages: ["typescript", "javascript"],
156
+ tier: "optional",
157
+ },
62
158
  };
63
159
 
160
+ /**
161
+ * Filter the tool registry by project languages.
162
+ * Universal tools (languages: ["*"]) are always included.
163
+ * If ["unknown"] is passed, returns all tools (no filtering).
164
+ * Returns an array of { name, ...ToolRegistryEntry } objects.
165
+ */
166
+ export function getToolsForLanguages(
167
+ languages: string[],
168
+ ): (ToolRegistryEntry & { name: ToolName })[] {
169
+ const entries = Object.entries(TOOL_REGISTRY) as [
170
+ ToolName,
171
+ ToolRegistryEntry,
172
+ ][];
173
+
174
+ // unknown → return everything
175
+ if (languages.includes("unknown")) {
176
+ return entries.map(([name, entry]) => ({ name, ...entry }));
177
+ }
178
+
179
+ return entries
180
+ .filter(([_, entry]) => {
181
+ // Universal tools always included
182
+ if (entry.languages.includes("*")) return true;
183
+ // Include if any of the tool's languages match the project's languages
184
+ return entry.languages.some((lang) => languages.includes(lang));
185
+ })
186
+ .map(([name, entry]) => ({ name, ...entry }));
187
+ }
188
+
64
189
  /**
65
190
  * Parse a version string from command output.
66
191
  * Looks for common version patterns like "1.2.3", "v1.2.3", "Version: 1.2.3".
@@ -165,11 +290,23 @@ export async function detectTool(name: ToolName): Promise<DetectedTool> {
165
290
  }
166
291
 
167
292
  /**
168
- * Detect all registered tools in parallel.
293
+ * Detect registered tools in parallel.
294
+ * When `languages` is provided, only tools relevant to those languages are detected.
295
+ * When omitted, all registered tools are detected (backward compatible).
169
296
  * Returns an array of DetectedTool in registry order.
170
297
  */
171
- export async function detectTools(): Promise<DetectedTool[]> {
172
- const names = Object.keys(TOOL_REGISTRY) as ToolName[];
298
+ export async function detectTools(
299
+ languages?: string[],
300
+ ): Promise<DetectedTool[]> {
301
+ let names: ToolName[];
302
+
303
+ if (languages) {
304
+ const filtered = getToolsForLanguages(languages);
305
+ names = filtered.map((t) => t.name);
306
+ } else {
307
+ names = Object.keys(TOOL_REGISTRY) as ToolName[];
308
+ }
309
+
173
310
  const results = await Promise.all(names.map((name) => detectTool(name)));
174
311
  return results;
175
312
  }