@boyingliu01/xp-gate 0.15.0 → 0.15.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.
- package/bin/xp-gate.js +24 -0
- package/hooks/pre-commit +24 -2
- package/hooks/pre-push +204 -14
- package/lib/doctor.js +50 -0
- package/lib/init.js +91 -0
- package/lib/language-tools.js +833 -0
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-specific tool registry for quality gates.
|
|
3
|
+
* Maps each language to its required/optional tools and config files.
|
|
4
|
+
*
|
|
5
|
+
* @module language-tools
|
|
6
|
+
* @covers Sprint Multi-Language Support
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tool definition for quality gate language tools.
|
|
16
|
+
* @typedef {Object} ToolDef
|
|
17
|
+
* @property {string} name - Tool name (executable)
|
|
18
|
+
* @property {string} checkCmd - Command to check version
|
|
19
|
+
* @property {Object.<string, string>} install - Install commands per platform
|
|
20
|
+
* @property {boolean} optional - If true, tool is optional (SKIP if missing)
|
|
21
|
+
* @property {string[]} gates - Which gates use this tool
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Language definition with tools and config files.
|
|
26
|
+
* @typedef {Object} LangDef
|
|
27
|
+
* @property {string} name - Language identifier
|
|
28
|
+
* @property {ToolDef[]} tools - Required and optional tools
|
|
29
|
+
* @property {string[]} configFiles - Project config files that indicate this language
|
|
30
|
+
* @property {string[]} extensions - File extensions for this language
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Registry of all supported languages and their tools.
|
|
35
|
+
*/
|
|
36
|
+
const LANGUAGE_REGISTRY = {
|
|
37
|
+
typescript: {
|
|
38
|
+
name: 'TypeScript/JavaScript',
|
|
39
|
+
extensions: ['ts', 'tsx', 'js', 'jsx', 'mjs'],
|
|
40
|
+
configFiles: ['package.json', 'tsconfig.json'],
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: 'eslint',
|
|
44
|
+
checkCmd: 'eslint --version',
|
|
45
|
+
install: {
|
|
46
|
+
darwin: 'npm install -g eslint',
|
|
47
|
+
linux: 'npm install -g eslint',
|
|
48
|
+
win32: 'npm install -g eslint',
|
|
49
|
+
},
|
|
50
|
+
optional: false,
|
|
51
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
python: {
|
|
57
|
+
name: 'Python',
|
|
58
|
+
extensions: ['py'],
|
|
59
|
+
configFiles: ['pyproject.toml', 'setup.py', 'requirements.txt', 'Pipfile'],
|
|
60
|
+
tools: [
|
|
61
|
+
{
|
|
62
|
+
name: 'mypy',
|
|
63
|
+
checkCmd: 'mypy --version',
|
|
64
|
+
install: {
|
|
65
|
+
darwin: 'pip install mypy',
|
|
66
|
+
linux: 'pip install mypy',
|
|
67
|
+
win32: 'pip install mypy',
|
|
68
|
+
},
|
|
69
|
+
optional: false,
|
|
70
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'ruff',
|
|
74
|
+
checkCmd: 'ruff --version',
|
|
75
|
+
install: {
|
|
76
|
+
darwin: 'pip install ruff',
|
|
77
|
+
linux: 'pip install ruff',
|
|
78
|
+
win32: 'pip install ruff',
|
|
79
|
+
},
|
|
80
|
+
optional: true, // fallback to flake8
|
|
81
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'flake8',
|
|
85
|
+
checkCmd: 'flake8 --version',
|
|
86
|
+
install: {
|
|
87
|
+
darwin: 'pip install flake8',
|
|
88
|
+
linux: 'pip install flake8',
|
|
89
|
+
win32: 'pip install flake8',
|
|
90
|
+
},
|
|
91
|
+
optional: true, // fallback if no ruff
|
|
92
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'pytest',
|
|
96
|
+
checkCmd: 'pytest --version',
|
|
97
|
+
install: {
|
|
98
|
+
darwin: 'pip install pytest',
|
|
99
|
+
linux: 'pip install pytest',
|
|
100
|
+
win32: 'pip install pytest',
|
|
101
|
+
},
|
|
102
|
+
optional: false,
|
|
103
|
+
gates: ['Gate 5 (Tests + Coverage)'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'black',
|
|
107
|
+
checkCmd: 'black --version',
|
|
108
|
+
install: {
|
|
109
|
+
darwin: 'pip install black',
|
|
110
|
+
linux: 'pip install black',
|
|
111
|
+
win32: 'pip install black',
|
|
112
|
+
},
|
|
113
|
+
optional: true,
|
|
114
|
+
gates: ['Gate 1 (Code Formatting)'],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
java: {
|
|
120
|
+
name: 'Java',
|
|
121
|
+
extensions: ['java'],
|
|
122
|
+
configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
|
123
|
+
tools: [
|
|
124
|
+
{
|
|
125
|
+
name: 'mvn',
|
|
126
|
+
checkCmd: 'mvn --version',
|
|
127
|
+
install: {
|
|
128
|
+
darwin: 'brew install maven',
|
|
129
|
+
linux: 'apt-get install maven || yum install maven',
|
|
130
|
+
win32: 'winget install Apache.Maven',
|
|
131
|
+
},
|
|
132
|
+
optional: true, // only if Maven project
|
|
133
|
+
gates: ['Gate 1 (Build)'],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'gradle',
|
|
137
|
+
checkCmd: 'gradle --version',
|
|
138
|
+
install: {
|
|
139
|
+
darwin: 'brew install gradle',
|
|
140
|
+
linux: 'apt-get install gradle || yum install gradle',
|
|
141
|
+
win32: 'winget install Gradle.Gradle',
|
|
142
|
+
},
|
|
143
|
+
optional: true, // only if Gradle project
|
|
144
|
+
gates: ['Gate 1 (Build)'],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'checkstyle',
|
|
148
|
+
checkCmd: 'checkstyle --version',
|
|
149
|
+
install: {
|
|
150
|
+
darwin: 'brew install checkstyle',
|
|
151
|
+
linux: 'apt-get install checkstyle',
|
|
152
|
+
win32: 'choco install checkstyle',
|
|
153
|
+
},
|
|
154
|
+
optional: true,
|
|
155
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'pmd',
|
|
159
|
+
checkCmd: 'pmd --version',
|
|
160
|
+
install: {
|
|
161
|
+
darwin: 'brew install pmd',
|
|
162
|
+
linux: 'apt-get install pmd',
|
|
163
|
+
win32: 'choco install pmd',
|
|
164
|
+
},
|
|
165
|
+
optional: true,
|
|
166
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
go: {
|
|
172
|
+
name: 'Go',
|
|
173
|
+
extensions: ['go'],
|
|
174
|
+
configFiles: ['go.mod', 'go.sum'],
|
|
175
|
+
tools: [
|
|
176
|
+
{
|
|
177
|
+
name: 'go',
|
|
178
|
+
checkCmd: 'go version',
|
|
179
|
+
install: {
|
|
180
|
+
darwin: 'brew install go',
|
|
181
|
+
linux: 'apt-get install golang || yum install golang',
|
|
182
|
+
win32: 'winget install GoLang.Go',
|
|
183
|
+
},
|
|
184
|
+
optional: false,
|
|
185
|
+
gates: ['Gate 1 (Build + Quality)'],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'golangci-lint',
|
|
189
|
+
checkCmd: 'golangci-lint --version',
|
|
190
|
+
install: {
|
|
191
|
+
darwin: 'brew install golangci-lint',
|
|
192
|
+
linux: 'go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest',
|
|
193
|
+
win32: 'go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest',
|
|
194
|
+
},
|
|
195
|
+
optional: true,
|
|
196
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
kotlin: {
|
|
202
|
+
name: 'Kotlin',
|
|
203
|
+
extensions: ['kt', 'kts'],
|
|
204
|
+
configFiles: ['build.gradle', 'build.gradle.kts', 'pom.xml'],
|
|
205
|
+
tools: [
|
|
206
|
+
{
|
|
207
|
+
name: 'gradle',
|
|
208
|
+
checkCmd: 'gradle --version',
|
|
209
|
+
install: {
|
|
210
|
+
darwin: 'brew install gradle',
|
|
211
|
+
linux: 'apt-get install gradle',
|
|
212
|
+
win32: 'winget install Gradle.Gradle',
|
|
213
|
+
},
|
|
214
|
+
optional: true,
|
|
215
|
+
gates: ['Gate 1 (Build)'],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'ktlint',
|
|
219
|
+
checkCmd: 'ktlint --version',
|
|
220
|
+
install: {
|
|
221
|
+
darwin: 'brew install ktlint',
|
|
222
|
+
linux: 'curl -sSLO https://github.com/pinterest/ktlint/releases/latest/download/ktlint && chmod +x ktlint',
|
|
223
|
+
win32: 'choco install ktlint',
|
|
224
|
+
},
|
|
225
|
+
optional: true,
|
|
226
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
swift: {
|
|
232
|
+
name: 'Swift',
|
|
233
|
+
extensions: ['swift'],
|
|
234
|
+
configFiles: ['Package.swift', 'project.xcodeproj', 'project.pbxproj'],
|
|
235
|
+
tools: [
|
|
236
|
+
{
|
|
237
|
+
name: 'swift',
|
|
238
|
+
checkCmd: 'swift --version',
|
|
239
|
+
install: {
|
|
240
|
+
darwin: 'xcode-select --install',
|
|
241
|
+
linux: 'See https://swift.org/download/',
|
|
242
|
+
win32: 'See https://swift.org/download/',
|
|
243
|
+
},
|
|
244
|
+
optional: false,
|
|
245
|
+
gates: ['Gate 1 (Build + Quality)'],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'swiftlint',
|
|
249
|
+
checkCmd: 'swiftlint --version',
|
|
250
|
+
install: {
|
|
251
|
+
darwin: 'brew install swiftlint',
|
|
252
|
+
linux: 'See https://github.com/realm/SwiftLint',
|
|
253
|
+
win32: 'See https://github.com/realm/SwiftLint',
|
|
254
|
+
},
|
|
255
|
+
optional: true,
|
|
256
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
dart: {
|
|
262
|
+
name: 'Dart',
|
|
263
|
+
extensions: ['dart'],
|
|
264
|
+
configFiles: ['pubspec.yaml'],
|
|
265
|
+
tools: [
|
|
266
|
+
{
|
|
267
|
+
name: 'dart',
|
|
268
|
+
checkCmd: 'dart --version',
|
|
269
|
+
install: {
|
|
270
|
+
darwin: 'brew tap dart-lang/dart && brew install dart',
|
|
271
|
+
linux: 'See https://dart.dev/get-dart',
|
|
272
|
+
win32: 'winget install Dart-Dart.dart',
|
|
273
|
+
},
|
|
274
|
+
optional: false,
|
|
275
|
+
gates: ['Gate 1 (Build + Quality)'],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
flutter: {
|
|
281
|
+
name: 'Flutter',
|
|
282
|
+
extensions: ['dart'],
|
|
283
|
+
configFiles: ['pubspec.yaml'],
|
|
284
|
+
tools: [
|
|
285
|
+
{
|
|
286
|
+
name: 'flutter',
|
|
287
|
+
checkCmd: 'flutter --version',
|
|
288
|
+
install: {
|
|
289
|
+
darwin: 'brew install --cask flutter',
|
|
290
|
+
linux: 'See https://flutter.dev/docs/get-started/install',
|
|
291
|
+
win32: 'winget install Flutter.Flutter',
|
|
292
|
+
},
|
|
293
|
+
optional: false,
|
|
294
|
+
gates: ['Gate 1 (Build + Quality)'],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
cpp: {
|
|
300
|
+
name: 'C/C++',
|
|
301
|
+
extensions: ['cpp', 'cxx', 'cc', 'c', 'hpp', 'h'],
|
|
302
|
+
configFiles: ['CMakeLists.txt', 'Makefile', 'conanfile.txt', 'vcpkg.json'],
|
|
303
|
+
tools: [
|
|
304
|
+
{
|
|
305
|
+
name: 'cmake',
|
|
306
|
+
checkCmd: 'cmake --version',
|
|
307
|
+
install: {
|
|
308
|
+
darwin: 'brew install cmake',
|
|
309
|
+
linux: 'apt-get install cmake || yum install cmake',
|
|
310
|
+
win32: 'winget install Kitware.CMake',
|
|
311
|
+
},
|
|
312
|
+
optional: true,
|
|
313
|
+
gates: ['Gate 1 (Build)'],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'clang-tidy',
|
|
317
|
+
checkCmd: 'clang-tidy --version',
|
|
318
|
+
install: {
|
|
319
|
+
darwin: 'brew install llvm',
|
|
320
|
+
linux: 'apt-get install clang-tidy',
|
|
321
|
+
win32: 'See LLVM installer',
|
|
322
|
+
},
|
|
323
|
+
optional: true,
|
|
324
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
shell: {
|
|
330
|
+
name: 'Shell',
|
|
331
|
+
extensions: ['sh', 'bash'],
|
|
332
|
+
configFiles: [],
|
|
333
|
+
tools: [
|
|
334
|
+
{
|
|
335
|
+
name: 'shellcheck',
|
|
336
|
+
checkCmd: 'shellcheck --version',
|
|
337
|
+
install: {
|
|
338
|
+
darwin: 'brew install shellcheck',
|
|
339
|
+
linux: 'apt-get install shellcheck || yum install shellcheck',
|
|
340
|
+
win32: 'winget install ShellCheck.ShellCheck',
|
|
341
|
+
},
|
|
342
|
+
optional: true,
|
|
343
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
powershell: {
|
|
349
|
+
name: 'PowerShell',
|
|
350
|
+
extensions: ['ps1', 'psm1', 'psd1'],
|
|
351
|
+
configFiles: [],
|
|
352
|
+
tools: [
|
|
353
|
+
{
|
|
354
|
+
name: 'pwsh',
|
|
355
|
+
checkCmd: 'pwsh --version',
|
|
356
|
+
install: {
|
|
357
|
+
darwin: 'brew install --cask powershell',
|
|
358
|
+
linux: 'apt-get install powershell || yum install powershell',
|
|
359
|
+
win32: 'winget install Microsoft.PowerShell',
|
|
360
|
+
},
|
|
361
|
+
optional: false,
|
|
362
|
+
gates: ['Gate 1 (Build + Quality)'],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'psscriptanalyzer',
|
|
366
|
+
checkCmd: 'pwsh -Command "Get-Module -List PSScriptAnalyzer"',
|
|
367
|
+
install: {
|
|
368
|
+
darwin: 'pwsh -Command "Install-Module -Name PSScriptAnalyzer -Force"',
|
|
369
|
+
linux: 'pwsh -Command "Install-Module -Name PSScriptAnalyzer -Force"',
|
|
370
|
+
win32: 'pwsh -Command "Install-Module -Name PSScriptAnalyzer -Force"',
|
|
371
|
+
},
|
|
372
|
+
optional: true,
|
|
373
|
+
gates: ['Gate 1 (Code Quality)'],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
iac: {
|
|
379
|
+
name: 'Infrastructure as Code',
|
|
380
|
+
extensions: ['tf', 'tfvars', 'yaml', 'yml'],
|
|
381
|
+
configFiles: ['*.tf', 'docker-compose*.yaml', 'k8s/*.yaml'],
|
|
382
|
+
tools: [
|
|
383
|
+
{
|
|
384
|
+
name: 'terraform',
|
|
385
|
+
checkCmd: 'terraform --version',
|
|
386
|
+
install: {
|
|
387
|
+
darwin: 'brew install terraform',
|
|
388
|
+
linux: 'See https://developer.hashicorp.com/terraform/downloads',
|
|
389
|
+
win32: 'winget install Hashicorp.Terraform',
|
|
390
|
+
},
|
|
391
|
+
optional: true,
|
|
392
|
+
gates: ['Gate 7 (IaC Security)'],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'tflint',
|
|
396
|
+
checkCmd: 'tflint --version',
|
|
397
|
+
install: {
|
|
398
|
+
darwin: 'brew install tflint',
|
|
399
|
+
linux: 'See https://github.com/terraform-linters/tflint',
|
|
400
|
+
win32: 'See https://github.com/terraform-linters/tflint',
|
|
401
|
+
},
|
|
402
|
+
optional: true,
|
|
403
|
+
gates: ['Gate 7 (IaC Security)'],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Detect languages used in the current project.
|
|
411
|
+
* @param {string} projectRoot - Project root directory
|
|
412
|
+
* @returns {{detected: string[], configFiles: Object.<string, string[]>}}
|
|
413
|
+
*/
|
|
414
|
+
function detectProjectLanguages(projectRoot) {
|
|
415
|
+
const detected = new Set();
|
|
416
|
+
const configFiles = {};
|
|
417
|
+
|
|
418
|
+
for (const [lang, def] of Object.entries(LANGUAGE_REGISTRY)) {
|
|
419
|
+
const foundConfigs = [];
|
|
420
|
+
|
|
421
|
+
for (const configFile of def.configFiles) {
|
|
422
|
+
// Handle glob patterns
|
|
423
|
+
if (configFile.includes('*')) {
|
|
424
|
+
// Simple glob matching
|
|
425
|
+
const pattern = configFile.replace(/\*/g, '.*');
|
|
426
|
+
const regex = new RegExp(pattern);
|
|
427
|
+
try {
|
|
428
|
+
const files = fs.readdirSync(projectRoot);
|
|
429
|
+
for (const file of files) {
|
|
430
|
+
if (regex.test(file)) {
|
|
431
|
+
foundConfigs.push(file);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch { /* ignore */ }
|
|
435
|
+
} else {
|
|
436
|
+
const configPath = path.join(projectRoot, configFile);
|
|
437
|
+
if (fs.existsSync(configPath)) {
|
|
438
|
+
foundConfigs.push(configFile);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (foundConfigs.length > 0) {
|
|
444
|
+
detected.add(lang);
|
|
445
|
+
configFiles[lang] = foundConfigs;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Special case: Flutter vs Dart
|
|
450
|
+
if (detected.has('flutter') && detected.has('dart')) {
|
|
451
|
+
detected.delete('dart'); // Flutter implies Dart
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check for Flutter specifically
|
|
455
|
+
const pubspecPath = path.join(projectRoot, 'pubspec.yaml');
|
|
456
|
+
if (fs.existsSync(pubspecPath)) {
|
|
457
|
+
try {
|
|
458
|
+
const content = fs.readFileSync(pubspecPath, 'utf8');
|
|
459
|
+
if (content.includes('flutter:')) {
|
|
460
|
+
detected.delete('dart');
|
|
461
|
+
detected.add('flutter');
|
|
462
|
+
}
|
|
463
|
+
} catch { /* ignore */ }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
detected: Array.from(detected),
|
|
468
|
+
configFiles,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Check if a CLI tool is available.
|
|
474
|
+
* @param {string} toolName - Tool name
|
|
475
|
+
* @param {string} checkCmd - Command to check version
|
|
476
|
+
* @returns {{available: boolean, version?: string}}
|
|
477
|
+
*/
|
|
478
|
+
function checkTool(toolName, checkCmd) {
|
|
479
|
+
const isWindows = process.platform === 'win32';
|
|
480
|
+
const shell = isWindows ? 'cmd.exe' : '/bin/sh';
|
|
481
|
+
const nullRedir = isWindows ? '2>nul' : '2>/dev/null';
|
|
482
|
+
const execOpts = {
|
|
483
|
+
encoding: 'utf8',
|
|
484
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
485
|
+
shell,
|
|
486
|
+
timeout: 10000
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const result = execSync(`${checkCmd} ${nullRedir}`, execOpts);
|
|
491
|
+
return { available: true, version: result.trim().split('\n')[0] };
|
|
492
|
+
} catch {
|
|
493
|
+
return { available: false };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get required tools for detected languages.
|
|
499
|
+
* @param {string[]} languages - Detected languages
|
|
500
|
+
* @returns {Object.<string, Array<{tool: ToolDef, status: string}>>}
|
|
501
|
+
*/
|
|
502
|
+
function getToolsForLanguages(languages) {
|
|
503
|
+
const result = {};
|
|
504
|
+
|
|
505
|
+
for (const lang of languages) {
|
|
506
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
507
|
+
if (!langDef) continue;
|
|
508
|
+
|
|
509
|
+
result[lang] = [];
|
|
510
|
+
|
|
511
|
+
for (const tool of langDef.tools) {
|
|
512
|
+
const status = checkTool(tool.name, tool.checkCmd);
|
|
513
|
+
result[lang].push({
|
|
514
|
+
tool,
|
|
515
|
+
status: status.available ? 'available' : 'missing',
|
|
516
|
+
version: status.version,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Install a tool using the appropriate command for the current platform.
|
|
526
|
+
* @param {ToolDef} toolDef - Tool definition
|
|
527
|
+
* @returns {{success: boolean, message: string}}
|
|
528
|
+
*/
|
|
529
|
+
function installTool(toolDef) {
|
|
530
|
+
const platform = process.platform;
|
|
531
|
+
const installCmd = toolDef.install[platform] || toolDef.install.linux;
|
|
532
|
+
|
|
533
|
+
if (!installCmd) {
|
|
534
|
+
return { success: false, message: `No install command for platform ${platform}` };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const isWindows = platform === 'win32';
|
|
538
|
+
const shell = isWindows ? 'cmd.exe' : '/bin/sh';
|
|
539
|
+
const execOpts = {
|
|
540
|
+
encoding: 'utf8',
|
|
541
|
+
stdio: 'inherit',
|
|
542
|
+
shell,
|
|
543
|
+
timeout: 120000
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
console.log(`Installing ${toolDef.name}: ${installCmd}`);
|
|
548
|
+
execSync(installCmd, execOpts);
|
|
549
|
+
return { success: true, message: `${toolDef.name} installed successfully` };
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return { success: false, message: `Failed to install ${toolDef.name}: ${error.message}` };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Generate project language config file.
|
|
557
|
+
* @param {string} projectRoot - Project root
|
|
558
|
+
* @param {string[]} languages - Detected languages
|
|
559
|
+
* @param {Object} toolStatus - Tool status per language
|
|
560
|
+
*/
|
|
561
|
+
function generateProjectConfig(projectRoot, languages, toolStatus) {
|
|
562
|
+
const config = {
|
|
563
|
+
version: 1,
|
|
564
|
+
languages: languages.reduce((acc, lang) => {
|
|
565
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
566
|
+
acc[lang] = {
|
|
567
|
+
name: langDef?.name || lang,
|
|
568
|
+
tools: (toolStatus[lang] || []).map(t => ({
|
|
569
|
+
name: t.tool.name,
|
|
570
|
+
status: t.status,
|
|
571
|
+
optional: t.tool.optional,
|
|
572
|
+
gates: t.tool.gates,
|
|
573
|
+
})),
|
|
574
|
+
};
|
|
575
|
+
return acc;
|
|
576
|
+
}, {}),
|
|
577
|
+
lastDetected: new Date().toISOString(),
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const configPath = path.join(projectRoot, '.xp-gate-config.json');
|
|
581
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
582
|
+
|
|
583
|
+
return configPath;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Read existing project config if present.
|
|
588
|
+
* @param {string} projectRoot - Project root
|
|
589
|
+
* @returns {Object|null}
|
|
590
|
+
*/
|
|
591
|
+
function readProjectConfig(projectRoot) {
|
|
592
|
+
const configPath = path.join(projectRoot, '.xp-gate-config.json');
|
|
593
|
+
if (!fs.existsSync(configPath)) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = {
|
|
605
|
+
LANGUAGE_REGISTRY,
|
|
606
|
+
detectProjectLanguages,
|
|
607
|
+
checkTool,
|
|
608
|
+
getToolsForLanguages,
|
|
609
|
+
installTool,
|
|
610
|
+
generateProjectConfig,
|
|
611
|
+
readProjectConfig,
|
|
612
|
+
handleDetectLanguages,
|
|
613
|
+
handleCheckTools,
|
|
614
|
+
handleInstallTools,
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Handle detect-languages CLI command.
|
|
619
|
+
* @param {string[]} args - Command arguments
|
|
620
|
+
* @returns {Promise<number>} Exit code
|
|
621
|
+
*/
|
|
622
|
+
async function handleDetectLanguages(args) {
|
|
623
|
+
const projectRoot = process.cwd();
|
|
624
|
+
const jsonOutput = args.includes('--json');
|
|
625
|
+
const writeConfig = args.includes('--write') || !jsonOutput;
|
|
626
|
+
|
|
627
|
+
console.log('Detecting project languages...');
|
|
628
|
+
console.log(`Project root: ${projectRoot}\n`);
|
|
629
|
+
|
|
630
|
+
const { detected, configFiles } = detectProjectLanguages(projectRoot);
|
|
631
|
+
|
|
632
|
+
if (detected.length === 0) {
|
|
633
|
+
if (jsonOutput) {
|
|
634
|
+
console.log(JSON.stringify({ languages: [], configFiles: {} }, null, 2));
|
|
635
|
+
} else {
|
|
636
|
+
console.log('No languages detected.');
|
|
637
|
+
}
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const toolStatus = getToolsForLanguages(detected);
|
|
642
|
+
|
|
643
|
+
if (jsonOutput) {
|
|
644
|
+
const result = {
|
|
645
|
+
languages: detected,
|
|
646
|
+
configFiles,
|
|
647
|
+
tools: {},
|
|
648
|
+
};
|
|
649
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
650
|
+
result.tools[lang] = tools.map(t => ({
|
|
651
|
+
name: t.tool.name,
|
|
652
|
+
status: t.status,
|
|
653
|
+
optional: t.tool.optional,
|
|
654
|
+
version: t.version,
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
console.log(JSON.stringify(result, null, 2));
|
|
658
|
+
} else {
|
|
659
|
+
console.log('Detected languages:');
|
|
660
|
+
for (const lang of detected) {
|
|
661
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
662
|
+
console.log(` • ${langDef?.name || lang} (${lang})`);
|
|
663
|
+
if (configFiles[lang]) {
|
|
664
|
+
console.log(` Config files: ${configFiles[lang].join(', ')}`);
|
|
665
|
+
}
|
|
666
|
+
if (toolStatus[lang]) {
|
|
667
|
+
const available = toolStatus[lang].filter(t => t.status === 'available').length;
|
|
668
|
+
const total = toolStatus[lang].length;
|
|
669
|
+
console.log(` Tools: ${available}/${total} available`);
|
|
670
|
+
for (const t of toolStatus[lang]) {
|
|
671
|
+
const statusIcon = t.status === 'available' ? '✓' : '✗';
|
|
672
|
+
const optMark = t.tool.optional ? ' (optional)' : '';
|
|
673
|
+
console.log(` ${statusIcon} ${t.tool.name}${optMark}${t.version ? ` — ${t.version}` : ''}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (writeConfig) {
|
|
680
|
+
const configPath = generateProjectConfig(projectRoot, detected, toolStatus);
|
|
681
|
+
console.log(`\nGenerated config: ${configPath}`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Handle check-tools CLI command.
|
|
689
|
+
* @param {string[]} args - Command arguments
|
|
690
|
+
* @returns {Promise<number>} Exit code
|
|
691
|
+
*/
|
|
692
|
+
async function handleCheckTools(args) {
|
|
693
|
+
const projectRoot = process.cwd();
|
|
694
|
+
const jsonOutput = args.includes('--json');
|
|
695
|
+
const langsArg = args.find(a => a.startsWith('--languages='));
|
|
696
|
+
let languages = langsArg ? langsArg.split('=')[1].split(',') : null;
|
|
697
|
+
|
|
698
|
+
// Auto-detect if no languages specified
|
|
699
|
+
if (!languages) {
|
|
700
|
+
const detected = detectProjectLanguages(projectRoot);
|
|
701
|
+
languages = detected.detected;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (languages.length === 0) {
|
|
705
|
+
console.log('No languages detected or specified. Use --languages=<langs> to specify.');
|
|
706
|
+
return 1;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const toolStatus = getToolsForLanguages(languages);
|
|
710
|
+
|
|
711
|
+
if (jsonOutput) {
|
|
712
|
+
const result = {};
|
|
713
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
714
|
+
result[lang] = tools.map(t => ({
|
|
715
|
+
name: t.tool.name,
|
|
716
|
+
status: t.status,
|
|
717
|
+
optional: t.tool.optional,
|
|
718
|
+
version: t.version,
|
|
719
|
+
gates: t.tool.gates,
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
console.log(JSON.stringify(result, null, 2));
|
|
723
|
+
} else {
|
|
724
|
+
console.log('Quality Gate Tool Status:\n');
|
|
725
|
+
let totalAvailable = 0;
|
|
726
|
+
let totalMissing = 0;
|
|
727
|
+
|
|
728
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
729
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
730
|
+
console.log(`${langDef?.name || lang}:`);
|
|
731
|
+
|
|
732
|
+
for (const t of tools) {
|
|
733
|
+
const statusIcon = t.status === 'available' ? '✓' : '✗';
|
|
734
|
+
const optMark = t.tool.optional ? ' (optional)' : '';
|
|
735
|
+
const version = t.version || 'N/A';
|
|
736
|
+
console.log(` ${statusIcon} ${t.tool.name}${optMark} — ${version}`);
|
|
737
|
+
if (t.status === 'available') {
|
|
738
|
+
totalAvailable++;
|
|
739
|
+
} else {
|
|
740
|
+
totalMissing++;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
console.log('');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
console.log(`Summary: ${totalAvailable} available, ${totalMissing} missing`);
|
|
747
|
+
if (totalMissing > 0) {
|
|
748
|
+
console.log('Run `xp-gate install-tools` to install missing tools.');
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Handle install-tools CLI command.
|
|
757
|
+
* @param {string[]} args - Command arguments
|
|
758
|
+
* @returns {Promise<number>} Exit code
|
|
759
|
+
*/
|
|
760
|
+
async function handleInstallTools(args) {
|
|
761
|
+
const projectRoot = process.cwd();
|
|
762
|
+
const dryRun = args.includes('--dry-run');
|
|
763
|
+
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
764
|
+
const langsArg = args.find(a => a.startsWith('--languages='));
|
|
765
|
+
let languages = langsArg ? langsArg.split('=')[1].split(',') : null;
|
|
766
|
+
|
|
767
|
+
// Auto-detect if no languages specified
|
|
768
|
+
if (!languages) {
|
|
769
|
+
const detected = detectProjectLanguages(projectRoot);
|
|
770
|
+
languages = detected.detected;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (languages.length === 0) {
|
|
774
|
+
console.log('No languages detected or specified.');
|
|
775
|
+
return 1;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
console.log('Checking tools for languages:', languages.join(', '));
|
|
779
|
+
console.log('');
|
|
780
|
+
|
|
781
|
+
const toolStatus = getToolsForLanguages(languages);
|
|
782
|
+
const missingTools = [];
|
|
783
|
+
|
|
784
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
785
|
+
for (const t of tools) {
|
|
786
|
+
if (t.status === 'missing' && !t.tool.optional) {
|
|
787
|
+
missingTools.push({ lang, tool: t.tool });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (missingTools.length === 0) {
|
|
793
|
+
console.log('All required tools are available!');
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
console.log(`Found ${missingTools.length} missing required tool(s):\n`);
|
|
798
|
+
for (const { lang, tool } of missingTools) {
|
|
799
|
+
const installCmd = tool.install[process.platform] || tool.install.linux || 'N/A';
|
|
800
|
+
console.log(` • ${tool.name} (${lang})`);
|
|
801
|
+
console.log(` Install: ${installCmd}`);
|
|
802
|
+
}
|
|
803
|
+
console.log('');
|
|
804
|
+
|
|
805
|
+
if (dryRun) {
|
|
806
|
+
console.log('Dry run — no tools installed.');
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!autoYes) {
|
|
811
|
+
console.log('Run with --yes to install missing tools.');
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
console.log('Installing missing tools...\n');
|
|
816
|
+
let successCount = 0;
|
|
817
|
+
let failCount = 0;
|
|
818
|
+
|
|
819
|
+
for (const { lang, tool } of missingTools) {
|
|
820
|
+
console.log(`Installing ${tool.name} for ${lang}...`);
|
|
821
|
+
const result = installTool(tool);
|
|
822
|
+
if (result.success) {
|
|
823
|
+
console.log(` ✓ ${result.message}\n`);
|
|
824
|
+
successCount++;
|
|
825
|
+
} else {
|
|
826
|
+
console.log(` ✗ ${result.message}\n`);
|
|
827
|
+
failCount++;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
console.log(`Installation complete: ${successCount} succeeded, ${failCount} failed.`);
|
|
832
|
+
return failCount > 0 ? 1 : 0;
|
|
833
|
+
}
|