feat(cli): add preview and doctor commands with clear descriptions

New Commands:
- Add PreviewCommand for live development server with hot reload
- Add DoctorCommand for comprehensive health checks (18 checks)
- Clear command descriptions distinguish build vs preview vs formats

Command Clarity:
- build: Build static files to disk (HTML)
- preview: Start live dev server with hot reload
- pdf: Build static PDF file to disk
- epub: Build static EPUB file to disk

Health Check Features:
- System dependencies (Python, Quarto, Git, Node.js, R)
- Configuration file validation
- Chapter file scanning (62 chapters found)
- Build artifacts status
- Git repository status
- File permissions check

All tests passing - comprehensive CLI ready for use
This commit is contained in:
Vijay Janapa Reddi
2025-08-27 15:09:21 +02:00
parent 2bdcc6e267
commit 70e92435eb
15 changed files with 674 additions and 12 deletions

47
binder
View File

@@ -40,6 +40,7 @@ class BookBinder:
self.html_config = self.book_dir / "config" / "_quarto-html.yml"
self.pdf_config = self.book_dir / "config" / "_quarto-pdf.yml"
self.epub_config = self.book_dir / "config" / "_quarto-epub.yml"
self.active_config = self.book_dir / "_quarto.yml"
def get_output_dir(self, format_type="html"):
@@ -48,7 +49,14 @@ class BookBinder:
import yaml
# Choose the appropriate config file
config_file = self.html_config if format_type == "html" else self.pdf_config
if format_type == "html":
config_file = self.html_config
elif format_type == "pdf":
config_file = self.pdf_config
elif format_type == "epub":
config_file = self.epub_config
else:
config_file = self.html_config
if not config_file.exists():
console.print(f"[yellow]⚠️ Config file not found: {config_file}[/yellow]")
@@ -613,7 +621,10 @@ class BookBinder:
config_file = self.pdf_config
format_arg = "titlepage-pdf"
build_subdir = "pdf"
elif format_type == "epub":
config_file = self.epub_config
format_arg = "epub"
build_subdir = "epub"
else:
raise ValueError(f"Unknown format type: {format_type}")
@@ -704,7 +715,10 @@ class BookBinder:
config_file = self.pdf_config
format_arg = "titlepage-pdf"
build_subdir = "pdf"
elif format_type == "epub":
config_file = self.epub_config
format_arg = "epub"
build_subdir = "epub"
else:
raise ValueError(f"Unknown format type: {format_type}")
@@ -3027,6 +3041,7 @@ This is an ACADEMIC TEXTBOOK release. Please format as:
fast_table.add_row("build [chapter[,ch2,...]]", "Build book or chapter(s) (HTML)", "./binder build intro,ops")
fast_table.add_row("preview [chapter[,ch2,...]]", "Preview book or chapter(s)", "./binder preview intro")
fast_table.add_row("pdf [chapter[,ch2,...]]", "Build book or chapter(s) (PDF)", "./binder pdf intro")
fast_table.add_row("epub [chapter[,ch2,...]]", "Build book or chapter(s) (EPUB)", "./binder epub intro")
full_table = Table(show_header=True, header_style="bold blue", box=None)
full_table.add_column("Command", style="blue", width=35)
@@ -3036,6 +3051,7 @@ This is an ACADEMIC TEXTBOOK release. Please format as:
full_table.add_row("build", "Build entire book (HTML)", "./binder build")
full_table.add_row("preview", "Preview entire book", "./binder preview")
full_table.add_row("pdf", "Build entire book (PDF)", "./binder pdf")
full_table.add_row("epub", "Build entire book (EPUB)", "./binder epub")
full_table.add_row("publish", "Deploy website updates (no release)", "./binder publish")
full_table.add_row("release", "Create formal textbook release", "./binder release")
@@ -3066,6 +3082,7 @@ This is an ACADEMIC TEXTBOOK release. Please format as:
shortcuts_table.add_row("b", "build")
shortcuts_table.add_row("p", "preview")
shortcuts_table.add_row("pdf", "pdf")
shortcuts_table.add_row("epub", "epub")
shortcuts_table.add_row("c", "clean")
shortcuts_table.add_row("ch", "check")
shortcuts_table.add_row("s", "switch")
@@ -3092,10 +3109,12 @@ This is an ACADEMIC TEXTBOOK release. Please format as:
examples.append("# Build multiple chapters (HTML)\n", style="dim")
examples.append(" ./binder pdf intro ", style="cyan")
examples.append("# Build single chapter as PDF\n", style="dim")
examples.append(" ./binder epub intro ", style="cyan")
examples.append("# Build single chapter as EPUB\n", style="dim")
examples.append(" ./binder preview ", style="cyan")
examples.append("# Preview entire book\n", style="dim")
examples.append(" ./binder pdf ", style="cyan")
examples.append("# Build entire book as PDF\n", style="dim")
examples.append(" ./binder epub ", style="cyan")
examples.append("# Build entire book as EPUB\n", style="dim")
examples.append(" ./binder c ", style="cyan")
examples.append("# Clean all artifacts\n", style="dim")
examples.append(" ./binder st ", style="cyan")
@@ -4024,6 +4043,24 @@ def main():
# Single chapter - build as PDF
binder.build_multiple([chapters], "pdf")
elif command == "epub":
binder.show_symlink_status()
if len(sys.argv) < 3:
# No target specified - build entire book as EPUB
console.print("[purple]📚 Building entire book (EPUB)...[/purple]")
binder.build_full("epub")
else:
# Chapter specified - build specific chapter(s) as EPUB
chapters = sys.argv[2]
console.print(f"[purple]📚 Building chapter(s) as EPUB: {chapters}[/purple]")
if ',' in chapters:
# Multiple chapters - build them together as EPUB
chapter_list = [ch.strip() for ch in chapters.split(',')]
binder.build_multiple(chapter_list, "epub")
else:
# Single chapter - build as EPUB
binder.build_multiple([chapters], "epub")
elif command == "clean":

