from __future__ import annotations from typing import Any, Callable, Dict, Iterable, Sequence from importlib import import_module as _import_module # A cmdlet is a callable taking (result, args, config) -> int Cmdlet = Callable[[Any, Sequence[str], Dict[str, Any]], int] # Registry of command-name -> cmdlet function REGISTRY: Dict[str, Cmdlet] = {} def register(names: Iterable[str]): """Decorator to register a function under one or more command names. Usage: @register(["add-tag", "add-tags"]) def _run(result, args, config) -> int: ... """ def _wrap(fn: Cmdlet) -> Cmdlet: for name in names: REGISTRY[name.replace('_', '-').lower()] = fn return fn return _wrap class AutoRegister: """Decorator that automatically registers a cmdlet function using CMDLET.aliases. Usage: CMDLET = Cmdlet( name="delete-file", aliases=["del", "del-file"], ... ) @AutoRegister(CMDLET) def _run(result, args, config) -> int: ... Registers the cmdlet under: - Its main name from CMDLET.name - All aliases from CMDLET.aliases This allows the help display to show: "cmd: delete-file | alias: del, del-file" """ def __init__(self, cmdlet): self.cmdlet = cmdlet def __call__(self, fn: Cmdlet) -> Cmdlet: """Register fn for the main name and all aliases in cmdlet.""" normalized_name = None # Register for main name first if hasattr(self.cmdlet, 'name') and self.cmdlet.name: normalized_name = self.cmdlet.name.replace('_', '-').lower() REGISTRY[normalized_name] = fn # Register for all aliases if hasattr(self.cmdlet, 'aliases') and self.cmdlet.aliases: for alias in self.cmdlet.aliases: normalized_alias = alias.replace('_', '-').lower() # Always register (aliases are separate from main name) REGISTRY[normalized_alias] = fn return fn def get(cmd_name: str) -> Cmdlet | None: return REGISTRY.get(cmd_name.replace('_', '-').lower()) def format_cmd_help(cmdlet) -> str: """Format a cmdlet for help display showing cmd:name and aliases. Example output: "delete-file | aliases: del, del-file" """ if not hasattr(cmdlet, 'name'): return str(cmdlet) cmd_str = f"cmd: {cmdlet.name}" if hasattr(cmdlet, 'aliases') and cmdlet.aliases: aliases_str = ", ".join(cmdlet.aliases) cmd_str += f" | aliases: {aliases_str}" return cmd_str # Dynamically import all cmdlet modules in this directory (ignore files starting with _ and __init__.py) import os cmdlet_dir = os.path.dirname(__file__) for filename in os.listdir(cmdlet_dir): if ( filename.endswith(".py") and not filename.startswith("_") and filename != "__init__.py" ): mod_name = filename[:-3] try: module = _import_module(f".{mod_name}", __name__) # Auto-register based on CMDLET object with exec function # This allows cmdlets to be fully self-contained in the CMDLET object if hasattr(module, 'CMDLET'): cmdlet_obj = module.CMDLET # Get the execution function from the CMDLET object run_fn = getattr(cmdlet_obj, 'exec', None) if hasattr(cmdlet_obj, 'exec') else None if callable(run_fn): # Register main name if hasattr(cmdlet_obj, 'name') and cmdlet_obj.name: normalized_name = cmdlet_obj.name.replace('_', '-').lower() REGISTRY[normalized_name] = run_fn # Register all aliases if hasattr(cmdlet_obj, 'aliases') and cmdlet_obj.aliases: for alias in cmdlet_obj.aliases: normalized_alias = alias.replace('_', '-').lower() REGISTRY[normalized_alias] = run_fn except Exception: continue # Import root-level modules that also register cmdlets # Note: search_libgen, search_soulseek, and search_debrid are now consolidated into search_provider.py # Use search-file -provider libgen, -provider soulseek, or -provider debrid instead for _root_mod in ("select_cmdlet",): try: _import_module(_root_mod) except Exception: # Allow missing optional modules continue # Also import helper modules that register cmdlets try: import helper.alldebrid as _alldebrid except Exception: pass