qyro 2.0.0__py3-none-any.whl

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 (45) hide show
  1. qyro/__init__.py +17 -0
  2. qyro/adapters/__init__.py +4 -0
  3. qyro/adapters/language_adapters/__init__.py +4 -0
  4. qyro/adapters/language_adapters/c/__init__.py +4 -0
  5. qyro/adapters/language_adapters/python/__init__.py +4 -0
  6. qyro/adapters/language_adapters/python/python_adapter.py +584 -0
  7. qyro/cli/__init__.py +8 -0
  8. qyro/cli/__main__.py +5 -0
  9. qyro/cli/cli.py +392 -0
  10. qyro/cli/interactive.py +297 -0
  11. qyro/common/__init__.py +37 -0
  12. qyro/common/animation.py +82 -0
  13. qyro/common/builder.py +434 -0
  14. qyro/common/compiler.py +895 -0
  15. qyro/common/config.py +93 -0
  16. qyro/common/constants.py +99 -0
  17. qyro/common/errors.py +176 -0
  18. qyro/common/frontend.py +74 -0
  19. qyro/common/health.py +358 -0
  20. qyro/common/kafka_manager.py +192 -0
  21. qyro/common/logging.py +149 -0
  22. qyro/common/memory.py +147 -0
  23. qyro/common/metrics.py +301 -0
  24. qyro/common/monitoring.py +468 -0
  25. qyro/common/parser.py +91 -0
  26. qyro/common/platform.py +609 -0
  27. qyro/common/redis_memory.py +1108 -0
  28. qyro/common/rpc.py +287 -0
  29. qyro/common/sandbox.py +191 -0
  30. qyro/common/schema_loader.py +33 -0
  31. qyro/common/secure_sandbox.py +490 -0
  32. qyro/common/toolchain_validator.py +617 -0
  33. qyro/common/type_generator.py +176 -0
  34. qyro/common/validation.py +401 -0
  35. qyro/common/validator.py +204 -0
  36. qyro/gateway/__init__.py +8 -0
  37. qyro/gateway/gateway.py +303 -0
  38. qyro/orchestrator/__init__.py +8 -0
  39. qyro/orchestrator/orchestrator.py +1223 -0
  40. qyro-2.0.0.dist-info/METADATA +244 -0
  41. qyro-2.0.0.dist-info/RECORD +45 -0
  42. qyro-2.0.0.dist-info/WHEEL +5 -0
  43. qyro-2.0.0.dist-info/entry_points.txt +2 -0
  44. qyro-2.0.0.dist-info/licenses/LICENSE +21 -0
  45. qyro-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,617 @@
