diff --git a/tito/commands/check.py b/tito/commands/check.py new file mode 100644 index 00000000..f8b969ec --- /dev/null +++ b/tito/commands/check.py @@ -0,0 +1,181 @@ +""" +Check command for TinyTorch CLI: comprehensive environment validation. + +Runs 60+ automated tests to validate the entire TinyTorch environment. +Perfect for students to share with TAs when something isn't working. +""" + +import sys +import subprocess +from argparse import ArgumentParser, Namespace +from pathlib import Path +from rich.panel import Panel +from rich.table import Table + +from .base import BaseCommand + +class CheckCommand(BaseCommand): + @property + def name(self) -> str: + return "check" + + @property + def description(self) -> str: + return "Run comprehensive environment validation (60+ tests)" + + def add_arguments(self, parser: ArgumentParser) -> None: + parser.add_argument( + '--verbose', + '-v', + action='store_true', + help='Show detailed test output' + ) + + def run(self, args: Namespace) -> int: + """Run comprehensive validation tests with rich output.""" + console = self.console + + console.print() + console.print(Panel( + "๐Ÿงช Running Comprehensive Environment Validation\n\n" + "This will test 60+ aspects of your TinyTorch environment.\n" + "Perfect for sharing with TAs if something isn't working!", + title="TinyTorch Environment Check", + border_style="bright_cyan" + )) + console.print() + + # Check if tests directory exists + tests_dir = Path("tests/environment") + if not tests_dir.exists(): + console.print(Panel( + "[red]โŒ Validation tests not found![/red]\n\n" + f"Expected location: {tests_dir.absolute()}\n\n" + "Please ensure you're running this from the TinyTorch root directory.", + title="Error", + border_style="red" + )) + return 1 + + # Run the validation tests with pytest + test_files = [ + "tests/environment/test_setup_validation.py", + "tests/environment/test_all_requirements.py" + ] + + console.print("[bold cyan]Running validation tests...[/bold cyan]") + console.print() + + # Build pytest command + pytest_args = [ + sys.executable, "-m", "pytest" + ] + test_files + [ + "-v" if args.verbose else "-q", + "--tb=short", + "--color=yes", + "-p", "no:warnings" # Suppress warnings for cleaner output + ] + + # Run pytest and capture output + result = subprocess.run( + pytest_args, + capture_output=True, + text=True + ) + + # Parse test results from output + output_lines = result.stdout.split('\n') + + # Count results + passed = failed = skipped = 0 + + for line in output_lines: + if 'passed' in line.lower(): + # Extract numbers from pytest summary + import re + match = re.search(r'(\d+) passed', line) + if match: + passed = int(match.group(1)) + match = re.search(r'(\d+) failed', line) + if match: + failed = int(match.group(1)) + match = re.search(r'(\d+) skipped', line) + if match: + skipped = int(match.group(1)) + + # Display results with rich formatting + console.print() + + # Summary table + results_table = Table(title="Test Results Summary", show_header=True, header_style="bold magenta") + results_table.add_column("Category", style="cyan", width=30) + results_table.add_column("Count", justify="right", width=10) + results_table.add_column("Status", width=20) + + if passed > 0: + results_table.add_row("Tests Passed", str(passed), "[green]โœ… OK[/green]") + if failed > 0: + results_table.add_row("Tests Failed", str(failed), "[red]โŒ Issues Found[/red]") + if skipped > 0: + results_table.add_row("Tests Skipped", str(skipped), "[yellow]โญ๏ธ Optional[/yellow]") + + console.print(results_table) + console.print() + + # Overall health status + if failed == 0: + status_panel = Panel( + "[bold green]โœ… Environment is HEALTHY![/bold green]\n\n" + f"All {passed} required checks passed.\n" + f"{skipped} optional checks skipped.\n\n" + "Your TinyTorch environment is ready to use! ๐ŸŽ‰\n\n" + "[dim]Next: [/dim][cyan]tito module 01[/cyan]", + title="Environment Status", + border_style="green" + ) + else: + status_panel = Panel( + f"[bold red]โŒ Found {failed} issue(s)[/bold red]\n\n" + f"{passed} checks passed, but some components need attention.\n\n" + "[bold]What to share with your TA:[/bold]\n" + "1. Copy the output above\n" + "2. Include the error messages below\n" + "3. Mention what you were trying to do\n\n" + "[dim]Or try:[/dim] [cyan]tito setup[/cyan] [dim]to reinstall[/dim]", + title="Environment Status", + border_style="red" + ) + + console.print(status_panel) + + # Show detailed output if verbose or if there are failures + if args.verbose or failed > 0: + console.print() + console.print(Panel("๐Ÿ“‹ Detailed Test Output", border_style="blue")) + console.print() + console.print(result.stdout) + + if result.stderr: + console.print() + console.print(Panel("โš ๏ธ Error Messages", border_style="yellow")) + console.print() + console.print(result.stderr) + + # Add helpful hints for common failures + if failed > 0: + console.print() + console.print(Panel( + "[bold]Common Solutions:[/bold]\n\n" + "โ€ข Missing packages: [cyan]pip install -r requirements.txt[/cyan]\n" + "โ€ข Jupyter issues: [cyan]pip install --upgrade jupyterlab[/cyan]\n" + "โ€ข Import errors: [cyan]pip install -e .[/cyan] [dim](reinstall TinyTorch)[/dim]\n" + "โ€ข Still stuck: Run [cyan]tito system check --verbose[/cyan]\n\n" + "[dim]Then share the full output with your TA[/dim]", + title="๐Ÿ’ก Quick Fixes", + border_style="yellow" + )) + + console.print() + + # Return appropriate exit code + return 0 if failed == 0 else 1 diff --git a/tito/commands/clean_workspace.py b/tito/commands/clean_workspace.py new file mode 100644 index 00000000..09d33b6c --- /dev/null +++ b/tito/commands/clean_workspace.py @@ -0,0 +1,232 @@ +""" +Clean command for TinyTorch CLI: clean up generated files and caches. +""" + +from argparse import ArgumentParser, Namespace +from pathlib import Path +import shutil +from rich.panel import Panel +from rich.table import Table +from rich.prompt import Confirm + +from .base import BaseCommand + +class CleanWorkspaceCommand(BaseCommand): + @property + def name(self) -> str: + return "clean" + + @property + def description(self) -> str: + return "Clean up generated files, caches, and temporary files" + + def add_arguments(self, parser: ArgumentParser) -> None: + parser.add_argument( + '--all', + action='store_true', + help='Clean everything including build artifacts' + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Show what would be deleted without actually deleting' + ) + parser.add_argument( + '-y', '--yes', + action='store_true', + help='Skip confirmation prompt' + ) + + def run(self, args: Namespace) -> int: + console = self.console + + console.print() + console.print(Panel( + "๐Ÿงน Cleaning TinyTorch Workspace", + title="Workspace Cleanup", + border_style="bright_yellow" + )) + console.print() + + # Define patterns to clean + patterns = { + '__pycache__': ('__pycache__/', 'Python bytecode cache'), + '.pytest_cache': ('.pytest_cache/', 'Pytest cache'), + '.ipynb_checkpoints': ('.ipynb_checkpoints/', 'Jupyter checkpoints'), + '*.pyc': ('*.pyc', 'Compiled Python files'), + '*.pyo': ('*.pyo', 'Optimized Python files'), + '*.pyd': ('*.pyd', 'Python extension modules'), + } + + if args.all: + # Additional patterns for --all + patterns.update({ + '.coverage': ('.coverage', 'Coverage data'), + 'htmlcov': ('htmlcov/', 'Coverage HTML report'), + '.tox': ('.tox/', 'Tox environments'), + 'dist': ('dist/', 'Distribution files'), + 'build': ('build/', 'Build files'), + '*.egg-info': ('*.egg-info/', 'Egg info directories'), + }) + + # Scan for files to delete + console.print("[bold cyan]๐Ÿ” Scanning for files to clean...[/bold cyan]") + console.print() + + files_to_delete = [] + total_size = 0 + + # Find __pycache__ directories + for pycache_dir in Path.cwd().rglob('__pycache__'): + for file in pycache_dir.iterdir(): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + files_to_delete.append(pycache_dir) + + # Find .pytest_cache directories + for cache_dir in Path.cwd().rglob('.pytest_cache'): + for file in cache_dir.rglob('*'): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + files_to_delete.append(cache_dir) + + # Find .ipynb_checkpoints directories + for checkpoint_dir in Path.cwd().rglob('.ipynb_checkpoints'): + for file in checkpoint_dir.rglob('*'): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + files_to_delete.append(checkpoint_dir) + + # Find .pyc, .pyo, .pyd files + for ext in ['*.pyc', '*.pyo', '*.pyd']: + for file in Path.cwd().rglob(ext): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + + if args.all: + # Additional cleanups for --all flag + for pattern in ['.coverage', 'htmlcov', '.tox', 'dist', 'build']: + target = Path.cwd() / pattern + if target.exists(): + if target.is_file(): + files_to_delete.append(target) + total_size += target.stat().st_size + elif target.is_dir(): + for file in target.rglob('*'): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + files_to_delete.append(target) + + # Find .egg-info directories + for egg_info in Path.cwd().rglob('*.egg-info'): + if egg_info.is_dir(): + for file in egg_info.rglob('*'): + if file.is_file(): + files_to_delete.append(file) + total_size += file.stat().st_size + files_to_delete.append(egg_info) + + if not files_to_delete: + console.print(Panel( + "[green]โœ… Workspace is already clean![/green]\n\n" + "No temporary files or caches found.", + title="Clean Workspace", + border_style="green" + )) + return 0 + + # Count file types + file_count = len([f for f in files_to_delete if f.is_file()]) + dir_count = len([f for f in files_to_delete if f.is_dir()]) + + # Summary table + summary_table = Table(title="Files Found", show_header=True, header_style="bold yellow") + summary_table.add_column("Type", style="cyan", width=30) + summary_table.add_column("Count", justify="right", width=15) + summary_table.add_column("Size", width=20) + + # Count by type + pycache_count = len([f for f in files_to_delete if '__pycache__' in str(f)]) + pytest_count = len([f for f in files_to_delete if '.pytest_cache' in str(f)]) + checkpoint_count = len([f for f in files_to_delete if '.ipynb_checkpoints' in str(f)]) + pyc_count = len([f for f in files_to_delete if str(f).endswith(('.pyc', '.pyo', '.pyd'))]) + + if pycache_count > 0: + summary_table.add_row("__pycache__/", str(pycache_count), "โ€”") + if pytest_count > 0: + summary_table.add_row(".pytest_cache/", str(pytest_count), "โ€”") + if checkpoint_count > 0: + summary_table.add_row(".ipynb_checkpoints/", str(checkpoint_count), "โ€”") + if pyc_count > 0: + summary_table.add_row("*.pyc/*.pyo/*.pyd", str(pyc_count), f"{total_size / 1024 / 1024:.2f} MB") + + if args.all: + other_count = file_count - pycache_count - pytest_count - checkpoint_count - pyc_count + if other_count > 0: + summary_table.add_row("Build artifacts", str(other_count), "โ€”") + + summary_table.add_row("[bold]Total[/bold]", f"[bold]{file_count} files, {dir_count} dirs[/bold]", f"[bold]{total_size / 1024 / 1024:.2f} MB[/bold]") + + console.print(summary_table) + console.print() + + # Dry run mode + if args.dry_run: + console.print(Panel( + "[yellow]๐Ÿ” DRY RUN MODE[/yellow]\n\n" + f"Would delete {file_count} files and {dir_count} directories ({total_size / 1024 / 1024:.2f} MB)\n\n" + "[dim]Remove --dry-run flag to actually delete these files[/dim]", + title="Dry Run", + border_style="yellow" + )) + return 0 + + # Confirm deletion + if not args.yes: + confirmed = Confirm.ask( + f"\n[yellow]โš ๏ธ Delete {file_count} files and {dir_count} directories ({total_size / 1024 / 1024:.2f} MB)?[/yellow]", + default=False + ) + if not confirmed: + console.print("\n[dim]Cleanup cancelled.[/dim]") + return 0 + + # Perform cleanup + console.print() + console.print("[bold cyan]๐Ÿ—‘๏ธ Cleaning workspace...[/bold cyan]") + + deleted_files = 0 + deleted_dirs = 0 + freed_space = 0 + + # Delete files first, then directories + for item in files_to_delete: + try: + if item.is_file(): + size = item.stat().st_size + item.unlink() + deleted_files += 1 + freed_space += size + elif item.is_dir() and not any(item.iterdir()): # Only delete if empty + shutil.rmtree(item, ignore_errors=True) + deleted_dirs += 1 + except Exception as e: + console.print(f"[dim red] โœ— Failed to delete {item}: {e}[/dim red]") + + # Success message + console.print() + console.print(Panel( + f"[bold green]โœ… Workspace Cleaned![/bold green]\n\n" + f"๐Ÿ—‘๏ธ Deleted: {deleted_files} files, {deleted_dirs} directories\n" + f"๐Ÿ’พ Freed: {freed_space / 1024 / 1024:.2f} MB\n" + f"โฑ๏ธ Time: < 1 second", + title="Cleanup Complete", + border_style="green" + )) + + return 0 diff --git a/tito/commands/report.py b/tito/commands/report.py new file mode 100644 index 00000000..0cf316f7 --- /dev/null +++ b/tito/commands/report.py @@ -0,0 +1,363 @@ +""" +Report command for TinyTorch CLI: generate comprehensive diagnostic report. + +This command generates a JSON report containing all environment information, +perfect for sharing with TAs, instructors, or when filing bug reports. +""" + +import sys +import os +import platform +import shutil +import json +from datetime import datetime +from pathlib import Path +from argparse import ArgumentParser, Namespace +from rich.panel import Panel +from rich.table import Table + +from .base import BaseCommand + + +class ReportCommand(BaseCommand): + @property + def name(self) -> str: + return "report" + + @property + def description(self) -> str: + return "Generate comprehensive diagnostic report (JSON)" + + def add_arguments(self, parser: ArgumentParser) -> None: + parser.add_argument( + '-o', '--output', + type=str, + default=None, + help='Output file path (default: tinytorch-report-TIMESTAMP.json)' + ) + parser.add_argument( + '--stdout', + action='store_true', + help='Print JSON to stdout instead of file' + ) + + def run(self, args: Namespace) -> int: + console = self.console + + console.print() + console.print(Panel( + "๐Ÿ“‹ Generating TinyTorch Diagnostic Report\n\n" + "[dim]This report contains all environment information\n" + "needed for debugging and support.[/dim]", + title="System Report", + border_style="bright_yellow" + )) + console.print() + + # Collect all diagnostic information + report = self._collect_report_data() + + # Determine output path + if args.stdout: + # Print to stdout + print(json.dumps(report, indent=2)) + return 0 + else: + # Write to file + if args.output: + output_path = Path(args.output) + else: + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + output_path = Path.cwd() / f"tinytorch-report-{timestamp}.json" + + try: + with open(output_path, 'w') as f: + json.dump(report, f, indent=2) + + console.print(Panel( + f"[bold green]โœ… Report Generated Successfully![/bold green]\n\n" + f"๐Ÿ“„ File: [cyan]{output_path}[/cyan]\n" + f"๐Ÿ“ฆ Size: {output_path.stat().st_size} bytes\n\n" + f"[dim]Share this file with your TA or instructor for support.[/dim]", + title="Report Complete", + border_style="green" + )) + + return 0 + except Exception as e: + console.print(Panel( + f"[red]โŒ Failed to write report:[/red]\n\n{str(e)}", + title="Error", + border_style="red" + )) + return 1 + + def _collect_report_data(self) -> dict: + """Collect comprehensive diagnostic information.""" + report = { + "report_metadata": { + "generated_at": datetime.now().isoformat(), + "report_version": "1.0", + "tinytorch_cli_version": self._get_tinytorch_version() + }, + "system": self._collect_system_info(), + "python": self._collect_python_info(), + "environment": self._collect_environment_info(), + "dependencies": self._collect_dependencies_info(), + "tinytorch": self._collect_tinytorch_info(), + "modules": self._collect_modules_info(), + "git": self._collect_git_info(), + "disk_memory": self._collect_disk_memory_info() + } + return report + + def _get_tinytorch_version(self) -> str: + """Get TinyTorch version.""" + try: + import tinytorch + return getattr(tinytorch, '__version__', 'unknown') + except ImportError: + return "not_installed" + + def _collect_system_info(self) -> dict: + """Collect system information.""" + return { + "os": platform.system(), + "os_release": platform.release(), + "os_version": platform.version(), + "machine": platform.machine(), + "processor": platform.processor(), + "platform": platform.platform(), + "node": platform.node() + } + + def _collect_python_info(self) -> dict: + """Collect Python interpreter information.""" + return { + "version": sys.version, + "version_info": { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial + }, + "implementation": platform.python_implementation(), + "compiler": platform.python_compiler(), + "executable": sys.executable, + "prefix": sys.prefix, + "base_prefix": getattr(sys, 'base_prefix', sys.prefix), + "path": sys.path + } + + def _collect_environment_info(self) -> dict: + """Collect environment variables and paths.""" + venv_exists = self.venv_path.exists() + in_venv = ( + os.environ.get('VIRTUAL_ENV') is not None or + (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or + hasattr(sys, 'real_prefix') + ) + + return { + "working_directory": str(Path.cwd()), + "virtual_environment": { + "exists": venv_exists, + "active": in_venv, + "path": str(self.venv_path) if venv_exists else None, + "VIRTUAL_ENV": os.environ.get('VIRTUAL_ENV'), + "CONDA_DEFAULT_ENV": os.environ.get('CONDA_DEFAULT_ENV'), + "CONDA_PREFIX": os.environ.get('CONDA_PREFIX') + }, + "PATH": os.environ.get('PATH', '').split(os.pathsep), + "PYTHONPATH": os.environ.get('PYTHONPATH', '').split(os.pathsep) if os.environ.get('PYTHONPATH') else [] + } + + def _collect_dependencies_info(self) -> dict: + """Collect installed package information.""" + dependencies = {} + + # Core dependencies + packages = [ + ('numpy', 'numpy'), + ('pytest', 'pytest'), + ('PyYAML', 'yaml'), + ('rich', 'rich'), + ('jupyterlab', 'jupyterlab'), + ('jupytext', 'jupytext'), + ('nbformat', 'nbformat'), + ('nbgrader', 'nbgrader'), + ('nbconvert', 'nbconvert'), + ('jupyter', 'jupyter'), + ('matplotlib', 'matplotlib'), + ('psutil', 'psutil'), + ('black', 'black'), + ('isort', 'isort'), + ('flake8', 'flake8') + ] + + for display_name, import_name in packages: + try: + module = __import__(import_name) + version = getattr(module, '__version__', 'unknown') + location = getattr(module, '__file__', 'unknown') + dependencies[display_name] = { + "installed": True, + "version": version, + "location": location + } + except ImportError: + dependencies[display_name] = { + "installed": False, + "version": None, + "location": None + } + + return dependencies + + def _collect_tinytorch_info(self) -> dict: + """Collect TinyTorch package information.""" + try: + import tinytorch + version = getattr(tinytorch, '__version__', 'unknown') + location = Path(tinytorch.__file__).parent + + # Check if in development mode + is_dev = (location / '../setup.py').exists() or (location / '../pyproject.toml').exists() + + return { + "installed": True, + "version": version, + "location": str(location), + "development_mode": is_dev, + "package_structure": { + "has_init": (location / '__init__.py').exists(), + "has_core": (location / 'core').exists(), + "has_ops": (location / 'ops').exists() + } + } + except ImportError: + return { + "installed": False, + "version": None, + "location": None, + "development_mode": False, + "package_structure": {} + } + + def _collect_modules_info(self) -> dict: + """Collect TinyTorch modules information.""" + modules_dir = Path.cwd() / "modules" + + if not modules_dir.exists(): + return {"exists": False, "modules": []} + + modules = [] + for module_path in sorted(modules_dir.iterdir()): + if module_path.is_dir() and module_path.name.startswith(('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')): + module_info = { + "name": module_path.name, + "path": str(module_path), + "has_notebook": any(module_path.glob("*.ipynb")), + "has_dev_py": any(module_path.glob("*_dev.py")), + "has_tests": (module_path / "tests").exists() + } + modules.append(module_info) + + return { + "exists": True, + "count": len(modules), + "modules": modules + } + + def _collect_git_info(self) -> dict: + """Collect git repository information.""" + git_dir = Path.cwd() / ".git" + + if not git_dir.exists(): + return {"is_repo": False} + + try: + import subprocess + + # Get current branch + branch = subprocess.check_output( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + stderr=subprocess.DEVNULL, + text=True + ).strip() + + # Get current commit + commit = subprocess.check_output( + ['git', 'rev-parse', '--short', 'HEAD'], + stderr=subprocess.DEVNULL, + text=True + ).strip() + + # Get remote URL + try: + remote = subprocess.check_output( + ['git', 'remote', 'get-url', 'origin'], + stderr=subprocess.DEVNULL, + text=True + ).strip() + except: + remote = None + + # Check for uncommitted changes + status = subprocess.check_output( + ['git', 'status', '--porcelain'], + stderr=subprocess.DEVNULL, + text=True + ).strip() + has_changes = len(status) > 0 + + return { + "is_repo": True, + "branch": branch, + "commit": commit, + "remote": remote, + "has_uncommitted_changes": has_changes, + "status": status if has_changes else None + } + except: + return {"is_repo": True, "error": "Failed to get git info"} + + def _collect_disk_memory_info(self) -> dict: + """Collect disk space and memory information.""" + info = {} + + # Disk space + try: + disk_usage = shutil.disk_usage(Path.cwd()) + info["disk"] = { + "total_bytes": disk_usage.total, + "used_bytes": disk_usage.used, + "free_bytes": disk_usage.free, + "total_gb": round(disk_usage.total / (1024**3), 2), + "used_gb": round(disk_usage.used / (1024**3), 2), + "free_gb": round(disk_usage.free / (1024**3), 2), + "percent_used": round((disk_usage.used / disk_usage.total) * 100, 1) + } + except Exception as e: + info["disk"] = {"error": str(e)} + + # Memory + try: + import psutil + mem = psutil.virtual_memory() + info["memory"] = { + "total_bytes": mem.total, + "available_bytes": mem.available, + "used_bytes": mem.used, + "total_gb": round(mem.total / (1024**3), 2), + "available_gb": round(mem.available / (1024**3), 2), + "used_gb": round(mem.used / (1024**3), 2), + "percent_used": mem.percent + } + except ImportError: + info["memory"] = {"error": "psutil not installed"} + except Exception as e: + info["memory"] = {"error": str(e)} + + return info diff --git a/tito/commands/version.py b/tito/commands/version.py new file mode 100644 index 00000000..8d40b810 --- /dev/null +++ b/tito/commands/version.py @@ -0,0 +1,127 @@ +""" +Version command for TinyTorch CLI: show version information for TinyTorch and dependencies. +""" + +from argparse import ArgumentParser, Namespace +import sys +import os +from pathlib import Path +from datetime import datetime +from rich.panel import Panel +from rich.table import Table + +from .base import BaseCommand + +class VersionCommand(BaseCommand): + @property + def name(self) -> str: + return "version" + + @property + def description(self) -> str: + return "Show version information for TinyTorch and dependencies" + + def add_arguments(self, parser: ArgumentParser) -> None: + # No arguments needed + pass + + def run(self, args: Namespace) -> int: + console = self.console + + console.print() + console.print(Panel( + "๐Ÿ“ฆ TinyTorch Version Information", + title="Version Info", + border_style="bright_magenta" + )) + console.print() + + # Main Version Table + version_table = Table(title="TinyTorch", show_header=True, header_style="bold cyan") + version_table.add_column("Component", style="yellow", width=25) + version_table.add_column("Version", style="white", width=50) + + # TinyTorch Version + try: + import tinytorch + tinytorch_version = getattr(tinytorch, '__version__', '0.1.0-dev') + tinytorch_path = Path(tinytorch.__file__).parent + + version_table.add_row("TinyTorch", f"v{tinytorch_version}") + version_table.add_row(" โ””โ”€ Installation", "Development Mode") + version_table.add_row(" โ””โ”€ Location", str(tinytorch_path)) + + # Check if it's a git repo + git_dir = Path.cwd() / ".git" + if git_dir.exists(): + try: + import subprocess + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + capture_output=True, + text=True, + check=True + ) + commit_hash = result.stdout.strip() + version_table.add_row(" โ””โ”€ Git Commit", commit_hash) + except Exception: + pass + + except ImportError: + version_table.add_row("TinyTorch", "[red]Not Installed[/red]") + + # Python Version + python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + version_table.add_row("Python", python_version) + + console.print(version_table) + console.print() + + # Dependencies Version Table + deps_table = Table(title="Core Dependencies", show_header=True, header_style="bold magenta") + deps_table.add_column("Package", style="cyan", width=20) + deps_table.add_column("Version", style="white", width=20) + deps_table.add_column("Status", width=30) + + dependencies = [ + ('numpy', 'NumPy'), + ('pytest', 'pytest'), + ('yaml', 'PyYAML'), + ('rich', 'Rich'), + ('jupyterlab', 'JupyterLab'), + ('jupytext', 'Jupytext'), + ('nbformat', 'nbformat'), + ] + + for import_name, display_name in dependencies: + try: + module = __import__(import_name) + version = getattr(module, '__version__', 'unknown') + deps_table.add_row(display_name, version, "[green]โœ… Installed[/green]") + except ImportError: + deps_table.add_row(display_name, "โ€”", "[red]โŒ Not Installed[/red]") + + console.print(deps_table) + console.print() + + # System Info (brief) + system_table = Table(title="System", show_header=True, header_style="bold blue") + system_table.add_column("Component", style="yellow", width=20) + system_table.add_column("Value", style="white", width=50) + + import platform + system_table.add_row("OS", f"{platform.system()} {platform.release()}") + system_table.add_row("Architecture", platform.machine()) + system_table.add_row("Python Implementation", platform.python_implementation()) + + console.print(system_table) + console.print() + + # Helpful info panel + console.print(Panel( + "[dim]๐Ÿ’ก For complete system information, run:[/dim] [cyan]tito system info[/cyan]\n" + "[dim]๐Ÿ’ก To check environment health, run:[/dim] [cyan]tito system health[/cyan]", + border_style="blue" + )) + + return 0