refactor(cli): reorganize commands and add welcome message

- Move tito update → tito system update
- Move tito logo → tito system logo
- Remove tito grade (use tito nbgrader instead)
- Add first-run welcome message for new users
- Update demo scripts and docs for new command paths
This commit is contained in:
Vijay Janapa Reddi
2026-01-14 16:51:06 -05:00
parent fd7c0f5e2f
commit 6c108c6323
11 changed files with 91 additions and 471 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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/

View File

@@ -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

View File

@@ -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 <name> # 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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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]",

View File

@@ -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]")

View File

@@ -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(),