mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-16 06:07:17 -05:00
696 lines
33 KiB
Python
696 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MLSysBook CLI - Modular Entry Point
|
|
|
|
A refactored, modular command-line interface for building, previewing,
|
|
and managing the Machine Learning Systems textbook.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from rich.console import Console
|
|
from rich.markup import escape as _rich_escape
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
# Import our modular components
|
|
# Ensure the book directory is in sys.path so 'from cli...' works regardless of CWD
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from cli.core.config import ConfigManager
|
|
from cli.core.discovery import ChapterDiscovery, AmbiguousChapterError
|
|
from cli.commands.build import BuildCommand
|
|
from cli.commands.preview import PreviewCommand
|
|
from cli.commands.doctor import DoctorCommand
|
|
from cli.commands.clean import CleanCommand
|
|
from cli.commands.maintenance import MaintenanceCommand
|
|
from cli.commands.debug import DebugCommand
|
|
from cli.commands.validate import ValidateCommand
|
|
from cli.commands.formatting import FormatCommand
|
|
from cli.commands.info import InfoCommand
|
|
from cli.commands.bib import BibCommand
|
|
from cli.commands.render import RenderCommand
|
|
from cli.commands.newsletter import NewsletterCommand
|
|
from cli.commands.headings import HeadingsCommand
|
|
from cli.commands.layout import LayoutCommand
|
|
from cli.commands.audit import AuditCommand
|
|
from cli.commands.reset import ResetCommand
|
|
from cli.commands.release import ReleaseCommand
|
|
|
|
console = Console()
|
|
|
|
|
|
def _cmd(text: str) -> str:
|
|
"""Escape command examples before rendering in Rich tables."""
|
|
return _rich_escape(text)
|
|
|
|
|
|
class MLSysBookCLI:
|
|
"""Main CLI application class."""
|
|
|
|
def __init__(self, verbose: bool = False, open_after: bool = False):
|
|
"""Initialize the CLI with all components.
|
|
|
|
Args:
|
|
verbose: If True, stream build output in real-time
|
|
open_after: If True, open build output after successful build
|
|
"""
|
|
self.root_dir = Path.cwd()
|
|
self.verbose = verbose
|
|
self.open_after = open_after
|
|
|
|
# Initialize core components
|
|
self.config_manager = ConfigManager(self.root_dir)
|
|
self.chapter_discovery = ChapterDiscovery(self.config_manager.book_dir)
|
|
|
|
# Initialize command handlers
|
|
self.build_command = BuildCommand(self.config_manager, self.chapter_discovery, verbose=verbose, open_after=open_after)
|
|
self.preview_command = PreviewCommand(self.config_manager, self.chapter_discovery)
|
|
self.doctor_command = DoctorCommand(self.config_manager, self.chapter_discovery)
|
|
self.clean_command = CleanCommand(self.config_manager, self.chapter_discovery)
|
|
self.maintenance_command = MaintenanceCommand(self.config_manager, self.chapter_discovery)
|
|
self.debug_command = DebugCommand(self.config_manager, self.chapter_discovery, verbose=verbose)
|
|
self.validate_command = ValidateCommand(self.config_manager, self.chapter_discovery)
|
|
self.format_command = FormatCommand(self.config_manager, self.chapter_discovery)
|
|
self.info_command = InfoCommand(self.config_manager, self.chapter_discovery)
|
|
self.bib_command = BibCommand(self.config_manager, self.chapter_discovery)
|
|
self.render_command = RenderCommand(self.config_manager, self.chapter_discovery)
|
|
self.newsletter_command = NewsletterCommand(self.config_manager, verbose=verbose)
|
|
self.headings_command = HeadingsCommand(self.config_manager, self.chapter_discovery)
|
|
self.layout_command = LayoutCommand(self.config_manager, self.chapter_discovery)
|
|
self.audit_command = AuditCommand(self.config_manager, self.chapter_discovery)
|
|
self.reset_command = ResetCommand(self.build_command)
|
|
self.release_command = ReleaseCommand(self.config_manager, self.chapter_discovery)
|
|
|
|
def show_banner(self):
|
|
"""Display the CLI banner."""
|
|
banner = Panel(
|
|
"[bold blue]📚 MLSysBook CLI v2.0[/bold blue]\n"
|
|
"[dim]⚡ Modular, maintainable, and fast[/dim]",
|
|
border_style="cyan"
|
|
)
|
|
console.print(banner)
|
|
|
|
def show_help(self):
|
|
"""Display help information."""
|
|
self.show_banner()
|
|
|
|
# Fast Chapter Commands
|
|
fast_table = Table(show_header=True, header_style="bold green", box=None)
|
|
fast_table.add_column("Command", style="green", width=35)
|
|
fast_table.add_column("Description", style="white", width=30)
|
|
fast_table.add_column("Example", style="dim", width=30)
|
|
|
|
fast_table.add_row(_cmd("build [fmt] [chapter[,ch2,...]]"), "Build HTML/PDF/EPUB by format", _cmd("./binder build pdf intro"))
|
|
fast_table.add_row(_cmd("preview [chapter[,ch2,...]]"), "Start live dev server with hot reload", _cmd("./binder preview intro"))
|
|
|
|
# Volume Commands
|
|
vol_table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
vol_table.add_column("Command", style="magenta", width=35)
|
|
vol_table.add_column("Description", style="white", width=30)
|
|
vol_table.add_column("Example", style="dim", width=30)
|
|
|
|
vol_table.add_row(_cmd("build html --vol1"), "Build Volume I website", _cmd("./binder build html --vol1"))
|
|
vol_table.add_row(_cmd("build html --vol2"), "Build Volume II website", _cmd("./binder build html --vol2"))
|
|
vol_table.add_row(_cmd("build pdf --vol1"), "Build Volume I as PDF", _cmd("./binder build pdf --vol1"))
|
|
vol_table.add_row(_cmd("build pdf --vol2"), "Build Volume II as PDF", _cmd("./binder build pdf --vol2"))
|
|
vol_table.add_row(_cmd("build pdf --vol1 --layout"), "Build Volume I PDF, then plan layout fixes", _cmd("./binder build pdf --vol1 --layout"))
|
|
vol_table.add_row(_cmd("build epub --vol1"), "Build Volume I as EPUB", _cmd("./binder build epub --vol1"))
|
|
vol_table.add_row(_cmd("build epub --vol2"), "Build Volume II as EPUB", _cmd("./binder build epub --vol2"))
|
|
vol_table.add_row(_cmd("list --vol1"), "List Volume I chapters", _cmd("./binder list --vol1"))
|
|
vol_table.add_row(_cmd("list --vol2"), "List Volume II chapters", _cmd("./binder list --vol2"))
|
|
|
|
# Full Book Commands
|
|
full_table = Table(show_header=True, header_style="bold blue", box=None)
|
|
full_table.add_column("Command", style="blue", width=35)
|
|
full_table.add_column("Description", style="white", width=30)
|
|
full_table.add_column("Example", style="dim", width=30)
|
|
|
|
full_table.add_row(_cmd("build"), "Build entire book as static HTML", _cmd("./binder build"))
|
|
full_table.add_row(_cmd("build html --all"), "Build ALL chapters using HTML config", _cmd("./binder build html --all"))
|
|
full_table.add_row(_cmd("preview"), "Start live dev server for entire book", _cmd("./binder preview"))
|
|
full_table.add_row(_cmd("build pdf --all"), "Build full book (both volumes)", _cmd("./binder build pdf --all"))
|
|
full_table.add_row(_cmd("build epub --all"), "Build full book (both volumes)", _cmd("./binder build epub --all"))
|
|
|
|
# Quality Commands
|
|
quality_table = Table(show_header=True, header_style="bold yellow", box=None)
|
|
quality_table.add_column("Command", style="yellow", width=38)
|
|
quality_table.add_column("Description", style="white", width=30)
|
|
quality_table.add_column("Example", style="dim", width=28)
|
|
|
|
quality_table.add_row(_cmd("check <group> [--scope ...]"), "Run validation checks", _cmd("./binder check refs"))
|
|
quality_table.add_row(_cmd("check all"), "Run all validation checks", _cmd("./binder check all --vol1"))
|
|
quality_table.add_row(_cmd("check spelling"), "Spell check prose and TikZ", _cmd("./binder check spelling"))
|
|
quality_table.add_row(_cmd("check pdf --vol1|--vol2"), "Verify built PDF cross-refs", _cmd("./binder check pdf --vol1"))
|
|
quality_table.add_row(_cmd("check registry"), "Registry migration gates", _cmd("./binder check registry"))
|
|
quality_table.add_row(_cmd("audit chapter-pdf|html"), "Per-chapter build audit ledger", _cmd("./binder audit chapter-pdf --vol1 training"))
|
|
quality_table.add_row(_cmd("release [--vol1|--vol2]"), "Run release gate and emit structured report", _cmd("./binder release --dry-run --json"))
|
|
quality_table.add_row(_cmd("fix <topic> <action>"), "Fix/manage content", _cmd("./binder fix headers add"))
|
|
quality_table.add_row(_cmd("format <target>"), "Auto-format content", _cmd("./binder format tables"))
|
|
quality_table.add_row(_cmd("info stats [--by-chapter]"), "Book statistics (words, figs, ...)", _cmd("./binder info stats --vol1"))
|
|
quality_table.add_row(_cmd("info figures [--format csv]"), "Extract figure list", _cmd("./binder info figures --vol1"))
|
|
quality_table.add_row(_cmd("info concepts|headers|acronyms"), "Extract concepts, headers, acronyms", _cmd("./binder info concepts --vol1"))
|
|
quality_table.add_row(_cmd("bib mechanical|normalize|sync"), "Bibliography management", _cmd("./binder bib sync --vol1"))
|
|
quality_table.add_row(_cmd("render plots [--vol1|chapter]"), "Render matplotlib plots to PNG gallery", _cmd("./binder render plots --vol1"))
|
|
quality_table.add_row(_cmd("layout --vol1|--vol2"), "Build/reuse PDF and emit auto-layout plan", _cmd("./binder layout --vol1 --no-build"))
|
|
quality_table.add_row(_cmd("layout check <pdf> [--threshold]"), "Flag PDF pages with excessive bottom whitespace", _cmd("./binder layout check book.pdf"))
|
|
quality_table.add_row(_cmd("layout tables --vol1|--vol2"), "Render table-only PDF audit/contact sheets", _cmd("./binder layout tables --vol2"))
|
|
|
|
# Newsletter Commands
|
|
nl_table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
nl_table.add_column("Command", style="magenta", width=38)
|
|
nl_table.add_column("Description", style="white", width=30)
|
|
nl_table.add_column("Example", style="dim", width=28)
|
|
|
|
nl_table.add_row(_cmd("newsletter new <title>"), "Create a new draft", _cmd('./binder newsletter new "Vol2 Update"'))
|
|
nl_table.add_row(_cmd("newsletter list"), "List drafts and sent", _cmd("./binder newsletter list"))
|
|
nl_table.add_row(_cmd("newsletter preview <slug>"), "Preview a draft", _cmd("./binder newsletter preview vol2"))
|
|
nl_table.add_row(_cmd("newsletter publish <slug>"), "Push draft to Buttondown", _cmd("./binder newsletter publish vol2"))
|
|
nl_table.add_row(_cmd("newsletter fetch"), "Pull sent emails for website", _cmd("./binder newsletter fetch"))
|
|
nl_table.add_row(_cmd("newsletter status"), "Subscriber count & recent", _cmd("./binder newsletter status"))
|
|
|
|
# Management Commands
|
|
mgmt_table = Table(show_header=True, header_style="bold blue", box=None)
|
|
mgmt_table.add_column("Command", style="green", width=38)
|
|
mgmt_table.add_column("Description", style="white", width=30)
|
|
mgmt_table.add_column("Example", style="dim", width=28)
|
|
|
|
mgmt_table.add_row(_cmd("debug <fmt> --vol1|--vol2"), "Find failing chapter + section", _cmd("./binder debug pdf --vol1"))
|
|
mgmt_table.add_row(_cmd("reset <fmt|all> [--vol1|--vol2]"), "Reset build YAML configs", _cmd("./binder reset pdf --vol1"))
|
|
mgmt_table.add_row(_cmd("clean [html|pdf|epub|artifacts]"), "Clean generated artifacts", _cmd("./binder clean artifacts"))
|
|
mgmt_table.add_row(_cmd("switch <format>"), "Switch active config", _cmd("./binder switch pdf"))
|
|
mgmt_table.add_row(_cmd("list"), "List available chapters", _cmd("./binder list"))
|
|
mgmt_table.add_row(_cmd("status"), "Show current config status", _cmd("./binder status"))
|
|
mgmt_table.add_row(_cmd("doctor"), "Run comprehensive health check", _cmd("./binder doctor"))
|
|
mgmt_table.add_row(_cmd("setup"), "Setup development environment", _cmd("./binder setup"))
|
|
mgmt_table.add_row(_cmd("help"), "Show this help", _cmd("./binder help"))
|
|
|
|
# Display tables
|
|
console.print(Panel(fast_table, title="⚡ Fast Chapter Commands", border_style="green"))
|
|
console.print(Panel(vol_table, title="📖 Volume Commands", border_style="magenta"))
|
|
console.print(Panel(full_table, title="📚 Full Book Commands", border_style="blue"))
|
|
console.print(Panel(quality_table, title="✅ Quality & Formatting", border_style="yellow"))
|
|
console.print(Panel(nl_table, title="📬 Newsletter", border_style="magenta"))
|
|
console.print(Panel(mgmt_table, title="🔧 Management", border_style="cyan"))
|
|
|
|
# Pro Tips
|
|
examples = Text()
|
|
examples.append("🎯 Modular CLI Examples:\n", style="bold magenta")
|
|
examples.append(" ./binder build pdf --vol1 ", style="cyan")
|
|
examples.append("# Build Volume I as PDF\n", style="dim")
|
|
examples.append(" ./binder build pdf --vol2 ", style="cyan")
|
|
examples.append("# Build Volume II as PDF\n", style="dim")
|
|
examples.append(" ./binder build pdf vol1/intro ", style="cyan")
|
|
examples.append("# Build specific chapter (disambiguate with vol prefix)\n", style="dim")
|
|
examples.append(" ./binder build pdf --all ", style="cyan")
|
|
examples.append("# Build entire book as PDF (both volumes)\n", style="dim")
|
|
examples.append(" ./binder list --vol1 ", style="cyan")
|
|
examples.append("# List only Volume I chapters\n", style="dim")
|
|
|
|
console.print(Panel(examples, title="💡 Pro Tips", border_style="magenta"))
|
|
|
|
# Global Options
|
|
options_text = Text()
|
|
options_text.append("🔧 Global Options:\n", style="bold yellow")
|
|
options_text.append(" -v, --verbose ", style="yellow")
|
|
options_text.append("Stream build output in real-time\n", style="dim")
|
|
options_text.append(" -o, --open ", style="yellow")
|
|
options_text.append("Open output after successful build\n", style="dim")
|
|
options_text.append(" Example: ", style="dim")
|
|
options_text.append("./binder build pdf --vol1 -v -o", style="cyan")
|
|
|
|
console.print(Panel(options_text, title="⚙️ Options", border_style="yellow"))
|
|
|
|
def _parse_build_args(self, args):
|
|
"""Parse `binder build` arguments into format, scope, and targets."""
|
|
format_type = None
|
|
volume = None
|
|
build_all = False
|
|
skip_hygiene = False
|
|
skip_validate = False
|
|
layout_after = False
|
|
remaining = []
|
|
|
|
for arg in args:
|
|
lower = arg.lower()
|
|
if lower == "--vol1":
|
|
volume = "vol1"
|
|
elif lower == "--vol2":
|
|
volume = "vol2"
|
|
elif lower == "--all":
|
|
build_all = True
|
|
elif lower == "--skip-hygiene":
|
|
# Emergency bypass for the EPUB pre-render hygiene check
|
|
# added in the fix/epub-issues work. See
|
|
# BuildCommand._preflight_epub_hygiene for context.
|
|
skip_hygiene = True
|
|
elif lower == "--skip-validate":
|
|
# Bypass the post-render smoke + epubcheck validation
|
|
# added in the fix/epub-issues work. See
|
|
# BuildCommand._postflight_epub_validation for context.
|
|
# Use when iterating on a known-broken build or when
|
|
# Java / epubcheck is unavailable locally.
|
|
skip_validate = True
|
|
elif lower == "--layout":
|
|
layout_after = True
|
|
elif format_type is None and lower in ("html", "pdf", "epub"):
|
|
format_type = lower
|
|
else:
|
|
remaining.append(arg)
|
|
|
|
if format_type is None:
|
|
format_type = "html"
|
|
|
|
chapters_arg = remaining[0] if remaining else None
|
|
return (
|
|
format_type,
|
|
volume,
|
|
build_all,
|
|
chapters_arg,
|
|
skip_hygiene,
|
|
skip_validate,
|
|
layout_after,
|
|
)
|
|
|
|
def handle_build_command(self, args):
|
|
"""Handle unified build command.
|
|
|
|
Usage:
|
|
./binder build
|
|
./binder build pdf
|
|
./binder build epub --vol2
|
|
./binder build html intro,frameworks
|
|
"""
|
|
if args and args[0].lower() == "reset":
|
|
console.print("[red]`binder build reset` was removed.[/red]")
|
|
console.print("[yellow]Use: ./binder reset <html|pdf|epub|all> [--vol1|--vol2][/yellow]")
|
|
return False
|
|
|
|
if "-h" in args or "--help" in args:
|
|
console.print("Usage: ./binder build [html|pdf|epub] [chapters] [--vol1|--vol2|--all] [--skip-hygiene] [--skip-validate] [--layout]", markup=False)
|
|
console.print("[dim]Build renders source artifacts. For PDF layout polish, add --layout to a full-volume PDF build.[/dim]")
|
|
console.print("[dim]Examples:[/dim]")
|
|
console.print("[dim] ./binder build[/dim]")
|
|
console.print("[dim] ./binder build pdf[/dim]")
|
|
console.print("[dim] ./binder build pdf intro,training --vol1[/dim]")
|
|
console.print("[dim] ./binder build html --all[/dim]")
|
|
console.print("[dim] ./binder build epub --vol1[/dim]")
|
|
console.print("[dim] ./binder build pdf --vol1 --layout # render Vol I, then emit auto-layout plan[/dim]")
|
|
console.print("[dim] ./binder build pdf --vol2 --layout # render Vol II, then emit auto-layout plan[/dim]")
|
|
console.print("[dim] ./binder build epub --vol1 --skip-hygiene # bypass pre-render hygiene check[/dim]")
|
|
console.print("[dim] ./binder build epub --vol1 --skip-validate # bypass post-render validation[/dim]")
|
|
console.print("[dim] ./binder build pdf --vol1 # runs pdftotext cross-ref scan after render[/dim]")
|
|
console.print("[dim]Layout rule: --layout is accepted only for `build pdf --vol1|--vol2`; it runs the same planner as `binder layout --vol1|--vol2 --no-build`.[/dim]")
|
|
return True
|
|
|
|
self.config_manager.show_symlink_status()
|
|
(
|
|
format_type,
|
|
volume,
|
|
build_all,
|
|
chapters_arg,
|
|
skip_hygiene,
|
|
skip_validate,
|
|
layout_after,
|
|
) = self._parse_build_args(args)
|
|
|
|
if build_all and chapters_arg:
|
|
console.print("[red]❌ Cannot combine explicit chapters with --all[/red]")
|
|
return False
|
|
|
|
if layout_after and (
|
|
format_type != "pdf" or not volume or build_all or chapters_arg
|
|
):
|
|
console.print(
|
|
"[red]❌ `--layout` is supported for full-volume PDF builds only.[/red]"
|
|
)
|
|
console.print(
|
|
"[yellow]Use: ./binder build pdf --vol1 --layout "
|
|
"or ./binder build pdf --vol2 --layout[/yellow]"
|
|
)
|
|
return False
|
|
|
|
if build_all:
|
|
if format_type == "html":
|
|
console.print("[green]🌐 Building HTML with ALL chapters...[/green]")
|
|
return self.build_command.build_html_only()
|
|
console.print(f"[green]🏗️ Building entire book ({format_type.upper()})...[/green]")
|
|
return self.build_command.build_full(format_type, skip_hygiene=skip_hygiene, skip_validate=skip_validate)
|
|
|
|
if volume and not chapters_arg:
|
|
volume_name = "Volume I" if volume == "vol1" else "Volume II"
|
|
console.print(f"[magenta]🏗️ Building {volume_name} ({format_type.upper()})...[/magenta]")
|
|
ok = self.build_command.build_volume(
|
|
volume,
|
|
format_type,
|
|
skip_hygiene=skip_hygiene,
|
|
skip_validate=skip_validate,
|
|
)
|
|
if ok and layout_after:
|
|
return self.layout_command.run([f"--{volume}", "--no-build"])
|
|
return ok
|
|
|
|
if volume and chapters_arg:
|
|
chapter_list = [ch.strip() for ch in chapters_arg.split(",")]
|
|
console.print(f"[green]🏗️ Building {format_type.upper()} chapters in {volume}: {chapters_arg}[/green]")
|
|
return self.build_command.build_chapters_with_volume(chapter_list, format_type, volume, skip_hygiene=skip_hygiene, skip_validate=skip_validate)
|
|
|
|
if chapters_arg:
|
|
chapter_list = [ch.strip() for ch in chapters_arg.split(",")]
|
|
console.print(f"[green]🏗️ Building {format_type.upper()} chapter(s): {chapters_arg}[/green]")
|
|
if format_type == "html":
|
|
return self.build_command.build_html_only(chapter_list)
|
|
return self.build_command.build_chapters(chapter_list, format_type, skip_hygiene=skip_hygiene, skip_validate=skip_validate)
|
|
|
|
console.print(f"[green]🏗️ Building entire book ({format_type.upper()})...[/green]")
|
|
if format_type == "html":
|
|
return self.build_command.build_full("html")
|
|
return self.build_command.build_full(format_type, skip_hygiene=skip_hygiene, skip_validate=skip_validate)
|
|
|
|
def handle_preview_command(self, args):
|
|
"""Handle preview command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder preview [chapter]", markup=False)
|
|
console.print("[dim]Examples:[/dim]")
|
|
console.print("[dim] ./binder preview[/dim]")
|
|
console.print("[dim] ./binder preview vol1/training[/dim]")
|
|
return True
|
|
|
|
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."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder doctor", markup=False)
|
|
console.print("[dim]Run comprehensive local tooling and repository health checks.[/dim]")
|
|
return True
|
|
return self.doctor_command.run_health_check()
|
|
|
|
def handle_clean_command(self, args):
|
|
"""Handle clean command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
self.clean_command.print_help()
|
|
return True
|
|
if len(args) > 0:
|
|
target = args[0].lower()
|
|
if target in ["html", "pdf", "epub"]:
|
|
return self.clean_command.clean_format(target)
|
|
elif target == "artifacts":
|
|
return self.clean_command.clean_artifacts()
|
|
else:
|
|
console.print(f"[red]❌ Unknown clean target: {target}[/red]")
|
|
console.print("[yellow]💡 Available: html, pdf, epub, artifacts[/yellow]")
|
|
return False
|
|
else:
|
|
# Clean all
|
|
return self.clean_command.clean_all()
|
|
|
|
def handle_switch_command(self, args):
|
|
"""Handle switch command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder switch <html|pdf|epub>", markup=False)
|
|
return True
|
|
if len(args) < 1:
|
|
console.print("[red]❌ Usage: ./binder switch <format>[/red]")
|
|
console.print("[yellow]💡 Available formats: html, pdf, epub[/yellow]")
|
|
return False
|
|
|
|
format_type = args[0].lower()
|
|
return self.maintenance_command.switch_format(format_type)
|
|
|
|
def handle_setup_command(self, args):
|
|
"""Handle setup command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder setup", markup=False)
|
|
console.print("[dim]Install/setup local development dependencies and pre-commit hooks.[/dim]")
|
|
return True
|
|
return self.maintenance_command.setup_environment()
|
|
|
|
def handle_hello_command(self, args):
|
|
"""Handle hello command."""
|
|
return self.maintenance_command.show_hello()
|
|
|
|
def handle_about_command(self, args):
|
|
"""Handle about command."""
|
|
return self.maintenance_command.show_about()
|
|
|
|
def handle_audit_command(self, args):
|
|
"""Handle audit command group (chapter-level build audits)."""
|
|
return self.audit_command.run(args)
|
|
|
|
def handle_check_command(self, args):
|
|
"""Handle check (validation) command group."""
|
|
return self.validate_command.run(args)
|
|
|
|
def handle_fix_command(self, args):
|
|
"""Handle fix (maintenance) namespace command."""
|
|
return self.maintenance_command.run_namespace(args)
|
|
|
|
def handle_format_command(self, args):
|
|
"""Handle format command group."""
|
|
return self.format_command.run(args)
|
|
|
|
def handle_info_command(self, args):
|
|
"""Handle info command group (stats, figures)."""
|
|
return self.info_command.run(args)
|
|
|
|
def handle_bib_command(self, args):
|
|
"""Handle bib command group (list, clean, update, sync)."""
|
|
return self.bib_command.run(args)
|
|
|
|
def handle_render_command(self, args):
|
|
"""Handle render command group (plots)."""
|
|
return self.render_command.run(args)
|
|
|
|
def handle_newsletter_command(self, args):
|
|
"""Handle newsletter command group (new, list, preview, publish, fetch, status)."""
|
|
return self.newsletter_command.run(args)
|
|
|
|
def handle_headings_command(self, args):
|
|
"""Handle headings command group (check, dry-run, apply)."""
|
|
return self.headings_command.run(args)
|
|
|
|
def handle_layout_command(self, args):
|
|
"""Handle layout command group (check)."""
|
|
return self.layout_command.run(args)
|
|
|
|
def handle_reset_command(self, args):
|
|
"""Handle reset command group."""
|
|
return self.reset_command.run(args)
|
|
|
|
def handle_release_command(self, args):
|
|
"""Handle release orchestration."""
|
|
return self.release_command.run(args)
|
|
|
|
|
|
def handle_debug_command(self, args):
|
|
"""Handle debug command.
|
|
|
|
Usage:
|
|
./binder debug pdf --vol1
|
|
./binder debug html --vol2 --chapter training
|
|
"""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder debug <pdf|html|epub> --vol1|--vol2 [--chapter <name>]", markup=False)
|
|
console.print("[dim]Examples:[/dim]")
|
|
console.print("[dim] ./binder debug pdf --vol1[/dim]")
|
|
console.print("[dim] ./binder debug html --vol2 --chapter training[/dim]")
|
|
return True
|
|
|
|
# Parse args: first positional is format, then flags
|
|
format_type = None
|
|
volume = None
|
|
chapter = None
|
|
|
|
i = 0
|
|
while i < len(args):
|
|
arg = args[i]
|
|
if arg == "--vol1":
|
|
volume = "vol1"
|
|
elif arg == "--vol2":
|
|
volume = "vol2"
|
|
elif arg == "--chapter" and i + 1 < len(args):
|
|
i += 1
|
|
chapter = args[i]
|
|
elif arg in ("pdf", "html", "epub") and format_type is None:
|
|
format_type = arg
|
|
else:
|
|
# Try as format or chapter
|
|
if format_type is None and arg in ("pdf", "html", "epub"):
|
|
format_type = arg
|
|
else:
|
|
console.print(f"[red]Unknown argument: {arg}[/red]")
|
|
return False
|
|
i += 1
|
|
|
|
if not format_type:
|
|
format_type = "pdf" # Default to PDF
|
|
if not volume:
|
|
console.print("[red]Please specify --vol1 or --vol2[/red]")
|
|
console.print("[yellow]Usage: ./binder debug <pdf|html|epub> --vol1|--vol2 [--chapter <name>][/yellow]")
|
|
return False
|
|
|
|
return self.debug_command.debug_build(format_type, volume, chapter)
|
|
|
|
def handle_list_command(self, args):
|
|
"""Handle list chapters command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder list [--vol1|--vol2]", markup=False)
|
|
return True
|
|
volume = None
|
|
if len(args) > 0:
|
|
if args[0] == "--vol1":
|
|
volume = "vol1"
|
|
elif args[0] == "--vol2":
|
|
volume = "vol2"
|
|
|
|
self.chapter_discovery.show_chapters(volume=volume)
|
|
return True
|
|
|
|
def handle_status_command(self, args):
|
|
"""Handle status command."""
|
|
if args and args[0].lower() in ("help", "-h", "--help"):
|
|
console.print("Usage: ./binder status", markup=False)
|
|
return True
|
|
console.print("[bold blue]📊 MLSysBook CLI Status[/bold blue]")
|
|
console.print(f"[dim]Root directory: {self.root_dir}[/dim]")
|
|
console.print(f"[dim]Book directory: {self.config_manager.book_dir}[/dim]")
|
|
|
|
# Show config status
|
|
self.config_manager.show_symlink_status()
|
|
|
|
# Show chapter count
|
|
chapters = self.chapter_discovery.get_all_chapters()
|
|
console.print(f"[dim]Available chapters: {len(chapters)}[/dim]")
|
|
|
|
return True
|
|
|
|
def run(self, args):
|
|
"""Run the CLI with given arguments."""
|
|
if len(args) < 1:
|
|
self.show_help()
|
|
return True
|
|
|
|
command = args[0].lower()
|
|
command_args = args[1:]
|
|
|
|
rich_help_commands = {
|
|
"audit", "bib", "clean", "fix", "format", "headings", "info",
|
|
"maintain", "newsletter", "render", "reset",
|
|
}
|
|
if command != "check" and command not in rich_help_commands and command_args == ["help"]:
|
|
command_args = ["--help"]
|
|
|
|
# Command mapping
|
|
commands = {
|
|
"build": self.handle_build_command,
|
|
"preview": self.handle_preview_command,
|
|
"clean": self.handle_clean_command,
|
|
"debug": self.handle_debug_command,
|
|
"switch": self.handle_switch_command,
|
|
"list": self.handle_list_command,
|
|
"status": self.handle_status_command,
|
|
"doctor": self.handle_doctor_command,
|
|
"audit": self.handle_audit_command,
|
|
"check": self.handle_check_command,
|
|
"fix": self.handle_fix_command,
|
|
"format": self.handle_format_command,
|
|
"info": self.handle_info_command,
|
|
"bib": self.handle_bib_command,
|
|
"headings": self.handle_headings_command,
|
|
"layout": self.handle_layout_command,
|
|
"render": self.handle_render_command,
|
|
"reset": self.handle_reset_command,
|
|
"release": self.handle_release_command,
|
|
"newsletter": self.handle_newsletter_command,
|
|
"setup": self.handle_setup_command,
|
|
"hello": self.handle_hello_command,
|
|
"about": self.handle_about_command,
|
|
# Aliases (backward compat)
|
|
"validate": self.handle_check_command,
|
|
"maintain": self.handle_fix_command,
|
|
|
|
"help": lambda args: self.show_help() or True,
|
|
}
|
|
|
|
# Deprecated short aliases (hard-cut cleanup).
|
|
deprecated_aliases = {
|
|
"b": "build",
|
|
"p": "preview",
|
|
"l": "list",
|
|
"s": "status",
|
|
"d": "doctor",
|
|
"h": "help",
|
|
}
|
|
if command in deprecated_aliases:
|
|
full = deprecated_aliases[command]
|
|
console.print(f"[red]❌ Alias '{command}' was removed.[/red]")
|
|
console.print(f"[yellow]💡 Use: ./binder {full} ...[/yellow]")
|
|
return False
|
|
|
|
if command in ("html", "pdf", "epub"):
|
|
console.print(f"[red]Top-level '{command}' commands were removed.[/red]")
|
|
console.print(f"[yellow]Build with: ./binder build {command} ...[/yellow]")
|
|
console.print(f"[yellow]Reset YAML with: ./binder reset {command} [--vol1|--vol2][/yellow]")
|
|
return False
|
|
|
|
if command in commands:
|
|
try:
|
|
return commands[command](command_args)
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]👋 Goodbye![/yellow]")
|
|
return False
|
|
except Exception as e:
|
|
console.print(f"[red]❌ Error: {e}[/red]")
|
|
return False
|
|
else:
|
|
console.print(f"[red]❌ Unknown command: {command}[/red]")
|
|
console.print("[yellow]💡 Use './binder help' to see available commands[/yellow]")
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
# Check for global flags
|
|
args = sys.argv[1:]
|
|
verbose = False
|
|
open_after = False
|
|
|
|
if "-v" in args:
|
|
verbose = True
|
|
args.remove("-v")
|
|
elif "--verbose" in args:
|
|
verbose = True
|
|
args.remove("--verbose")
|
|
|
|
if "-o" in args:
|
|
open_after = True
|
|
args.remove("-o")
|
|
elif "--open" in args:
|
|
open_after = True
|
|
args.remove("--open")
|
|
|
|
cli = MLSysBookCLI(verbose=verbose, open_after=open_after)
|
|
success = cli.run(args)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|