9
cli/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""
MLSysBook CLI Package
A modular command-line interface for building, previewing, and managing
the Machine Learning Systems textbook.
"""
__version__ = "2.0.0"
__author__ = "MLSysBook Team"

6
cli/commands/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
Command implementations for the MLSysBook CLI.
Each command is implemented as a separate module for better maintainability
and testing.
"""

438
cli/commands/doctor.py Normal file
View File

@@ -0,0 +1,438 @@
"""
Health check command for MLSysBook CLI.
Performs comprehensive system health checks to ensure everything is working properly.
"""
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple, Dict, Any
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
class DoctorCommand:
"""Performs health checks for the MLSysBook system."""
def __init__(self, config_manager, chapter_discovery):
"""Initialize doctor command.
Args:
config_manager: ConfigManager instance
chapter_discovery: ChapterDiscovery instance
"""
self.config_manager = config_manager
self.chapter_discovery = chapter_discovery
self.checks = []
def run_health_check(self) -> bool:
"""Run comprehensive health check.
Returns:
True if all checks pass, False if any issues found
"""
console.print("[bold blue]🏥 MLSysBook Health Check[/bold blue]")
console.print("[dim]Running comprehensive system diagnostics...[/dim]\n")
self.checks = []
# Run all health checks
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
transient=True
) as progress:
# System dependencies
task = progress.add_task("Checking system dependencies...", total=None)
self._check_system_dependencies()
progress.update(task, completed=True)
# Configuration files
task = progress.add_task("Validating configuration files...", total=None)
self._check_configuration_files()
progress.update(task, completed=True)
# Chapter files
task = progress.add_task("Scanning chapter files...", total=None)
self._check_chapter_files()
progress.update(task, completed=True)
# Build artifacts
task = progress.add_task("Checking build artifacts...", total=None)
self._check_build_artifacts()
progress.update(task, completed=True)
# Git status
task = progress.add_task("Checking git status...", total=None)
self._check_git_status()
progress.update(task, completed=True)
# Permissions
task = progress.add_task("Checking file permissions...", total=None)
self._check_permissions()
progress.update(task, completed=True)
# Display results
self._display_results()
# Return overall health status
failed_checks = [check for check in self.checks if not check["passed"]]
return len(failed_checks) == 0
def _check_system_dependencies(self) -> None:
"""Check required system dependencies."""
dependencies = [
("Python", [sys.executable, "--version"]),
("Quarto", ["quarto", "--version"]),
("Git", ["git", "--version"]),
("Node.js", ["node", "--version"]),
("R", ["R", "--version"]),
]
for name, cmd in dependencies:
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
version = result.stdout.strip().split('\n')[0]
self.checks.append({
"category": "Dependencies",
"name": name,
"passed": True,
"message": f"{version}",
"details": None
})
else:
self.checks.append({
"category": "Dependencies",
"name": name,
"passed": False,
"message": "❌ Not found or error",
"details": result.stderr.strip() if result.stderr else "Command failed"
})
except (subprocess.TimeoutExpired, FileNotFoundError):
self.checks.append({
"category": "Dependencies",
"name": name,
"passed": False,
"message": "❌ Not installed",
"details": f"Command '{' '.join(cmd)}' not found"
})
def _check_configuration_files(self) -> None:
"""Check Quarto configuration files."""
configs = [
("HTML Config", self.config_manager.html_config),
("PDF Config", self.config_manager.pdf_config),
("EPUB Config", self.config_manager.epub_config),
]
for name, config_path in configs:
if config_path.exists():
try:
# Try to read and parse the config
format_type = name.lower().split()[0]
config_data = self.config_manager.read_config(format_type)
# Check for required sections
required_sections = ["project", "book"]
missing_sections = [s for s in required_sections if s not in config_data]
if missing_sections:
self.checks.append({
"category": "Configuration",
"name": name,
"passed": False,
"message": "⚠️ Missing sections",
"details": f"Missing: {', '.join(missing_sections)}"
})
else:
self.checks.append({
"category": "Configuration",
"name": name,
"passed": True,
"message": "✅ Valid",
"details": None
})
except Exception as e:
self.checks.append({
"category": "Configuration",
"name": name,
"passed": False,
"message": "❌ Parse error",
"details": str(e)
})
else:
self.checks.append({
"category": "Configuration",
"name": name,
"passed": False,
"message": "❌ Not found",
"details": f"File not found: {config_path}"
})
# Check active config symlink
if self.config_manager.active_config.is_symlink():
target = self.config_manager.active_config.readlink()
self.checks.append({
"category": "Configuration",
"name": "Active Config",
"passed": True,
"message": f"✅ Linked to {target}",
"details": None
})
elif self.config_manager.active_config.exists():
self.checks.append({
"category": "Configuration",
"name": "Active Config",
"passed": False,
"message": "⚠️ Regular file (not symlink)",
"details": "Should be a symlink to format-specific config"
})
else:
self.checks.append({
"category": "Configuration",
"name": "Active Config",
"passed": False,
"message": "❌ Not found",
"details": "No _quarto.yml found"
})
def _check_chapter_files(self) -> None:
"""Check chapter files and structure."""
chapters = self.chapter_discovery.get_all_chapters()
self.checks.append({
"category": "Content",
"name": "Chapter Count",
"passed": len(chapters) > 0,
"message": f"{len(chapters)} chapters found" if len(chapters) > 0 else "❌ No chapters found",
"details": None
})
# Check for common issues
large_chapters = [ch for ch in chapters if ch["size"] > 500 * 1024] # > 500KB
if large_chapters:
self.checks.append({
"category": "Content",
"name": "Large Chapters",
"passed": False,
"message": f"⚠️ {len(large_chapters)} large chapters (>500KB)",
"details": f"Large chapters: {', '.join([ch['name'] for ch in large_chapters[:3]])}"
})
# Check for missing chapters (common ones)
expected_chapters = ["introduction", "ml_systems", "training", "ops"]
missing_chapters = []
for expected in expected_chapters:
if not self.chapter_discovery.find_chapter_file(expected):
missing_chapters.append(expected)
if missing_chapters:
self.checks.append({
"category": "Content",
"name": "Core Chapters",
"passed": False,
"message": f"⚠️ {len(missing_chapters)} core chapters missing",
"details": f"Missing: {', '.join(missing_chapters)}"
})
else:
self.checks.append({
"category": "Content",
"name": "Core Chapters",
"passed": True,
"message": "✅ All core chapters present",
"details": None
})
def _check_build_artifacts(self) -> None:
"""Check for build artifacts and output directories."""
formats = ["html", "pdf", "epub"]
for format_type in formats:
output_dir = self.config_manager.get_output_dir(format_type)
if output_dir.exists():
# Count files in output directory
files = list(output_dir.rglob("*"))
file_count = len([f for f in files if f.is_file()])
self.checks.append({
"category": "Build Artifacts",
"name": f"{format_type.upper()} Output",
"passed": True,
"message": f"{file_count} files in {output_dir.name}/",
"details": None
})
else:
self.checks.append({
"category": "Build Artifacts",
"name": f"{format_type.upper()} Output",
"passed": True, # Not an error if no builds exist yet
"message": "📁 No build artifacts (clean)",
"details": None
})
def _check_git_status(self) -> None:
"""Check git repository status."""
try:
# Check if we're in a git repo
result = subprocess.run(
["git", "rev-parse", "--git-dir"],
capture_output=True,
text=True,
cwd=self.config_manager.root_dir
)
if result.returncode != 0:
self.checks.append({
"category": "Git",
"name": "Repository",
"passed": False,
"message": "❌ Not a git repository",
"details": None
})
return
# Check current branch
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True,
text=True,
cwd=self.config_manager.root_dir
)
if result.returncode == 0:
branch = result.stdout.strip()
self.checks.append({
"category": "Git",
"name": "Current Branch",
"passed": True,
"message": f"{branch}",
"details": None
})
# Check for uncommitted changes
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True,
text=True,
cwd=self.config_manager.root_dir
)
if result.returncode == 0:
changes = result.stdout.strip()
if changes:
change_count = len(changes.split('\n'))
self.checks.append({
"category": "Git",
"name": "Working Tree",
"passed": True, # Not necessarily an error
"message": f"📝 {change_count} uncommitted changes",
"details": None
})
else:
self.checks.append({
"category": "Git",
"name": "Working Tree",
"passed": True,
"message": "✅ Clean",
"details": None
})
except Exception as e:
self.checks.append({
"category": "Git",
"name": "Status Check",
"passed": False,
"message": "❌ Error checking git status",
"details": str(e)
})
def _check_permissions(self) -> None:
"""Check file permissions for key files."""
key_files = [
("binder", self.config_manager.root_dir / "binder"),
("binder2", self.config_manager.root_dir / "binder2"),
]
for name, file_path in key_files:
if file_path.exists():
is_executable = file_path.stat().st_mode & 0o111
self.checks.append({
"category": "Permissions",
"name": name,
"passed": bool(is_executable),
"message": "✅ Executable" if is_executable else "❌ Not executable",
"details": f"chmod +x {file_path.name}" if not is_executable else None
})
else:
self.checks.append({
"category": "Permissions",
"name": name,
"passed": False,
"message": "❌ File not found",
"details": None
})
def _display_results(self) -> None:
"""Display health check results in a formatted table."""
console.print()
# Group checks by category
categories = {}
for check in self.checks:
cat = check["category"]
if cat not in categories:
categories[cat] = []
categories[cat].append(check)
# Display each category
for category, checks in categories.items():
table = Table(show_header=True, header_style="bold blue", box=None)
table.add_column("Check", style="white", width=20)
table.add_column("Status", style="white", width=30)
table.add_column("Details", style="dim", width=40)
for check in checks:
details = check["details"] if check["details"] else ""
table.add_row(
check["name"],
check["message"],
details
)
console.print(Panel(table, title=f"🔍 {category}", border_style="cyan"))
# Summary
total_checks = len(self.checks)
passed_checks = len([c for c in self.checks if c["passed"]])
failed_checks = total_checks - passed_checks
if failed_checks == 0:
console.print(Panel(
f"[bold green]🎉 All {total_checks} health checks passed![/bold green]\n"
"[dim]Your MLSysBook environment is healthy and ready to use.[/dim]",
title="✅ Health Check Summary",
border_style="green"
))
else:
console.print(Panel(
f"[bold yellow]⚠️ {passed_checks}/{total_checks} checks passed[/bold yellow]\n"
f"[red]{failed_checks} issues found that may need attention.[/red]\n"
"[dim]Review the details above and fix any critical issues.[/dim]",
title="⚠️ Health Check Summary",
border_style="yellow"
))

