#!/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 [--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 "), "Fix/manage content", _cmd("./binder fix headers add")) quality_table.add_row(_cmd("format "), "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 [--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 "), "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()