1
+ """
2
+ Nexus Toolchain Validator
3
+ Provides pre-flight validation for all supported language toolchains.
4
+ Checks availability and minimum versions with platform-specific installation instructions.
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+ import re
10
+ from typing import Dict, List, Tuple, Optional
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+
14
+ from .logging import get_logger
15
+
16
+ logger = get_logger("nexus.toolchain_validator")
17
+
18
+
19
+ class ToolchainStatus(Enum):
20
+ """Status of a toolchain check."""
21
+ AVAILABLE = "available"
22
+ MISSING = "missing"
23
+ VERSION_TOO_OLD = "version_too_old"
24
+ ERROR = "error"
25
+
26
+
27
+ @dataclass
28
+ class ToolchainCheck:
29
+ """Result of a toolchain check."""
30
+ name: str
31
+ status: ToolchainStatus
32
+ version: Optional[str] = None
33
+ error_message: Optional[str] = None
34
+ install_instructions: Optional[str] = None
35
+
36
+
37
+ class ToolchainValidator:
38
+ """
39
+ Validates toolchain availability and versions for all supported languages.
40
+
41
+ Supported Languages:
42
+ - C (gcc/clang)
43
+ - Java (javac)
44
+ - Rust (cargo)
45
+ - Go (go)
46
+ - Node.js (node/npm)
47
+ - Python (python)
48
+ """
49
+
50
+ # Minimum version requirements
51
+ MIN_VERSIONS = {
52
+ 'gcc': '9.0',
53
+ 'clang': '11.0',
54
+ 'javac': '11',
55
+ 'cargo': '1.70',
56
+ 'go': '1.20',
57
+ 'node': '18.0.0',
58
+ 'python': '3.8',
59
+ }
60
+
61
+ # Recommended versions
62
+ RECOMMENDED_VERSIONS = {
63
+ 'gcc': '11.0',
64
+ 'clang': '15.0',
65
+ 'javac': '17',
66
+ 'cargo': '1.75',
67
+ 'go': '1.21',
68
+ 'node': '20.0.0',
69
+ 'python': '3.11',
70
+ }
71
+
72
+ # Platform-specific installation commands
73
+ INSTALL_COMMANDS = {
74
+ 'linux': {
75
+ 'gcc': 'sudo apt install gcc || sudo yum install gcc',
76
+ 'clang': 'sudo apt install clang || sudo yum install clang',
77
+ 'javac': 'sudo apt install openjdk-17-jdk || sudo yum install java-17-openjdk-devel',
78
+ 'cargo': 'curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh',
79
+ 'go': 'wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz && sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz',
80
+ 'node': 'curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install nodejs',
81
+ 'python': 'sudo apt install python3 python3-pip || sudo yum install python3 python3-pip',
82
+ },
83
+ 'darwin': { # macOS
84
+ 'gcc': 'xcode-select --install',
85
+ 'clang': 'xcode-select --install',
86
+ 'javac': 'brew install openjdk@17',
87
+ 'cargo': 'curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh',
88
+ 'go': 'brew install go',
89
+ 'node': 'brew install node',
90
+ 'python': 'brew install python',
91
+ },
92
+ 'win32': { # Windows
93
+ 'gcc': 'Download and install MinGW-w64 from https://www.mingw-w64.org/ or use: choco install mingw',
94
+ 'clang': 'Download and install LLVM from https://llvm.org/ or use: choco install llvm',
95
+ 'javac': 'Download and install JDK 17 from https://adoptium.net/ or use: choco install temurin17jdk',
96
+ 'cargo': 'Download and install from https://rustup.rs/ or use: choco install rust',
97
+ 'go': 'Download and install from https://go.dev/dl/ or use: choco install golang',
98
+ 'node': 'Download and install from https://nodejs.org/ or use: choco install nodejs',
99
+ 'python': 'Download and install from https://python.org/ or use: choco install python',
100
+ },
101
+ }
102
+
103
+ def __init__(self):
104
+ self.platform = self._detect_platform()
105
+ self.checks: Dict[str, ToolchainCheck] = {}
106
+
107
+ def _detect_platform(self) -> str:
108
+ """Detect the current platform."""
109
+ if sys.platform == 'win32':
110
+ return 'win32'
111
+ elif sys.platform == 'darwin':
112
+ return 'darwin'
113
+ else:
114
+ return 'linux'
115
+
116
+ def _get_install_command(self, tool: str) -> str:
117
+ """Get platform-specific installation command for a tool."""
118
+ return self.INSTALL_COMMANDS.get(self.platform, {}).get(
119
+ tool,
120
+ f"Please install {tool} manually"
121
+ )
122
+
123
+ def _run_command(self, cmd: List[str], timeout: int = 5) -> Tuple[int, str, str]:
124
+ """
125
+ Run a command and return (returncode, stdout, stderr).
126
+
127
+ Args:
128
+ cmd: Command to run as a list
129
+ timeout: Timeout in seconds
130
+
131
+ Returns:
132
+ Tuple of (returncode, stdout, stderr)
133
+ """
134
+ try:
135
+ result = subprocess.run(
136
+ cmd,
137
+ capture_output=True,
138
+ timeout=timeout,
139
+ text=True,
140
+ encoding='utf-8',
141
+ errors='replace'
142
+ )
143
+ return result.returncode, result.stdout, result.stderr
144
+ except FileNotFoundError:
145
+ return 1, "", "Command not found"
146
+ except subprocess.TimeoutExpired:
147
+ return 1, "", "Command timed out"
148
+ except Exception as e:
149
+ return 1, "", str(e)
150
+
151
+ def _parse_version(self, version_string: str, tool: str) -> Optional[str]:
152
+ """
153
+ Parse version string from tool output.
154
+
155
+ Args:
156
+ version_string: Raw version output from tool
157
+ tool: Name of the tool
158
+
159
+ Returns:
160
+ Parsed version string or None
161
+ """
162
+ version_string = version_string.strip()
163
+
164
+ # Tool-specific version parsing patterns
165
+ patterns = {
166
+ 'gcc': r'gcc\s+\(.*?\)\s+([\d.]+)',
167
+ 'clang': r'clang\s+version\s+([\d.]+)',
168
+ 'javac': r'javac\s+([\d.]+)',
169
+ 'cargo': r'cargo\s+([\d.]+)',
170
+ 'go': r'go\s+version\s+go([\d.]+)',
171
+ 'node': r'v?([\d.]+)',
172
+ 'python': r'Python\s+([\d.]+)',
173
+ }
174
+
175
+ pattern = patterns.get(tool)
176
+ if pattern:
177
+ match = re.search(pattern, version_string)
178
+ if match:
179
+ return match.group(1)
180
+
181
+ # Fallback: try to extract first version-like string
182
+ version_match = re.search(r'(\d+(?:\.\d+)*)', version_string)
183
+ if version_match:
184
+ return version_match.group(1)
185
+
186
+ return None
187
+
188
+ def _compare_versions(self, version1: str, version2: str) -> int:
189
+ """
190
+ Compare two version strings.
191
+
192
+ Args:
193
+ version1: First version string
194
+ version2: Second version string
195
+
196
+ Returns:
197
+ -1 if version1 < version2, 0 if equal, 1 if version1 > version2
198
+ """
199
+ v1_parts = [int(x) for x in version1.split('.')]
200
+ v2_parts = [int(x) for x in version2.split('.')]
201
+
202
+ # Pad shorter version with zeros
203
+ max_len = max(len(v1_parts), len(v2_parts))
204
+ v1_parts.extend([0] * (max_len - len(v1_parts)))
205
+ v2_parts.extend([0] * (max_len - len(v2_parts)))
206
+
207
+ for v1, v2 in zip(v1_parts, v2_parts):
208
+ if v1 < v2:
209
+ return -1
210
+ elif v1 > v2:
211
+ return 1
212
+
213
+ return 0
214
+
215
+ def check_gcc(self) -> ToolchainCheck:
216
+ """Check for GCC compiler."""
217
+ returncode, stdout, stderr = self._run_command(['gcc', '--version'])
218
+
219
+ if returncode != 0:
220
+ return ToolchainCheck(
221
+ name='gcc',
222
+ status=ToolchainStatus.MISSING,
223
+ install_instructions=self._get_install_command('gcc')
224
+ )
225
+
226
+ version = self._parse_version(stdout, 'gcc')
227
+ if not version:
228
+ return ToolchainCheck(
229
+ name='gcc',
230
+ status=ToolchainStatus.ERROR,
231
+ error_message='Could not parse version',
232
+ install_instructions=self._get_install_command('gcc')
233
+ )
234
+
235
+ min_version = self.MIN_VERSIONS.get('gcc', '9.0')
236
+ if self._compare_versions(version, min_version) < 0:
237
+ return ToolchainCheck(
238
+ name='gcc',
239
+ status=ToolchainStatus.VERSION_TOO_OLD,
240
+ version=version,
241
+ error_message=f'GCC version {version} is too old (minimum: {min_version})',
242
+ install_instructions=self._get_install_command('gcc')
243
+ )
244
+
245
+ return ToolchainCheck(
246
+ name='gcc',
247
+ status=ToolchainStatus.AVAILABLE,
248
+ version=version
249
+ )
250
+
251
+ def check_clang(self) -> ToolchainCheck:
252
+ """Check for Clang compiler."""
253
+ returncode, stdout, stderr = self._run_command(['clang', '--version'])
254
+
255
+ if returncode != 0:
256
+ return ToolchainCheck(
257
+ name='clang',
258
+ status=ToolchainStatus.MISSING,
259
+ install_instructions=self._get_install_command('clang')
260
+ )
261
+
262
+ version = self._parse_version(stdout, 'clang')
263
+ if not version:
264
+ return ToolchainCheck(
265
+ name='clang',
266
+ status=ToolchainStatus.ERROR,
267
+ error_message='Could not parse version',
268
+ install_instructions=self._get_install_command('clang')
269
+ )
270
+
271
+ min_version = self.MIN_VERSIONS.get('clang', '11.0')
272
+ if self._compare_versions(version, min_version) < 0:
273
+ return ToolchainCheck(
274
+ name='clang',
275
+ status=ToolchainStatus.VERSION_TOO_OLD,
276
+ version=version,
277
+ error_message=f'Clang version {version} is too old (minimum: {min_version})',
278
+ install_instructions=self._get_install_command('clang')
279
+ )
280
+
281
+ return ToolchainCheck(
282
+ name='clang',
283
+ status=ToolchainStatus.AVAILABLE,
284
+ version=version
285
+ )
286
+
287
+ def check_javac(self) -> ToolchainCheck:
288
+ """Check for Java compiler."""
289
+ returncode, stdout, stderr = self._run_command(['javac', '-version'])
290
+
291
+ if returncode != 0:
292
+ return ToolchainCheck(
293
+ name='javac',
294
+ status=ToolchainStatus.MISSING,
295
+ install_instructions=self._get_install_command('javac')
296
+ )
297
+
298
+ # javac -version may output to stdout or stderr depending on platform
299
+ version_output = stdout if stdout else stderr
300
+ version = self._parse_version(version_output, 'javac')
301
+ if not version:
302
+ return ToolchainCheck(
303
+ name='javac',
304
+ status=ToolchainStatus.ERROR,
305
+ error_message='Could not parse version',
306
+ install_instructions=self._get_install_command('javac')
307
+ )
308
+
309
+ min_version = self.MIN_VERSIONS.get('javac', '11')
310
+ if self._compare_versions(version, min_version) < 0:
311
+ return ToolchainCheck(
312
+ name='javac',
313
+ status=ToolchainStatus.VERSION_TOO_OLD,
314
+ version=version,
315
+ error_message=f'Java version {version} is too old (minimum: {min_version})',
316
+ install_instructions=self._get_install_command('javac')
317
+ )
318
+
319
+ return ToolchainCheck(
320
+ name='javac',
321
+ status=ToolchainStatus.AVAILABLE,
322
+ version=version
323
+ )
324
+
325
+ def check_cargo(self) -> ToolchainCheck:
326
+ """Check for Cargo (Rust)."""
327
+ returncode, stdout, stderr = self._run_command(['cargo', '--version'])
328
+
329
+ if returncode != 0:
330
+ return ToolchainCheck(
331
+ name='cargo',
332
+ status=ToolchainStatus.MISSING,
333
+ install_instructions=self._get_install_command('cargo')
334
+ )
335
+
336
+ version = self._parse_version(stdout, 'cargo')
337
+ if not version:
338
+ return ToolchainCheck(
339
+ name='cargo',
340
+ status=ToolchainStatus.ERROR,
341
+ error_message='Could not parse version',
342
+ install_instructions=self._get_install_command('cargo')
343
+ )
344
+
345
+ min_version = self.MIN_VERSIONS.get('cargo', '1.70')
346
+ if self._compare_versions(version, min_version) < 0:
347
+ return ToolchainCheck(
348
+ name='cargo',
349
+ status=ToolchainStatus.VERSION_TOO_OLD,
350
+ version=version,
351
+ error_message=f'Cargo version {version} is too old (minimum: {min_version})',
352
+ install_instructions=self._get_install_command('cargo')
353
+ )
354
+
355
+ return ToolchainCheck(
356
+ name='cargo',
357
+ status=ToolchainStatus.AVAILABLE,
358
+ version=version
359
+ )
360
+
361
+ def check_go(self) -> ToolchainCheck:
362
+ """Check for Go compiler."""
363
+ returncode, stdout, stderr = self._run_command(['go', 'version'])
364
+
365
+ if returncode != 0:
366
+ return ToolchainCheck(
367
+ name='go',
368
+ status=ToolchainStatus.MISSING,
369
+ install_instructions=self._get_install_command('go')
370
+ )
371
+
372
+ version = self._parse_version(stdout, 'go')
373
+ if not version:
374
+ return ToolchainCheck(
375
+ name='go',
376
+ status=ToolchainStatus.ERROR,
377
+ error_message='Could not parse version',
378
+ install_instructions=self._get_install_command('go')
379
+ )
380
+
381
+ min_version = self.MIN_VERSIONS.get('go', '1.20')
382
+ if self._compare_versions(version, min_version) < 0:
383
+ return ToolchainCheck(
384
+ name='go',
385
+ status=ToolchainStatus.VERSION_TOO_OLD,
386
+ version=version,
387
+ error_message=f'Go version {version} is too old (minimum: {min_version})',
388
+ install_instructions=self._get_install_command('go')
389
+ )
390
+
391
+ return ToolchainCheck(
392
+ name='go',
393
+ status=ToolchainStatus.AVAILABLE,
394
+ version=version
395
+ )
396
+
397
+ def check_node(self) -> ToolchainCheck:
398
+ """Check for Node.js."""
399
+ returncode, stdout, stderr = self._run_command(['node', '--version'])
400
+
401
+ if returncode != 0:
402
+ return ToolchainCheck(
403
+ name='node',
404
+ status=ToolchainStatus.MISSING,
405
+ install_instructions=self._get_install_command('node')
406
+ )
407
+
408
+ version = self._parse_version(stdout, 'node')
409
+ if not version:
410
+ return ToolchainCheck(
411
+ name='node',
412
+ status=ToolchainStatus.ERROR,
413
+ error_message='Could not parse version',
414
+ install_instructions=self._get_install_command('node')
415
+ )
416
+
417
+ min_version = self.MIN_VERSIONS.get('node', '18.0.0')
418
+ if self._compare_versions(version, min_version) < 0:
419
+ return ToolchainCheck(
420
+ name='node',
421
+ status=ToolchainStatus.VERSION_TOO_OLD,
422
+ version=version,
423
+ error_message=f'Node.js version {version} is too old (minimum: {min_version})',
424
+ install_instructions=self._get_install_command('node')
425
+ )
426
+
427
+ return ToolchainCheck(
428
+ name='node',
429
+ status=ToolchainStatus.AVAILABLE,
430
+ version=version
431
+ )
432
+
433
+ def check_python(self) -> ToolchainCheck:
434
+ """Check for Python."""
435
+ returncode, stdout, stderr = self._run_command([sys.executable, '--version'])
436
+
437
+ if returncode != 0:
438
+ return ToolchainCheck(
439
+ name='python',
440
+ status=ToolchainStatus.MISSING,
441
+ install_instructions=self._get_install_command('python')
442
+ )
443
+
444
+ version = self._parse_version(stdout, 'python')
445
+ if not version:
446
+ return ToolchainCheck(
447
+ name='python',
448
+ status=ToolchainStatus.ERROR,
449
+ error_message='Could not parse version',
450
+ install_instructions=self._get_install_command('python')
451
+ )
452
+
453
+ min_version = self.MIN_VERSIONS.get('python', '3.8')
454
+ if self._compare_versions(version, min_version) < 0:
455
+ return ToolchainCheck(
456
+ name='python',
457
+ status=ToolchainStatus.VERSION_TOO_OLD,
458
+ version=version,
459
+ error_message=f'Python version {version} is too old (minimum: {min_version})',
460
+ install_instructions=self._get_install_command('python')
461
+ )
462
+
463
+ return ToolchainCheck(
464
+ name='python',
465
+ status=ToolchainStatus.AVAILABLE,
466
+ version=version
467
+ )
468
+
469
+ def check_all(self) -> Dict[str, ToolchainCheck]:
470
+ """
471
+ Check all toolchains.
472
+
473
+ Returns:
474
+ Dictionary mapping toolchain names to check results
475
+ """
476
+ self.checks = {
477
+ 'gcc': self.check_gcc(),
478
+ 'clang': self.check_clang(),
479
+ 'javac': self.check_javac(),
480
+ 'cargo': self.check_cargo(),
481
+ 'go': self.check_go(),
482
+ 'node': self.check_node(),
483
+ 'python': self.check_python(),
484
+ }
485
+
486
+ return self.checks
487
+
488
+ def get_available_languages(self) -> List[str]:
489
+ """
490
+ Get list of languages with available toolchains.
491
+
492
+ Returns:
493
+ List of language names that have available toolchains
494
+ """
495
+ available = []
496
+
497
+ # Check C compilers
498
+ if self.checks.get('gcc', ToolchainCheck('gcc', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
499
+ available.append('c')
500
+ if self.checks.get('clang', ToolchainCheck('clang', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
501
+ available.append('c')
502
+
503
+ # Check other languages
504
+ if self.checks.get('javac', ToolchainCheck('javac', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
505
+ available.append('java')
506
+ if self.checks.get('cargo', ToolchainCheck('cargo', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
507
+ available.append('rust')
508
+ if self.checks.get('go', ToolchainCheck('go', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
509
+ available.append('go')
510
+ if self.checks.get('node', ToolchainCheck('node', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
511
+ available.append('typescript')
512
+ if self.checks.get('python', ToolchainCheck('python', ToolchainStatus.MISSING)).status == ToolchainStatus.AVAILABLE:
513
+ available.append('python')
514
+
515
+ return list(set(available)) # Remove duplicates
516
+
517
+ def is_language_available(self, language: str) -> bool:
518
+ """
519
+ Check if a specific language toolchain is available.
520
+
521
+ Args:
522
+ language: Language name (c, java, rust, go, typescript, python)
523
+
524
+ Returns:
525
+ True if toolchain is available, False otherwise
526
+ """
527
+ language_map = {
528
+ 'c': ['gcc', 'clang'],
529
+ 'java': ['javac'],
530
+ 'rust': ['cargo'],
531
+ 'go': ['go'],
532
+ 'typescript': ['node'],
533
+ 'ts': ['node'],
534
+ 'python': ['python'],
535
+ 'py': ['python'],
536
+ }
537
+
538
+ tools = language_map.get(language.lower(), [])
539
+ for tool in tools:
540
+ check = self.checks.get(tool)
541
+ if check and check.status == ToolchainStatus.AVAILABLE:
542
+ return True
543
+
544
+ return False
545
+
546
+ def print_report(self):
547
+ """Print a formatted report of all toolchain checks."""
548
+ print("\n" + "=" * 70)
549
+ print("NEXUS TOOLCHAIN VALIDATION REPORT")
550
+ print("=" * 70)
551
+ print(f"Platform: {self.platform}")
552
+ print("=" * 70)
553
+
554
+ available_count = 0
555
+ missing_count = 0
556
+ warning_count = 0
557
+
558
+ for tool, check in self.checks.items():
559
+ status_icon = {
560
+ ToolchainStatus.AVAILABLE: "✓",
561
+ ToolchainStatus.MISSING: "✗",
562
+ ToolchainStatus.VERSION_TOO_OLD: "⚠",
563
+ ToolchainStatus.ERROR: "!",
564
+ }.get(check.status, "?")
565
+
566
+ status_color = {
567
+ ToolchainStatus.AVAILABLE: "green",
568
+ ToolchainStatus.MISSING: "red",
569
+ ToolchainStatus.VERSION_TOO_OLD: "yellow",
570
+ ToolchainStatus.ERROR: "red",
571
+ }.get(check.status, "white")
572
+
573
+ version_str = f" (v{check.version})" if check.version else ""
574
+
575
+ if check.status == ToolchainStatus.AVAILABLE:
576
+ available_count += 1
577
+ elif check.status == ToolchainStatus.MISSING:
578
+ missing_count += 1
579
+ else:
580
+ warning_count += 1
581
+
582
+ print(f"{status_icon} {tool.upper():10} {check.status.value:20} {version_str}")
583
+
584
+ if check.error_message:
585
+ print(f" └─ {check.error_message}")
586
+
587
+ if check.install_instructions:
588
+ print(f" └─ Install: {check.install_instructions}")
589
+
590
+ print("=" * 70)
591
+ print(f"Summary: {available_count} available, {missing_count} missing, {warning_count} warnings")
592
+ print("=" * 70)
593
+
594
+ if missing_count > 0 or warning_count > 0:
595
+ print("\n⚠ Some toolchains are missing or outdated.")
596
+ print("You can still use Nexus with available languages.")
597
+ print("Install missing toolchains for full functionality.\n")
598
+ else:
599
+ print("\n✓ All toolchains are available and up to date!\n")
600
+
601
+
602
+ def validate_toolchains() -> ToolchainValidator:
603
+ """
604
+ Convenience function to validate all toolchains.
605
+
606
+ Returns:
607
+ ToolchainValidator instance with check results
608
+ """
609
+ validator = ToolchainValidator()
610
+ validator.check_all()
611
+ return validator
612
+
613
+
614
+ if __name__ == "__main__":
615
+ # Run validation when executed directly
616
+ validator = validate_toolchains()
617
+ validator.print_report()