mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-03-11 17:49:25 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]",
|
||||
|
||||
@@ -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]")
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user