diff --git a/tinytorch/site/_static/demos/scripts/demo.sh b/tinytorch/site/_static/demos/scripts/demo.sh index b9151d422..2e0e5e5ff 100755 --- a/tinytorch/site/_static/demos/scripts/demo.sh +++ b/tinytorch/site/_static/demos/scripts/demo.sh @@ -109,7 +109,7 @@ EOF source .venv/bin/activate 2>/dev/null || source activate.sh MODULE_STATUS_TIME=$(time_command "tito_module_status" "tito module status") - LOGO_TIME=$(time_command "tito_logo" "tito logo") + LOGO_TIME=$(time_command "tito_logo" "tito system logo") # Add system info if command -v jq &> /dev/null; then @@ -131,7 +131,7 @@ EOF printf "%-30s %10s\n" "git clone" "$(echo "scale=2; $GIT_CLONE_TIME / 1000" | bc)" printf "%-30s %10s\n" "./setup-environment.sh" "$(echo "scale=2; $SETUP_TIME / 1000" | bc)" printf "%-30s %10s\n" "tito module status" "$(echo "scale=2; $MODULE_STATUS_TIME / 1000" | bc)" - printf "%-30s %10s\n" "tito logo" "$(echo "scale=2; $LOGO_TIME / 1000" | bc)" + printf "%-30s %10s\n" "tito system logo" "$(echo "scale=2; $LOGO_TIME / 1000" | bc)" echo "" # Recommendations diff --git a/tinytorch/site/_static/demos/scripts/tito-demo.sh b/tinytorch/site/_static/demos/scripts/tito-demo.sh index 8ac314241..66a508b5f 100755 --- a/tinytorch/site/_static/demos/scripts/tito-demo.sh +++ b/tinytorch/site/_static/demos/scripts/tito-demo.sh @@ -211,8 +211,8 @@ validate() { "$collect_timing" \ "true" - test_command "tito logo" \ - "source activate.sh && tito logo" \ + test_command "tito system logo" \ + "source activate.sh && tito system logo" \ "TinyTorch" \ "$collect_timing" \ "true" diff --git a/tinytorch/site/_static/demos/scripts/validate_demos.sh b/tinytorch/site/_static/demos/scripts/validate_demos.sh index 46e3d7dad..58a5146a1 100755 --- a/tinytorch/site/_static/demos/scripts/validate_demos.sh +++ b/tinytorch/site/_static/demos/scripts/validate_demos.sh @@ -80,8 +80,8 @@ test_command "tito module status" \ "source activate.sh && tito module status" \ "Module" -test_command "tito logo" \ - "source activate.sh && tito logo" \ +test_command "tito system logo" \ + "source activate.sh && tito system logo" \ "TinyTorch" echo "" diff --git a/tinytorch/site/_static/demos/tapes/01-zero-to-ready.tape b/tinytorch/site/_static/demos/tapes/01-zero-to-ready.tape index 18a07ec4a..9b679fbc4 100644 --- a/tinytorch/site/_static/demos/tapes/01-zero-to-ready.tape +++ b/tinytorch/site/_static/demos/tapes/01-zero-to-ready.tape @@ -76,7 +76,7 @@ Sleep 2s # STEP 6: READ THE STORY - Understand the TinyTorch philosophy # ============================================================================== -Type "tito logo" +Type "tito system logo" Sleep 400ms Enter Wait+Line@10ms /profvjreddi/ diff --git a/tinytorch/site/_static/demos/tapes/05-logo.tape b/tinytorch/site/_static/demos/tapes/05-logo.tape index 81e9b2f67..835538a89 100644 --- a/tinytorch/site/_static/demos/tapes/05-logo.tape +++ b/tinytorch/site/_static/demos/tapes/05-logo.tape @@ -1,5 +1,5 @@ # VHS Tape: 🔥 The TinyTorch Story - Philosophy & Vision -# Purpose: Show the beautiful story behind TinyTorch with tito logo +# Purpose: Show the beautiful story behind TinyTorch with tito system logo # Duration: 45-50 seconds Output "gifs/05-logo.gif" @@ -53,7 +53,7 @@ Sleep 500ms # SHOW THE LOGO: Display the complete philosophy # ============================================================================== -Type "tito logo" +Type "tito system logo" Sleep 400ms Enter Sleep 3s # Let the output complete diff --git a/tinytorch/site/getting-started.md b/tinytorch/site/getting-started.md index a2b62bd3d..9b5a4cf27 100644 --- a/tinytorch/site/getting-started.md +++ b/tinytorch/site/getting-started.md @@ -61,7 +61,7 @@ tito --version **Update TinyTorch:** ```bash -tito update +tito system update ``` ## Step 2: Your First Module (15 Minutes) @@ -187,7 +187,7 @@ tito milestone run # Run a milestone with your code # Utilities tito setup # First-time setup (safe to re-run) -tito update # Update TinyTorch (your work is preserved) +tito system update # Update TinyTorch (your work is preserved) tito --help # Full command reference ``` diff --git a/tinytorch/tito/commands/grade.py b/tinytorch/tito/commands/grade.py deleted file mode 100644 index 56de4fc4b..000000000 --- a/tinytorch/tito/commands/grade.py +++ /dev/null @@ -1,444 +0,0 @@ -""" -Grade command for TinyTorch - wraps NBGrader functionality. - -This command provides a simplified interface to NBGrader, allowing instructors -to manage assignments and grading without needing to know NBGrader details. -""" - -import subprocess -from pathlib import Path -from argparse import Namespace -from typing import Optional -from rich.console import Console -from rich.panel import Panel -from rich.text import Text -from rich.table import Table -from rich.progress import Progress, SpinnerColumn, TextColumn -from .base import BaseCommand - - -class GradeCommand(BaseCommand): - """Handle grading operations through NBGrader.""" - - @property - def name(self) -> str: - return "grade" - - @property - def description(self) -> str: - return "Simplified grading interface (instructor tool)" - - def add_arguments(self, parser): - """Add arguments for the grade command.""" - # Subcommands for different grading operations - grade_subparsers = parser.add_subparsers(dest='grade_action', help='Grade operations') - - # Release assignment to students - release_parser = grade_subparsers.add_parser( - 'release', - help='Release assignment to students (removes solutions)' - ) - release_parser.add_argument( - 'module', - help='Module name (e.g., 01_setup or setup)' - ) - release_parser.add_argument( - '--course-id', - default='tinytorch', - help='Course identifier (default: tinytorch)' - ) - - # Generate assignment (with solutions for instructor) - generate_parser = grade_subparsers.add_parser( - 'generate', - help='Generate assignment with solutions (instructor version)' - ) - generate_parser.add_argument( - 'module', - help='Module name to generate' - ) - - # Collect student submissions - collect_parser = grade_subparsers.add_parser( - 'collect', - help='Collect student submissions' - ) - collect_parser.add_argument( - 'module', - help='Module to collect' - ) - collect_parser.add_argument( - '--student', - help='Specific student ID (collects all if not specified)' - ) - - # Autograde submissions - autograde_parser = grade_subparsers.add_parser( - 'autograde', - help='Automatically grade collected submissions' - ) - autograde_parser.add_argument( - 'module', - help='Module to autograde' - ) - autograde_parser.add_argument( - '--student', - help='Specific student ID (grades all if not specified)' - ) - - # Manual grading interface - manual_parser = grade_subparsers.add_parser( - 'manual', - help='Open manual grading interface' - ) - manual_parser.add_argument( - 'module', - help='Module to grade manually' - ) - - # Generate feedback - feedback_parser = grade_subparsers.add_parser( - 'feedback', - help='Generate feedback for students' - ) - feedback_parser.add_argument( - 'module', - help='Module to generate feedback for' - ) - - # Export grades - export_parser = grade_subparsers.add_parser( - 'export', - help='Export grades to CSV' - ) - export_parser.add_argument( - '--module', - help='Specific module (exports all if not specified)' - ) - export_parser.add_argument( - '--output', - default='grades.csv', - help='Output file name' - ) - - # Setup NBGrader course - setup_parser = grade_subparsers.add_parser( - 'setup', - help='Set up NBGrader course structure' - ) - - def run(self, args: Namespace) -> int: - """Execute the grade command.""" - if not hasattr(args, 'grade_action') or not args.grade_action: - self._show_help() - return 0 - - action = args.grade_action - - # Route to appropriate handler - if action == 'release': - return self._release_assignment(args) - elif action == 'generate': - return self._generate_assignment(args) - elif action == 'collect': - return self._collect_submissions(args) - elif action == 'autograde': - return self._autograde_submissions(args) - elif action == 'manual': - return self._manual_grade(args) - elif action == 'feedback': - return self._generate_feedback(args) - elif action == 'export': - return self._export_grades(args) - elif action == 'setup': - return self._setup_course(args) - else: - self._show_help() - return 0 - - def _show_help(self): - """Show help information for grade command.""" - help_panel = Panel( - "[bold cyan]TinyTorch Grade Command[/bold cyan]\n\n" - "Simplified interface to NBGrader for managing assignments and grading.\n\n" - "[bold]Available Commands:[/bold]\n" - " tito grade setup - Set up NBGrader course structure\n" - " tito grade generate MODULE - Generate instructor version with solutions\n" - " tito grade release MODULE - Release student version (no solutions)\n" - " tito grade collect MODULE - Collect student submissions\n" - " tito grade autograde MODULE - Auto-grade submissions\n" - " tito grade manual MODULE - Manual grading interface\n" - " tito grade feedback MODULE - Generate student feedback\n" - " tito grade export - Export grades to CSV\n\n" - "[bold]Typical Workflow:[/bold]\n" - " 1. tito grade setup # One-time setup\n" - " 2. tito grade generate 01_setup # Create instructor version\n" - " 3. tito grade release 01_setup # Create student version\n" - " 4. [Students complete work]\n" - " 5. tito grade collect 01_setup # Collect submissions\n" - " 6. tito grade autograde 01_setup # Auto-grade\n" - " 7. tito grade manual 01_setup # Manual review\n" - " 8. tito grade feedback 01_setup # Generate feedback\n" - " 9. tito grade export # Export grades\n\n" - "[dim]Note: NBGrader must be installed and configured[/dim]", - title="Grade Help", - border_style="bright_cyan" - ) - self.console.print(help_panel) - - def _normalize_module_name(self, module: str) -> str: - """Normalize module name to full format.""" - # If already in full format, return as is - if module.startswith(tuple(f"{i:02d}_" for i in range(100))): - return module - - # Try to find the module by short name - source_dir = Path("modules") - if source_dir.exists(): - for module_dir in source_dir.iterdir(): - if module_dir.is_dir() and module_dir.name.endswith(f"_{module}"): - return module_dir.name - - return module - - def _release_assignment(self, args: Namespace) -> int: - """Release assignment to students (removes solutions).""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Releasing Assignment: {module}[/bold]") - - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=self.console - ) as progress: - task = progress.add_task("Creating student version...", total=None) - - try: - # Step 1: Generate assignment first - result = subprocess.run( - ["nbgrader", "generate_assignment", module, - "--source", f"modules/{module}", - "--force"], - capture_output=True, - text=True - ) - - if result.returncode != 0: - self.console.print(f"[red]❌ Failed to generate assignment: {result.stderr}[/red]") - return 1 - - progress.update(task, description="Releasing to students...") - - # Step 2: Release assignment - result = subprocess.run( - ["nbgrader", "release_assignment", module, - "--course", args.course_id], - capture_output=True, - text=True - ) - - if result.returncode != 0: - self.console.print(f"[red]❌ Failed to release: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Assignment {module} released to students![/green]") - self.console.print(f"[dim]Student version available in: release/{args.course_id}/{module}/[/dim]") - return 0 - - def _generate_assignment(self, args: Namespace) -> int: - """Generate assignment with solutions for instructor.""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Generating Instructor Assignment: {module}[/bold]") - - try: - result = subprocess.run( - ["nbgrader", "generate_assignment", module, - "--source", f"modules/{module}", - "--force"], - capture_output=True, - text=True - ) - - if result.returncode != 0: - self.console.print(f"[red]❌ Failed to generate: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Instructor version generated![/green]") - self.console.print(f"[dim]Available in: source/{module}/[/dim]") - return 0 - - def _collect_submissions(self, args: Namespace) -> int: - """Collect student submissions.""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Collecting Submissions: {module}[/bold]") - - cmd = ["nbgrader", "collect", module] - if args.student: - cmd.extend(["--student", args.student]) - - try: - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - self.console.print(f"[red]❌ Collection failed: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Submissions collected![/green]") - return 0 - - def _autograde_submissions(self, args: Namespace) -> int: - """Auto-grade collected submissions.""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Auto-Grading: {module}[/bold]") - - cmd = ["nbgrader", "autograde", module] - if args.student: - cmd.extend(["--student", args.student]) - - try: - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - self.console.print(f"[red]❌ Auto-grading failed: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Auto-grading complete![/green]") - self.console.print("[dim]Use 'tito grade manual' for manual review[/dim]") - return 0 - - def _manual_grade(self, args: Namespace) -> int: - """Open manual grading interface.""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Opening Manual Grading Interface[/bold]") - self.console.print("[dim]This will open in your browser...[/dim]") - - try: - # Launch formgrader interface - subprocess.Popen(["nbgrader", "formgrader"]) - self.console.print("[green]✅ Grading interface launched![/green]") - self.console.print("[dim]Access at: http://localhost:5000[/dim]") - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - return 0 - - def _generate_feedback(self, args: Namespace) -> int: - """Generate feedback for students.""" - module = self._normalize_module_name(args.module) - - self.console.print(f"\n[bold]Generating Feedback: {module}[/bold]") - - try: - result = subprocess.run( - ["nbgrader", "generate_feedback", module], - capture_output=True, - text=True - ) - - if result.returncode != 0: - self.console.print(f"[red]❌ Feedback generation failed: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Feedback generated![/green]") - return 0 - - def _export_grades(self, args: Namespace) -> int: - """Export grades to CSV.""" - self.console.print(f"\n[bold]Exporting Grades[/bold]") - - try: - cmd = ["nbgrader", "export"] - if args.module: - module = self._normalize_module_name(args.module) - cmd.extend(["--assignment", module]) - cmd.extend(["--to", args.output]) - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode != 0: - self.console.print(f"[red]❌ Export failed: {result.stderr}[/red]") - return 1 - - except FileNotFoundError: - self.console.print("[red]❌ NBGrader not found. Install with: pip install nbgrader[/red]") - return 1 - - self.console.print(f"[green]✅ Grades exported to {args.output}![/green]") - return 0 - - def _setup_course(self, args: Namespace) -> int: - """Set up NBGrader course structure.""" - self.console.print("\n[bold]Setting Up NBGrader Course[/bold]") - - # Create necessary directories - dirs_to_create = [ - "source", - "release", - "submitted", - "autograded", - "feedback" - ] - - for dir_name in dirs_to_create: - Path(dir_name).mkdir(exist_ok=True) - self.console.print(f" ✅ Created {dir_name}/") - - # Create nbgrader_config.py if it doesn't exist - config_file = Path("nbgrader_config.py") - if not config_file.exists(): - config_content = '''"""NBGrader configuration for TinyTorch.""" - -c = get_config() - -c.CourseDirectory.course_id = "tinytorch" -c.CourseDirectory.source_directory = "modules" -c.CourseDirectory.release_directory = "release" -c.CourseDirectory.submitted_directory = "submitted" -c.CourseDirectory.autograded_directory = "autograded" -c.CourseDirectory.feedback_directory = "feedback" - -# Exchange settings -c.Exchange.root = "/tmp/exchange" -c.Exchange.course_id = "tinytorch" - -# Grading settings -c.ExecuteOptions.timeout = 300 # 5 minutes per cell -c.ExecuteOptions.allow_errors = True -c.ExecuteOptions.interrupt_on_timeout = True -''' - config_file.write_text(config_content) - self.console.print(" ✅ Created nbgrader_config.py") - - self.console.print("[green]✅ NBGrader course setup complete![/green]") - self.console.print("\n[bold]Next Steps:[/bold]") - self.console.print(" 1. tito grade generate 01_setup # Create instructor version") - self.console.print(" 2. tito grade release 01_setup # Create student version") - - return 0 diff --git a/tinytorch/tito/commands/logo.py b/tinytorch/tito/commands/system/logo.py similarity index 98% rename from tinytorch/tito/commands/logo.py rename to tinytorch/tito/commands/system/logo.py index 7d179075c..e60871534 100644 --- a/tinytorch/tito/commands/logo.py +++ b/tinytorch/tito/commands/system/logo.py @@ -9,7 +9,7 @@ from rich.text import Text from rich.align import Align from pathlib import Path -from .base import BaseCommand +from ..base import BaseCommand class LogoCommand(BaseCommand): @property @@ -28,7 +28,7 @@ class LogoCommand(BaseCommand): console = self.console # Display the ASCII logo first - from ..core.console import print_ascii_logo + from ...core.console import print_ascii_logo print_ascii_logo() # Create the explanation text diff --git a/tinytorch/tito/commands/system/system.py b/tinytorch/tito/commands/system/system.py index a51a37603..f80f0dfcb 100644 --- a/tinytorch/tito/commands/system/system.py +++ b/tinytorch/tito/commands/system/system.py @@ -9,6 +9,9 @@ from ..base import BaseCommand from .info import InfoCommand from .health import HealthCommand from .jupyter import JupyterCommand +from .update import UpdateCommand +from .logo import LogoCommand + class SystemCommand(BaseCommand): @property @@ -50,6 +53,22 @@ class SystemCommand(BaseCommand): jupyter_cmd = JupyterCommand(self.config) jupyter_cmd.add_arguments(jupyter_parser) + # Update subcommand + update_parser = subparsers.add_parser( + 'update', + help='Check for and install updates' + ) + update_cmd = UpdateCommand(self.config) + update_cmd.add_arguments(update_parser) + + # Logo subcommand + logo_parser = subparsers.add_parser( + 'logo', + help='Learn about the TinyTorch logo and its meaning' + ) + logo_cmd = LogoCommand(self.config) + logo_cmd.add_arguments(logo_parser) + def run(self, args: Namespace) -> int: console = self.console @@ -59,7 +78,9 @@ class SystemCommand(BaseCommand): "Available subcommands:\n" " • [bold]info[/bold] - Show system/environment information\n" " • [bold]health[/bold] - Environment health check and validation\n" - " • [bold]jupyter[/bold] - Start Jupyter notebook server\n\n" + " • [bold]jupyter[/bold] - Start Jupyter notebook server\n" + " • [bold]update[/bold] - Check for and install updates\n" + " • [bold]logo[/bold] - Learn about the TinyTorch logo\n\n" "[dim]Example: tito system health[/dim]", title="System Command Group", border_style="bright_cyan" @@ -76,6 +97,12 @@ class SystemCommand(BaseCommand): elif args.system_command == 'jupyter': cmd = JupyterCommand(self.config) return cmd.execute(args) + elif args.system_command == 'update': + cmd = UpdateCommand(self.config) + return cmd.execute(args) + elif args.system_command == 'logo': + cmd = LogoCommand(self.config) + return cmd.execute(args) else: console.print(Panel( f"[red]Unknown system subcommand: {args.system_command}[/red]", diff --git a/tinytorch/tito/commands/update.py b/tinytorch/tito/commands/system/update.py similarity index 98% rename from tinytorch/tito/commands/update.py rename to tinytorch/tito/commands/system/update.py index 335db18eb..a028635b7 100644 --- a/tinytorch/tito/commands/update.py +++ b/tinytorch/tito/commands/system/update.py @@ -14,7 +14,7 @@ import os from argparse import ArgumentParser, Namespace from typing import Optional, Tuple -from .base import BaseCommand +from ..base import BaseCommand class UpdateCommand(BaseCommand): @@ -211,7 +211,7 @@ class UpdateCommand(BaseCommand): if args.check: self.console.print() self.console.print("To update, run:") - self.console.print(f" [cyan]tito update[/cyan]") + self.console.print(f" [cyan]tito system update[/cyan]") self.console.print() self.console.print("Or manually:") self.console.print(f" [dim]curl -fsSL {self.INSTALL_URL} | bash[/dim]") diff --git a/tinytorch/tito/main.py b/tinytorch/tito/main.py index 09860749f..380056129 100644 --- a/tinytorch/tito/main.py +++ b/tinytorch/tito/main.py @@ -30,15 +30,12 @@ from .commands.system import SystemCommand from .commands.module import ModuleWorkflowCommand from .commands.package import PackageCommand from .commands.nbgrader import NBGraderCommand -from .commands.grade import GradeCommand -from .commands.logo import LogoCommand from .commands.milestone import MilestoneCommand from .commands.setup import SetupCommand from .commands.benchmark import BenchmarkCommand from .commands.community import CommunityCommand from .commands.dev import DevCommand from .commands.olympics import OlympicsCommand -from .commands.update import UpdateCommand # Import version from tinytorch package try: @@ -65,6 +62,7 @@ class TinyTorchCLI: """Initialize the CLI application.""" self.config = CLIConfig.from_project_root() self.console = get_console() + self._tito_dir = self.config.project_root / '.tito' # SINGLE SOURCE OF TRUTH: All valid commands registered here self.commands: Dict[str, Type[BaseCommand]] = { # Essential @@ -82,10 +80,6 @@ class TinyTorchCLI: 'community': CommunityCommand, 'benchmark': BenchmarkCommand, 'olympics': OlympicsCommand, - # Utilities - 'update': UpdateCommand, - 'grade': GradeCommand, - 'logo': LogoCommand, } # Command categorization for help display @@ -139,6 +133,46 @@ class TinyTorchCLI: return "\n".join(lines) + def _is_first_run(self) -> bool: + """Check if this is the first time running tito.""" + return not self._tito_dir.exists() + + def _mark_welcome_shown(self) -> None: + """Mark that the welcome message has been shown by creating .tito/ folder.""" + self._tito_dir.mkdir(parents=True, exist_ok=True) + + def _show_first_run_welcome(self) -> None: + """Show a one-time welcome message for new users.""" + if not self._is_first_run(): + return + + from rich import box + + welcome_text = f"""[{Theme.EMPHASIS}]🎓 LEARNING APPROACH[/{Theme.EMPHASIS}] + +Solutions are included in the notebooks. [bold]This is intentional![/bold] + +The best way to learn: + [{Theme.SUCCESS}]1.[/{Theme.SUCCESS}] Read the module and run the code + [{Theme.SUCCESS}]2.[/{Theme.SUCCESS}] Study how the solutions work + [{Theme.SUCCESS}]3.[/{Theme.SUCCESS}] Try implementing from scratch + [{Theme.DIM}](reset with: tito module reset)[/{Theme.DIM}] + +[{Theme.WARNING}]🐛 PRE-RELEASE:[/{Theme.WARNING}] We're looking for bugs and feedback! + Found something? → [{Theme.INFO}]github.com/harvard-edge/cs249r_book/discussions[/{Theme.INFO}]""" + + self.console.print() + self.console.print(Panel( + welcome_text, + title="[bold]Welcome to TinyTorch (Pre-release)[/bold]", + border_style=Theme.BORDER_WELCOME, + box=box.ROUNDED + )) + self.console.print() + + # Mark as shown so it only appears once + self._mark_welcome_shown() + def _generate_epilog(self) -> str: """Generate dynamic epilog from registered commands.""" lines = [] @@ -288,7 +322,7 @@ class TinyTorchCLI: self.config.no_color = True # Guard against running outside a virtual environment unless explicitly allowed - if parsed_args.command not in ['setup', 'logo', None]: + if parsed_args.command not in ['setup', None]: # Check both sys.prefix (traditional activation) and VIRTUAL_ENV (direnv/PATH-based) in_venv = sys.prefix != sys.base_prefix or os.environ.get("VIRTUAL_ENV") is not None allow_system = os.environ.get("TITO_ALLOW_SYSTEM") == "1" @@ -301,14 +335,14 @@ class TinyTorchCLI: ) return 1 - # Show banner for interactive commands (except logo which has its own display) # Skip banner for dev command with --json flag (CI/CD output) skip_banner = ( - parsed_args.command == 'logo' or - (parsed_args.command == 'dev' and hasattr(parsed_args, 'json') and parsed_args.json) + parsed_args.command == 'dev' and hasattr(parsed_args, 'json') and parsed_args.json ) if parsed_args.command and not self.config.no_color and not skip_banner: print_banner() + # Show first-run welcome (only once, ever) + self._show_first_run_welcome() # Validate environment for most commands (skip for health) skip_validation = ( @@ -326,6 +360,9 @@ class TinyTorchCLI: # Show ASCII logo first print_ascii_logo() + # Show first-run welcome (only once, ever) + self._show_first_run_welcome() + # Generate dynamic welcome message self.console.print(Panel( self._generate_welcome_text(),