@boyingliu01/xp-gate 0.15.4 → 0.15.5

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 (72) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/gate-3.sh +68 -0
  6. package/adapters/gate-4.sh +80 -0
  7. package/adapters/gate-7.sh +45 -0
  8. package/adapters/gate-8.sh +56 -0
  9. package/adapters/gate-9.sh +104 -0
  10. package/adapters/go.sh +0 -0
  11. package/adapters/iac.sh +0 -0
  12. package/adapters/java.sh +0 -0
  13. package/adapters/kotlin.sh +0 -0
  14. package/adapters/objectivec.sh +0 -0
  15. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  16. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  17. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  18. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  19. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  20. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  21. package/adapters/powershell.sh +0 -0
  22. package/adapters/python.sh +0 -0
  23. package/adapters/shell.sh +0 -0
  24. package/adapters/swift.sh +0 -0
  25. package/adapters/typescript.sh +0 -0
  26. package/bin/xp-gate.js +0 -0
  27. package/gate-3.sh +0 -0
  28. package/gate-4.sh +0 -0
  29. package/gate-7.sh +0 -0
  30. package/gate-8.sh +0 -0
  31. package/gate-9.sh +0 -0
  32. package/hooks/adapter-common.sh +0 -0
  33. package/hooks/gate-3.sh +0 -0
  34. package/hooks/gate-4.sh +0 -0
  35. package/hooks/gate-7.sh +0 -0
  36. package/hooks/gate-8.sh +0 -0
  37. package/hooks/gate-9.sh +0 -0
  38. package/hooks/pre-commit +0 -0
  39. package/hooks/pre-push +0 -0
  40. package/lib/__tests__/phase-transition.test.js +272 -272
  41. package/lib/__tests__/sprint-audit.test.js +207 -207
  42. package/lib/doctor-tui.js +198 -198
  43. package/lib/language-tools.js +833 -833
  44. package/lib/phase-transition.js +249 -249
  45. package/lib/sprint-audit.js +235 -235
  46. package/mock-policy/AGENTS.md +2 -2
  47. package/mutation/AGENTS.md +2 -2
  48. package/package.json +1 -1
  49. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  50. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  51. package/plugins/claude-code/bin/xp-gate-check +0 -0
  52. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  53. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  54. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  55. package/plugins/opencode/package.json +1 -1
  56. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  57. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  58. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  59. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  60. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  61. package/plugins/qoder/agents/delphi-technical.md +48 -48
  62. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  63. package/plugins/qoder/bin/xp-gate-check +0 -0
  64. package/plugins/qoder/plugin.json +1 -1
  65. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  66. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  67. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  68. package/principles/AGENTS.md +2 -2
  69. package/scripts/delphi-external-review.cjs +432 -432
  70. package/skills/delphi-review/AGENTS.md +2 -2
  71. package/skills/sprint-flow/AGENTS.md +2 -2
  72. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -1,833 +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
- }
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
+ }