@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.
- package/package.json +1 -1
- package/src/cloud/__tests__/client.test.ts +171 -0
- package/src/cloud/auth.ts +1 -0
- package/src/cloud/client.ts +94 -0
- package/src/cloud/types.ts +48 -0
- package/src/context/engine.ts +72 -5
- package/src/feedback/__tests__/sync.test.ts +63 -1
- package/src/feedback/collector.ts +54 -0
- package/src/feedback/sync.ts +92 -3
- package/src/git/__tests__/git.test.ts +15 -0
- package/src/git/index.ts +25 -0
- package/src/index.ts +12 -1
- package/src/init/__tests__/detect-stack.test.ts +237 -0
- package/src/init/__tests__/init.test.ts +184 -0
- package/src/init/index.ts +443 -74
- package/src/verify/__tests__/detect-filter.test.ts +303 -0
- package/src/verify/detect.ts +162 -25
|
@@ -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
|
+
});
|
package/src/verify/detect.ts
CHANGED
|
@@ -37,30 +37,155 @@ export interface DetectedTool {
|
|
|
37
37
|
available: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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(
|
|
172
|
-
|
|
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
|
}
|