| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- from abc import ABC
- from pathlib import Path
- from typing import List, Optional, Dict, Any
- import logging
- import yaml
- from typer import Typer, Option, Argument
- from rich.console import Console
- from .config import get_config
- from .exceptions import TemplateNotFoundError, TemplateValidationError
- from .library import LibraryManager
- from .prompt import SimplifiedPromptHandler
- logger = logging.getLogger('boilerplates')
- console = Console() # Single shared console instance
- class Module(ABC):
- """Streamlined base module that auto-detects variables from templates."""
-
- # Required class attributes for subclasses
- name = None
- description = None
- files = None
-
- def __init__(self):
- if not all([self.name, self.description, self.files]):
- raise ValueError(
- f"Module {self.__class__.__name__} must define name, description, and files"
- )
-
- self.libraries = LibraryManager()
- self.metadata = self._load_metadata()
-
- def _load_metadata(self) -> Dict[str, Any]:
- """Load module metadata from .meta.yaml file if it exists."""
- import inspect
- # Get the path to the actual module file
- module_path = Path(inspect.getfile(self.__class__))
- meta_file = module_path.with_suffix('.meta.yaml')
-
- if meta_file.exists():
- try:
- with open(meta_file, 'r') as f:
- return yaml.safe_load(f) or {}
- except Exception as e:
- logger.debug(f"Failed to load metadata for {self.name}: {e}")
-
- return {}
- def list(self):
- """List all templates."""
- templates = self.libraries.find(self.name, self.files, sorted=True)
- for template in templates:
- console.print(f"[cyan]{template.id}[/cyan] - {template.name}")
- return templates
- def show(self, id: str = Argument(..., help="Template ID")):
- """Show template details."""
- logger.debug(f"Showing template: {id}")
-
- template = self._get_template(id)
- if not template:
- return
-
- # Header
- version = f" v{template.version}" if template.version else ""
- console.print(f"[bold magenta]{template.name} ({template.id}{version})[/bold magenta]")
- console.print(f"[dim white]{template.description}[/dim white]\n")
-
- # Metadata (only print if exists)
- metadata = [
- ("Author", template.author),
- ("Date", template.date),
- ("Tags", ', '.join(template.tags) if template.tags else None)
- ]
-
- for label, value in metadata:
- if value:
- console.print(f"{label}: [cyan]{value}[/cyan]")
-
- # Variables
- if template.vars:
- console.print(f"Variables: [cyan]{', '.join(sorted(template.vars))}[/cyan]")
-
- # Content
- if template.content:
- console.print(f"\n{template.content}")
-
- def _get_template(self, template_id: str, raise_on_missing: bool = False):
- """Get template by ID with unified error handling."""
- template = self.libraries.find_by_id(self.name, self.files, template_id)
-
- if not template:
- logger.error(f"Template '{template_id}' not found")
- if raise_on_missing:
- raise TemplateNotFoundError(template_id, self.name)
- console.print(f"[red]Template '{template_id}' not found[/red]")
-
- return template
- def generate(
- self,
- id: str = Argument(..., help="Template ID"),
- out: Optional[Path] = Option(None, "--out", "-o")
- ):
- """Generate from template."""
- logger.debug(f"Generating template: {id}")
-
- template = self._get_template(id, raise_on_missing=True)
-
- # Validate template (will raise TemplateValidationError if validation fails)
- self._validate_template(template, id)
-
- # Process variables and render
- values = self._process_variables(template)
-
- try:
- content = template.render(values)
- except Exception as e:
- logger.error(f"Failed to render template: {e}")
- raise
-
- # Output result
- if out:
- out.parent.mkdir(parents=True, exist_ok=True)
- out.write_text(content)
- console.print(f"[green]✅ Generated to {out}[/green]")
- else:
- console.print(f"\n{'='*60}\nGenerated Content:\n{'='*60}")
- console.print(content)
-
- def _validate_template(self, template, template_id: str) -> None:
- """Validate template and raise error if validation fails."""
- # Template is now self-validating, no need for registered variables
- warnings = template.validate()
-
- # If there are non-critical warnings, log them but don't fail
- if warnings:
- logger.warning(f"Template '{template_id}' has validation warnings: {warnings}")
-
- def _process_variables(self, template) -> Dict[str, Any]:
- """Process template variables with prompting."""
- # Use template's analyzed variables
- if not template.variables:
- return {}
-
- # Apply metadata to variables
- self._apply_metadata_to_variables(template.variables, template.variable_metadata)
-
- # Collect defaults from analyzed variables
- defaults = {}
- for var_name, var in template.variables.items():
- if var.default is not None:
- defaults[var_name] = var.default
-
- # Use rich output if enabled
- if not get_config().use_rich_output:
- # Simple fallback - just prompt for missing values
- values = defaults.copy()
- for var_name, var in template.variables.items():
- if var_name not in values:
- values[var_name] = input(f"Enter {var_name}: ")
- return values
-
- # Pass metadata to prompt handler
- prompt_handler = SimplifiedPromptHandler(template.variables)
- prompt_handler.category_metadata = self.metadata.get('categories', {})
- return prompt_handler()
-
- def _apply_metadata_to_variables(self, variables: Dict[str, Any], template_metadata: Dict[str, Any]):
- """Apply metadata from module and template to variables."""
- # First apply module metadata
- module_var_metadata = self.metadata.get('variables', {})
- for var_name, var in variables.items():
- if var_name in module_var_metadata:
- meta = module_var_metadata[var_name]
- if 'hint' in meta and not var.hint:
- var.hint = meta['hint']
- if 'description' in meta and not var.description:
- var.description = meta['description']
- if 'tip' in meta and not var.tip:
- var.tip = meta['tip']
- if 'validation' in meta and not var.validation:
- var.validation = meta['validation']
- if 'icon' in meta and not var.icon:
- var.icon = meta['icon']
-
- # Then apply template metadata (overrides module metadata)
- for var_name, var in variables.items():
- if var_name in template_metadata:
- meta = template_metadata[var_name]
- if 'hint' in meta:
- var.hint = meta['hint']
- if 'description' in meta:
- var.description = meta['description']
- if 'tip' in meta:
- var.tip = meta['tip']
- if 'validation' in meta:
- var.validation = meta['validation']
- if 'icon' in meta:
- var.icon = meta['icon']
-
- def register_cli(self, app: Typer):
- """Register module commands with the main app."""
- module_app = Typer()
- module_app.command()(self.list)
- module_app.command()(self.show)
- module_app.command()(self.generate)
- app.add_typer(module_app, name=self.name, help=self.description)
|