117
cli/commands/preview.py Normal file
View File

@@ -0,0 +1,117 @@
"""
Preview command implementation for MLSysBook CLI.
Handles starting development servers with live reload for interactive development.
"""
import subprocess
import signal
import sys
from pathlib import Path
from typing import List, Optional
from rich.console import Console
console = Console()
class PreviewCommand:
"""Handles preview operations for the MLSysBook."""
def __init__(self, config_manager, chapter_discovery):
"""Initialize preview command.
Args:
config_manager: ConfigManager instance
chapter_discovery: ChapterDiscovery instance
"""
self.config_manager = config_manager
self.chapter_discovery = chapter_discovery
def preview_full(self, format_type: str = "html") -> bool:
"""Start full preview server for the entire book.
Args:
format_type: Format to preview ('html' only supported)
Returns:
True if server started successfully, False otherwise
"""
if format_type != "html":
console.print(f"[yellow]⚠️ Preview only supports HTML format, not {format_type}[/yellow]")
return False
console.print("[blue]🌐 Starting full book preview server...[/blue]")
# Setup config
config_name = self.config_manager.setup_symlink(format_type)
console.print(f"[blue]🔗 Using {config_name}[/blue]")
console.print("[dim]🛑 Press Ctrl+C to stop the server[/dim]")
try:
# Start Quarto preview server
preview_cmd = ["quarto", "preview"]
console.print(f"[blue]💻 Command: {' '.join(preview_cmd)}[/blue]")
# Run preview server (this will block until Ctrl+C)
result = subprocess.run(
preview_cmd,
cwd=self.config_manager.book_dir
)
return result.returncode == 0
except KeyboardInterrupt:
console.print("\n[yellow]🛑 Preview server stopped[/yellow]")
return True
except Exception as e:
console.print(f"[red]❌ Preview server error: {e}[/red]")
return False
def preview_chapter(self, chapter_name: str) -> bool:
"""Start preview server for a specific chapter.
Args:
chapter_name: Name of the chapter to preview
Returns:
True if server started successfully, False otherwise
"""
# Find the chapter file
chapter_file = self.chapter_discovery.find_chapter_file(chapter_name)
if not chapter_file:
console.print(f"[red]❌ No chapter found matching '{chapter_name}'[/red]")
console.print("[yellow]💡 Available chapters:[/yellow]")
self.chapter_discovery.show_chapters()
return False
target_path = str(chapter_file.relative_to(self.config_manager.book_dir))
chapter_display_name = str(chapter_file.relative_to(self.config_manager.book_dir / "contents")).replace(".qmd", "")
console.print(f"[blue]🌐 Starting preview for[/blue] [bold]{chapter_display_name}[/bold]")
# Setup HTML config for preview
self.config_manager.setup_symlink("html")
console.print("[dim]🛑 Press Ctrl+C to stop the server[/dim]")
try:
# Start Quarto preview for specific file
preview_cmd = ["quarto", "preview", target_path]
console.print(f"[blue]💻 Command: {' '.join(preview_cmd)}[/blue]")
# Run preview server (this will block until Ctrl+C)
result = subprocess.run(
preview_cmd,
cwd=self.config_manager.book_dir
)
return result.returncode == 0
except KeyboardInterrupt:
console.print("\n[yellow]🛑 Preview server stopped[/yellow]")
return True
except Exception as e:
console.print(f"[red]❌ Preview server error: {e}[/red]")
return False

8
cli/core/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
Core functionality for the MLSysBook CLI.
This module contains shared components used across different commands:
- Configuration management
- File and chapter discovery
- Output directory handling
"""

6
cli/formats/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
Format-specific handlers for different output types.
Each format (HTML, PDF, EPUB) has its own module containing
format-specific logic and configuration.
"""

View File

@@ -17,6 +17,8 @@ from rich.text import Text
from core.config import ConfigManager
from core.discovery import ChapterDiscovery
from commands.build import BuildCommand
from commands.preview import PreviewCommand
from commands.doctor import DoctorCommand
console = Console()
@@ -34,6 +36,8 @@ class MLSysBookCLI:
# Initialize command handlers
self.build_command = BuildCommand(self.config_manager, self.chapter_discovery)
self.preview_command = PreviewCommand(self.config_manager, self.chapter_discovery)
self.doctor_command = DoctorCommand(self.config_manager, self.chapter_discovery)
def show_banner(self):
"""Display the CLI banner."""
@@ -54,9 +58,10 @@ class MLSysBookCLI:
fast_table.add_column("Description", style="white", width=30)
fast_table.add_column("Example", style="dim", width=30)
fast_table.add_row("build [chapter[,ch2,...]]", "Build book or chapter(s) (HTML)", "./binder2 build intro,ops")
fast_table.add_row("pdf [chapter[,ch2,...]]", "Build book or chapter(s) (PDF)", "./binder2 pdf intro")
fast_table.add_row("epub [chapter[,ch2,...]]", "Build book or chapter(s) (EPUB)", "./binder2 epub intro")
fast_table.add_row("build [chapter[,ch2,...]]", "Build static files to disk (HTML)", "./binder2 build intro,ops")
fast_table.add_row("preview [chapter[,ch2,...]]", "Start live dev server with hot reload", "./binder2 preview intro")
fast_table.add_row("pdf [chapter[,ch2,...]]", "Build static PDF file to disk", "./binder2 pdf intro")
fast_table.add_row("epub [chapter[,ch2,...]]", "Build static EPUB file to disk", "./binder2 epub intro")
# Full Book Commands
full_table = Table(show_header=True, header_style="bold blue", box=None)
@@ -64,9 +69,10 @@ class MLSysBookCLI:
full_table.add_column("Description", style="white", width=30)
full_table.add_column("Example", style="dim", width=30)
full_table.add_row("build", "Build entire book (HTML)", "./binder2 build")
full_table.add_row("pdf", "Build entire book (PDF)", "./binder2 pdf")
full_table.add_row("epub", "Build entire book (EPUB)", "./binder2 epub")
full_table.add_row("build", "Build entire book as static HTML", "./binder2 build")
full_table.add_row("preview", "Start live dev server for entire book", "./binder2 preview")
full_table.add_row("pdf", "Build entire book as static PDF", "./binder2 pdf")
full_table.add_row("epub", "Build entire book as static EPUB", "./binder2 epub")
# Management Commands
mgmt_table = Table(show_header=True, header_style="bold blue", box=None)
@@ -76,6 +82,7 @@ class MLSysBookCLI:
mgmt_table.add_row("list", "List available chapters", "./binder2 list")
mgmt_table.add_row("status", "Show current config status", "./binder2 status")
mgmt_table.add_row("doctor", "Run comprehensive health check", "./binder2 doctor")
mgmt_table.add_row("help", "Show this help", "./binder2 help")
# Display tables
@@ -140,6 +147,29 @@ class MLSysBookCLI:
chapter_list = [ch.strip() for ch in chapters.split(',')]
return self.build_command.build_chapters(chapter_list, "epub")
def handle_preview_command(self, args):
"""Handle preview command."""
self.config_manager.show_symlink_status()
if len(args) < 1:
# No target specified - preview entire book
console.print("[blue]🌐 Starting preview for entire book...[/blue]")
return self.preview_command.preview_full("html")
else:
# Chapter specified
chapter = args[0]
if ',' in chapter:
console.print("[yellow]⚠️ Preview only supports single chapters, not multiple[/yellow]")
console.print("[dim]💡 Use the first chapter from your list[/dim]")
chapter = chapter.split(',')[0].strip()
console.print(f"[blue]🌐 Starting preview for chapter: {chapter}[/blue]")
return self.preview_command.preview_chapter(chapter)
def handle_doctor_command(self, args):
"""Handle doctor/health check command."""
return self.doctor_command.run_health_check()
def handle_list_command(self, args):
"""Handle list chapters command."""
self.chapter_discovery.show_chapters()
@@ -172,19 +202,24 @@ class MLSysBookCLI:
# Command mapping
commands = {
"build": self.handle_build_command,
"preview": self.handle_preview_command,
"pdf": self.handle_pdf_command,
"epub": self.handle_epub_command,
"list": self.handle_list_command,
"status": self.handle_status_command,
"doctor": self.handle_doctor_command,
"help": lambda args: self.show_help() or True,
}
# Command aliases
aliases = {
"b": "build",
"p": "pdf", # Changed from preview to pdf for simplicity
"p": "preview",
"pdf": "pdf", # Keep pdf as explicit command
"epub": "epub", # Keep epub as explicit command
"l": "list",
"s": "status",
"d": "doctor",
"h": "help",
}

6
cli/utils/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
Utility functions and helpers for the MLSysBook CLI.
Contains shared utilities for console output, validation, and other
common functionality.
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB