Ver Fonte

hotfix/v0.0.5

xcad há 4 meses atrás
pai
commit
7b98d03e20

+ 98 - 1
cli/core/display.py

@@ -13,6 +13,7 @@ if TYPE_CHECKING:
 
 logger = logging.getLogger(__name__)
 console = Console()
+console_err = Console(stderr=True)
 
 
 class IconManager:
@@ -160,6 +161,14 @@ class DisplayManager:
     - External code should never directly call IconManager or console.print
     - Consistent formatting across all display types
     """
+    
+    def __init__(self, quiet: bool = False):
+        """Initialize DisplayManager.
+        
+        Args:
+            quiet: If True, suppress all non-error output
+        """
+        self.quiet = quiet
 
     def display_templates_table(
         self, templates: list, module_name: str, title: str
@@ -220,6 +229,18 @@ class DisplayManager:
             message: The message to display
             context: Optional context information
         """
+        # Errors and warnings always go to stderr, even in quiet mode
+        # Success and info respect quiet mode and go to stdout
+        if level in ('error', 'warning'):
+            output_console = console_err
+            should_print = True
+        else:
+            output_console = console
+            should_print = not self.quiet
+        
+        if not should_print:
+            return
+        
         icon = IconManager.get_status_icon(level)
         colors = {'error': 'red', 'warning': 'yellow', 'success': 'green', 'info': 'blue'}
         color = colors.get(level, 'white')
@@ -230,7 +251,7 @@ class DisplayManager:
         else:
             text = f"{level.capitalize()}: {message}" if level == 'error' or level == 'warning' else message
         
-        console.print(f"[{color}]{icon} {text}[/{color}]")
+        output_console.print(f"[{color}]{icon} {text}[/{color}]")
         
         # Log appropriately
         log_message = f"{context}: {message}" if context else message
@@ -666,3 +687,79 @@ class DisplayManager:
             'arrow': IconManager.arrow_right(),
         }
         return icon_map.get(icon_type, '')
+    
+    def display_template_render_error(self, error: 'TemplateRenderError', context: str | None = None) -> None:
+        """Display a detailed template rendering error with context and suggestions.
+        
+        Args:
+            error: TemplateRenderError exception with detailed error information
+            context: Optional context information (e.g., template ID)
+        """
+        from rich.panel import Panel
+        from rich.syntax import Syntax
+        
+        # Always display errors to stderr
+        # Display main error header
+        icon = IconManager.get_status_icon('error')
+        if context:
+            console_err.print(f"\n[red bold]{icon} Template Rendering Error[/red bold] [dim]({context})[/dim]")
+        else:
+            console_err.print(f"\n[red bold]{icon} Template Rendering Error[/red bold]")
+        
+        console_err.print()
+        
+        # Display error message
+        if error.file_path:
+            console_err.print(f"[red]Error in file:[/red] [cyan]{error.file_path}[/cyan]")
+            if error.line_number:
+                location = f"Line {error.line_number}"
+                if error.column:
+                    location += f", Column {error.column}"
+                console_err.print(f"[red]Location:[/red] {location}")
+        
+        console_err.print(f"[red]Message:[/red] {str(error.original_error) if error.original_error else str(error)}")
+        console_err.print()
+        
+        # Display code context if available
+        if error.context_lines:
+            console_err.print("[bold cyan]Code Context:[/bold cyan]")
+            
+            # Build the context text
+            context_text = "\n".join(error.context_lines)
+            
+            # Display in a panel with syntax highlighting if possible
+            file_ext = Path(error.file_path).suffix if error.file_path else ""
+            if file_ext == ".j2":
+                # Remove .j2 to get base extension for syntax highlighting
+                base_name = Path(error.file_path).stem
+                base_ext = Path(base_name).suffix
+                lexer = "jinja2" if not base_ext else None
+            else:
+                lexer = None
+            
+            try:
+                if lexer:
+                    syntax = Syntax(context_text, lexer, line_numbers=False, theme="monokai")
+                    console_err.print(Panel(syntax, border_style="red", padding=(1, 2)))
+                else:
+                    console_err.print(Panel(context_text, border_style="red", padding=(1, 2)))
+            except Exception:
+                # Fallback to plain panel if syntax highlighting fails
+                console_err.print(Panel(context_text, border_style="red", padding=(1, 2)))
+            
+            console_err.print()
+        
+        # Display suggestions if available
+        if error.suggestions:
+            console_err.print("[bold yellow]Suggestions:[/bold yellow]")
+            for i, suggestion in enumerate(error.suggestions, 1):
+                bullet = IconManager.UI_BULLET
+                console_err.print(f"  [yellow]{bullet}[/yellow] {suggestion}")
+            console_err.print()
+        
+        # Display variable context in debug mode
+        if error.variable_context:
+            console_err.print("[bold blue]Available Variables (Debug):[/bold blue]")
+            var_list = ", ".join(sorted(error.variable_context.keys()))
+            console_err.print(f"[dim]{var_list}[/dim]")
+            console_err.print()

+ 33 - 2
cli/core/exceptions.py

@@ -4,7 +4,7 @@ This module defines specific exception types for better error handling
 and diagnostics throughout the application.
 """
 
-from typing import Optional, List
+from typing import Optional, List, Dict
 
 
 class BoilerplatesError(Exception):
@@ -61,7 +61,38 @@ class TemplateValidationError(TemplateError):
 
 class TemplateRenderError(TemplateError):
     """Raised when template rendering fails."""
-    pass
+    
+    def __init__(
+        self,
+        message: str,
+        file_path: Optional[str] = None,
+        line_number: Optional[int] = None,
+        column: Optional[int] = None,
+        context_lines: Optional[List[str]] = None,
+        variable_context: Optional[Dict[str, str]] = None,
+        suggestions: Optional[List[str]] = None,
+        original_error: Optional[Exception] = None
+    ):
+        self.file_path = file_path
+        self.line_number = line_number
+        self.column = column
+        self.context_lines = context_lines or []
+        self.variable_context = variable_context or {}
+        self.suggestions = suggestions or []
+        self.original_error = original_error
+        
+        # Build enhanced error message
+        parts = [message]
+        
+        if file_path:
+            location = f"File: {file_path}"
+            if line_number:
+                location += f", Line: {line_number}"
+                if column:
+                    location += f", Column: {column}"
+            parts.append(location)
+        
+        super().__init__("\n".join(parts))
 
 
 class VariableError(BoilerplatesError):

+ 125 - 62
cli/core/module.py

@@ -12,6 +12,11 @@ from rich.prompt import Confirm
 from typer import Argument, Context, Option, Typer, Exit
 
 from .display import DisplayManager
+from .exceptions import (
+    TemplateRenderError,
+    TemplateSyntaxError,
+    TemplateValidationError
+)
 from .library import LibraryManager
 from .prompt import PromptHandler
 from .template import Template
@@ -424,12 +429,13 @@ class Module(ABC):
     console.print(f"[dim]Files would have been generated in '{output_dir}'[/dim]")
     logger.info(f"Dry run completed for template '{id}' - {len(rendered_files)} files, {total_size} bytes")
 
-  def _write_generated_files(self, output_dir: Path, rendered_files: Dict[str, str]) -> None:
+  def _write_generated_files(self, output_dir: Path, rendered_files: Dict[str, str], quiet: bool = False) -> None:
     """Write rendered files to the output directory.
     
     Args:
         output_dir: Directory to write files to
         rendered_files: Dictionary of file paths to rendered content
+        quiet: Suppress output messages
     """
     output_dir.mkdir(parents=True, exist_ok=True)
     
@@ -438,9 +444,11 @@ class Module(ABC):
       full_path.parent.mkdir(parents=True, exist_ok=True)
       with open(full_path, 'w', encoding='utf-8') as f:
         f.write(content)
-      console.print(f"[green]Generated file: {file_path}[/green]")  # Keep simple per-file output
+      if not quiet:
+        console.print(f"[green]Generated file: {file_path}[/green]")  # Keep simple per-file output
     
-    self.display.display_success(f"Template generated successfully in '{output_dir}'")
+    if not quiet:
+      self.display.display_success(f"Template generated successfully in '{output_dir}'")
     logger.info(f"Template written to directory: {output_dir}")
 
   def generate(
@@ -451,6 +459,7 @@ class Module(ABC):
     var: Optional[list[str]] = Option(None, "--var", "-v", help="Variable override (repeatable). Supports: KEY=VALUE or KEY VALUE"),
     dry_run: bool = Option(False, "--dry-run", help="Preview template generation without writing files"),
     show_files: bool = Option(False, "--show-files", help="Display generated file contents in plain text (use with --dry-run)"),
+    quiet: bool = Option(False, "--quiet", "-q", help="Suppress all non-error output"),
     ctx: Context = None,
   ) -> None:
     """Generate from template.
@@ -478,6 +487,10 @@ class Module(ABC):
         cli compose generate traefik --dry-run --show-files
     """
     logger.info(f"Starting generation for template '{id}' from module '{self.name}'")
+    
+    # Create a display manager with quiet mode if needed
+    display = DisplayManager(quiet=quiet) if quiet else self.display
+    
     template = self._load_template_by_id(id)
 
     # Apply defaults and overrides
@@ -488,8 +501,9 @@ class Module(ABC):
     if template.variables:
       template.variables.sort_sections()
 
-    self._display_template_details(template, id)
-    console.print()
+    if not quiet:
+      self._display_template_details(template, id)
+      console.print()
 
     # Collect variable values
     variable_values = self._collect_variable_values(template, interactive)
@@ -499,10 +513,13 @@ class Module(ABC):
       if template.variables:
         template.variables.validate_all()
       
-      rendered_files, variable_values = template.render(template.variables)
+      # Check if we're in debug mode (logger level is DEBUG)
+      debug_mode = logger.isEnabledFor(logging.DEBUG)
+      
+      rendered_files, variable_values = template.render(template.variables, debug=debug_mode)
       
       if not rendered_files:
-        self.display.display_error("Template rendering returned no files", context="template generation")
+        display.display_error("Template rendering returned no files", context="template generation")
         raise Exit(code=1)
       
       logger.info(f"Successfully rendered template '{id}'")
@@ -510,29 +527,38 @@ class Module(ABC):
       # Determine output directory
       output_dir = Path(directory) if directory else Path(id)
       
-      # Check for conflicts and get confirmation
-      existing_files = self._check_output_directory(output_dir, rendered_files, interactive)
-      if existing_files is None:
-        return  # User cancelled
-      
-      # Get final confirmation for generation
-      dir_not_empty = output_dir.exists() and any(output_dir.iterdir())
-      if not self._get_generation_confirmation(output_dir, rendered_files, existing_files, 
-                                               dir_not_empty, dry_run, interactive):
-        return  # User cancelled
+      # Check for conflicts and get confirmation (skip in quiet mode)
+      if not quiet:
+        existing_files = self._check_output_directory(output_dir, rendered_files, interactive)
+        if existing_files is None:
+          return  # User cancelled
+        
+        # Get final confirmation for generation
+        dir_not_empty = output_dir.exists() and any(output_dir.iterdir())
+        if not self._get_generation_confirmation(output_dir, rendered_files, existing_files, 
+                                                 dir_not_empty, dry_run, interactive):
+          return  # User cancelled
+      else:
+        # In quiet mode, just check for existing files without prompts
+        existing_files = []
       
       # Execute generation (dry run or actual)
       if dry_run:
-        self._execute_dry_run(id, output_dir, rendered_files, show_files)
+        if not quiet:
+          self._execute_dry_run(id, output_dir, rendered_files, show_files)
       else:
-        self._write_generated_files(output_dir, rendered_files)
+        self._write_generated_files(output_dir, rendered_files, quiet=quiet)
       
-      # Display next steps
-      if template.metadata.next_steps:
-        self.display.display_next_steps(template.metadata.next_steps, variable_values)
+      # Display next steps (not in quiet mode)
+      if template.metadata.next_steps and not quiet:
+        display.display_next_steps(template.metadata.next_steps, variable_values)
 
+    except TemplateRenderError as e:
+      # Display enhanced error information for template rendering errors (always show errors)
+      display.display_template_render_error(e, context=f"template '{id}'")
+      raise Exit(code=1)
     except Exception as e:
-      self.display.display_error(str(e), context=f"generating template '{id}'")
+      display.display_error(str(e), context=f"generating template '{id}'")
       raise Exit(code=1)
 
   def config_get(
@@ -726,6 +752,7 @@ class Module(ABC):
   def validate(
     self,
     template_id: str = Argument(None, help="Template ID to validate (if omitted, validates all templates)"),
+    path: Optional[str] = Option(None, "--path", "-p", help="Validate a template from a specific directory path"),
     verbose: bool = Option(False, "--verbose", "-v", help="Show detailed validation information"),
     semantic: bool = Option(True, "--semantic/--no-semantic", help="Enable semantic validation (Docker Compose schema, etc.)")
   ) -> None:
@@ -746,6 +773,9 @@ class Module(ABC):
         # Validate a specific template
         cli compose validate gitlab
         
+        # Validate a template from a specific path
+        cli compose validate --path /path/to/template
+        
         # Validate with verbose output
         cli compose validate --verbose
         
@@ -755,56 +785,89 @@ class Module(ABC):
     from rich.table import Table
     from .validators import get_validator_registry
     
-    if template_id:
-      # Validate a specific template
+    # Validate from path takes precedence
+    if path:
+      try:
+        template_path = Path(path).resolve()
+        if not template_path.exists():
+          self.display.display_error(f"Path does not exist: {path}")
+          raise Exit(code=1)
+        if not template_path.is_dir():
+          self.display.display_error(f"Path is not a directory: {path}")
+          raise Exit(code=1)
+        
+        console.print(f"[bold]Validating template from path:[/bold] [cyan]{template_path}[/cyan]\n")
+        template = Template(template_path, library_name="local")
+        template_id = template.id
+      except Exception as e:
+        self.display.display_error(f"Failed to load template from path '{path}': {e}")
+        raise Exit(code=1)
+    elif template_id:
+      # Validate a specific template by ID
       try:
         template = self._load_template_by_id(template_id)
         console.print(f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]\n")
+      except Exception as e:
+        self.display.display_error(f"Failed to load template '{template_id}': {e}")
+        raise Exit(code=1)
+    else:
+      # Validate all templates - handled separately below
+      template = None
+    
+    # Single template validation
+    if template:
+      try:
+        # Trigger validation by accessing used_variables
+        _ = template.used_variables
+        # Trigger variable definition validation by accessing variables
+        _ = template.variables
+        self.display.display_success("Jinja2 validation passed")
         
-        try:
-          # Trigger validation by accessing used_variables
-          _ = template.used_variables
-          # Trigger variable definition validation by accessing variables
-          _ = template.variables
-          self.display.display_success("Jinja2 validation passed")
+        # Semantic validation
+        if semantic:
+          console.print(f"\n[bold cyan]Running semantic validation...[/bold cyan]")
+          registry = get_validator_registry()
+          has_semantic_errors = False
           
-          # Semantic validation
-          if semantic:
-            console.print(f"\n[bold cyan]Running semantic validation...[/bold cyan]")
-            registry = get_validator_registry()
-            has_semantic_errors = False
-            
-            # Render template with default values for validation
-            rendered_files, _ = template.render(template.variables)
+          # Render template with default values for validation
+          debug_mode = logger.isEnabledFor(logging.DEBUG)
+          rendered_files, _ = template.render(template.variables, debug=debug_mode)
+          
+          for file_path, content in rendered_files.items():
+            result = registry.validate_file(content, file_path)
             
-            for file_path, content in rendered_files.items():
-              result = registry.validate_file(content, file_path)
+            if result.errors or result.warnings or (verbose and result.info):
+              console.print(f"\n[cyan]File:[/cyan] {file_path}")
+              result.display(f"{file_path}")
               
-              if result.errors or result.warnings or (verbose and result.info):
-                console.print(f"\n[cyan]File:[/cyan] {file_path}")
-                result.display(f"{file_path}")
-                
-                if result.errors:
-                  has_semantic_errors = True
-            
-            if not has_semantic_errors:
-              self.display.display_success("Semantic validation passed")
-            else:
-              self.display.display_error("Semantic validation found errors")
-              raise Exit(code=1)
+              if result.errors:
+                has_semantic_errors = True
           
-          if verbose:
-            console.print(f"\n[dim]Template path: {template.template_dir}[/dim]")
-            console.print(f"[dim]Found {len(template.used_variables)} variables[/dim]")
+          if not has_semantic_errors:
+            self.display.display_success("Semantic validation passed")
+          else:
+            self.display.display_error("Semantic validation found errors")
+            raise Exit(code=1)
+        
+        if verbose:
+          console.print(f"\n[dim]Template path: {template.template_dir}[/dim]")
+          console.print(f"[dim]Found {len(template.used_variables)} variables[/dim]")
+          if semantic:
             console.print(f"[dim]Generated {len(rendered_files)} files[/dim]")
-        except ValueError as e:
-          self.display.display_error(f"Validation failed for '{template_id}':")
-          console.print(f"\n{e}")
-          raise Exit(code=1)
-          
+      
+      except TemplateRenderError as e:
+        # Display enhanced error information for template rendering errors
+        self.display.display_template_render_error(e, context=f"template '{template_id}'")
+        raise Exit(code=1)
+      except (TemplateSyntaxError, TemplateValidationError, ValueError) as e:
+        self.display.display_error(f"Validation failed for '{template_id}':")
+        console.print(f"\n{e}")
+        raise Exit(code=1)
       except Exception as e:
-        console.print(f"[red]Error loading template '{template_id}': {e}[/red]")
+        self.display.display_error(f"Unexpected error validating '{template_id}': {e}")
         raise Exit(code=1)
+      
+      return
     else:
       # Validate all templates
       console.print(f"[bold]Validating all {self.name} templates...[/bold]\n")

+ 218 - 5
cli/core/template.py

@@ -22,10 +22,168 @@ from jinja2 import Environment, FileSystemLoader, meta
 from jinja2.sandbox import SandboxedEnvironment
 from jinja2 import nodes
 from jinja2.visitor import NodeVisitor
+from jinja2.exceptions import (
+    TemplateSyntaxError as Jinja2TemplateSyntaxError,
+    UndefinedError,
+    TemplateError as Jinja2TemplateError,
+    TemplateNotFound as Jinja2TemplateNotFound
+)
 
 logger = logging.getLogger(__name__)
 
 
+def _extract_error_context(
+    file_path: Path,
+    line_number: Optional[int],
+    context_size: int = 3
+) -> List[str]:
+  """Extract lines of context around an error location.
+  
+  Args:
+      file_path: Path to the file with the error
+      line_number: Line number where error occurred (1-indexed)
+      context_size: Number of lines to show before and after
+      
+  Returns:
+      List of context lines with line numbers
+  """
+  if not line_number or not file_path.exists():
+    return []
+  
+  try:
+    with open(file_path, 'r', encoding='utf-8') as f:
+      lines = f.readlines()
+    
+    start_line = max(0, line_number - context_size - 1)
+    end_line = min(len(lines), line_number + context_size)
+    
+    context = []
+    for i in range(start_line, end_line):
+      line_num = i + 1
+      marker = '>>>' if line_num == line_number else '   '
+      context.append(f"{marker} {line_num:4d} | {lines[i].rstrip()}")
+    
+    return context
+  except (IOError, OSError):
+    return []
+
+
+def _get_common_jinja_suggestions(error_msg: str, available_vars: set) -> List[str]:
+  """Generate helpful suggestions based on common Jinja2 errors.
+  
+  Args:
+      error_msg: The error message from Jinja2
+      available_vars: Set of available variable names
+      
+  Returns:
+      List of actionable suggestions
+  """
+  suggestions = []
+  error_lower = error_msg.lower()
+  
+  # Undefined variable errors
+  if 'undefined' in error_lower or 'is not defined' in error_lower:
+    # Try to extract variable name from error message
+    import re
+    var_match = re.search(r"'([^']+)'.*is undefined", error_msg)
+    if not var_match:
+      var_match = re.search(r"'([^']+)'.*is not defined", error_msg)
+    
+    if var_match:
+      undefined_var = var_match.group(1)
+      suggestions.append(f"Variable '{undefined_var}' is not defined in the template spec")
+      
+      # Suggest similar variable names (basic fuzzy matching)
+      similar = [v for v in available_vars if undefined_var.lower() in v.lower() or v.lower() in undefined_var.lower()]
+      if similar:
+        suggestions.append(f"Did you mean one of these? {', '.join(sorted(similar)[:5])}")
+      
+      suggestions.append(f"Add '{undefined_var}' to your template.yaml spec with a default value")
+      suggestions.append("Or use the Jinja2 default filter: {{ " + undefined_var + " | default('value') }}")
+    else:
+      suggestions.append("Check that all variables used in templates are defined in template.yaml")
+      suggestions.append("Use the Jinja2 default filter for optional variables: {{ var | default('value') }}")
+  
+  # Syntax errors
+  elif 'unexpected' in error_lower or 'expected' in error_lower:
+    suggestions.append("Check for syntax errors in your Jinja2 template")
+    suggestions.append("Common issues: missing {% endfor %}, {% endif %}, or {% endblock %}")
+    suggestions.append("Make sure all {{ }} and {% %} tags are properly closed")
+  
+  # Filter errors
+  elif 'filter' in error_lower:
+    suggestions.append("Check that the filter name is spelled correctly")
+    suggestions.append("Verify the filter exists in Jinja2 built-in filters")
+    suggestions.append("Make sure filter arguments are properly formatted")
+  
+  # Template not found
+  elif 'not found' in error_lower or 'does not exist' in error_lower:
+    suggestions.append("Check that the included/imported template file exists")
+    suggestions.append("Verify the template path is relative to the template directory")
+    suggestions.append("Make sure the file has the .j2 extension if it's a Jinja2 template")
+  
+  # Type errors
+  elif 'type' in error_lower and ('int' in error_lower or 'str' in error_lower or 'bool' in error_lower):
+    suggestions.append("Check that variable values have the correct type")
+    suggestions.append("Use Jinja2 filters to convert types: {{ var | int }}, {{ var | string }}")
+  
+  # Add generic helpful tip
+  if not suggestions:
+    suggestions.append("Check the Jinja2 template syntax and variable usage")
+    suggestions.append("Enable --debug mode for more detailed rendering information")
+  
+  return suggestions
+
+
+def _parse_jinja_error(
+    error: Exception,
+    template_file: TemplateFile,
+    template_dir: Path,
+    available_vars: set
+) -> tuple[str, Optional[int], Optional[int], List[str], List[str]]:
+  """Parse a Jinja2 exception to extract detailed error information.
+  
+  Args:
+      error: The Jinja2 exception
+      template_file: The TemplateFile being rendered
+      template_dir: Template directory path
+      available_vars: Set of available variable names
+      
+  Returns:
+      Tuple of (error_message, line_number, column, context_lines, suggestions)
+  """
+  error_msg = str(error)
+  line_number = None
+  column = None
+  context_lines = []
+  suggestions = []
+  
+  # Extract line number from Jinja2 errors
+  if hasattr(error, 'lineno'):
+    line_number = error.lineno
+  
+  # Extract file path and get context
+  file_path = template_dir / template_file.relative_path
+  if line_number and file_path.exists():
+    context_lines = _extract_error_context(file_path, line_number)
+  
+  # Generate suggestions based on error type
+  if isinstance(error, UndefinedError):
+    error_msg = f"Undefined variable: {error}"
+    suggestions = _get_common_jinja_suggestions(str(error), available_vars)
+  elif isinstance(error, Jinja2TemplateSyntaxError):
+    error_msg = f"Template syntax error: {error}"
+    suggestions = _get_common_jinja_suggestions(str(error), available_vars)
+  elif isinstance(error, Jinja2TemplateNotFound):
+    error_msg = f"Template file not found: {error}"
+    suggestions = _get_common_jinja_suggestions(str(error), available_vars)
+  else:
+    # Generic Jinja2 error
+    suggestions = _get_common_jinja_suggestions(error_msg, available_vars)
+  
+  return error_msg, line_number, column, context_lines, suggestions
+
+
 @dataclass
 class TemplateFile:
     """Represents a single file within a template directory."""
@@ -428,9 +586,13 @@ class Template:
       keep_trailing_newline=False,
     )
 
-  def render(self, variables: VariableCollection) -> tuple[Dict[str, str], Dict[str, Any]]:
+  def render(self, variables: VariableCollection, debug: bool = False) -> tuple[Dict[str, str], Dict[str, Any]]:
     """Render all .j2 files in the template directory.
     
+    Args:
+        variables: VariableCollection with values to use for rendering
+        debug: Enable debug mode with verbose output
+        
     Returns:
         Tuple of (rendered_files, variable_values) where variable_values includes autogenerated values
     """
@@ -449,30 +611,81 @@ class Template:
           variable_values[var_name] = generated_value
           logger.debug(f"Auto-generated value for variable '{var_name}'")
     
-    logger.debug(f"Rendering template '{self.id}' with variables: {variable_values}")
+    if debug:
+      logger.info(f"Rendering template '{self.id}' in debug mode")
+      logger.info(f"Available variables: {sorted(variable_values.keys())}")
+      logger.info(f"Variable values: {variable_values}")
+    else:
+      logger.debug(f"Rendering template '{self.id}' with variables: {variable_values}")
+    
     rendered_files = {}
+    available_vars = set(variable_values.keys())
+    
     for template_file in self.template_files: # Iterate over TemplateFile objects
       if template_file.file_type == 'j2':
         try:
+          if debug:
+            logger.info(f"Rendering Jinja2 template: {template_file.relative_path}")
+          
           template = self.jinja_env.get_template(str(template_file.relative_path)) # Use lazy-loaded jinja_env
           rendered_content = template.render(**variable_values)
+          
           # Sanitize the rendered content to remove excessive blank lines
           rendered_content = self._sanitize_content(rendered_content, template_file.output_path)
           rendered_files[str(template_file.output_path)] = rendered_content
+          
+          if debug:
+            logger.info(f"Successfully rendered: {template_file.relative_path} -> {template_file.output_path}")
+        
+        except (UndefinedError, Jinja2TemplateSyntaxError, Jinja2TemplateNotFound, Jinja2TemplateError) as e:
+          # Parse Jinja2 error to extract detailed information
+          error_msg, line_num, col, context_lines, suggestions = _parse_jinja_error(
+              e, template_file, self.template_dir, available_vars
+          )
+          
+          logger.error(f"Error rendering template file {template_file.relative_path}: {error_msg}")
+          
+          # Create enhanced TemplateRenderError with all context
+          raise TemplateRenderError(
+              message=error_msg,
+              file_path=str(template_file.relative_path),
+              line_number=line_num,
+              column=col,
+              context_lines=context_lines,
+              variable_context={k: str(v) for k, v in variable_values.items()} if debug else {},
+              suggestions=suggestions,
+              original_error=e
+          )
+        
         except Exception as e:
-          logger.error(f"Error rendering template file {template_file.relative_path}: {e}")
-          raise TemplateRenderError(f"Error rendering {template_file.relative_path}: {e}")
+          # Catch any other unexpected errors
+          logger.error(f"Unexpected error rendering template file {template_file.relative_path}: {e}")
+          raise TemplateRenderError(
+              message=f"Unexpected rendering error: {e}",
+              file_path=str(template_file.relative_path),
+              suggestions=["This is an unexpected error. Please check the template for issues."],
+              original_error=e
+          )
+      
       elif template_file.file_type == 'static':
           # For static files, just read their content and add to rendered_files
           # This ensures static files are also part of the output dictionary
           file_path = self.template_dir / template_file.relative_path
           try:
+              if debug:
+                logger.info(f"Copying static file: {template_file.relative_path}")
+              
               with open(file_path, "r", encoding="utf-8") as f:
                   content = f.read()
                   rendered_files[str(template_file.output_path)] = content
           except (IOError, OSError) as e:
               logger.error(f"Error reading static file {file_path}: {e}")
-              raise TemplateRenderError(f"Error reading static file {file_path}: {e}")
+              raise TemplateRenderError(
+                  message=f"Error reading static file: {e}",
+                  file_path=str(template_file.relative_path),
+                  suggestions=["Check that the file exists and has read permissions"],
+                  original_error=e
+              )
           
     return rendered_files, variable_values
   

+ 318 - 0
tests/test-compose-template-errors.sh

@@ -0,0 +1,318 @@
+#!/usr/bin/env bash
+#
+# Test script for template rendering error handling
+# This script creates various templates with errors and validates them
+# to test the improved error handling and display
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Test directory
+TEST_DIR="/tmp/boilerplates-error-tests"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Template Error Handling Tests${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+# Clean up test directory
+rm -rf "$TEST_DIR"
+mkdir -p "$TEST_DIR"
+
+# Function to create a test template
+create_test_template() {
+  local name=$1
+  local description=$2
+  local template_content=$3
+  local spec_content=$4
+  
+  local template_dir="$TEST_DIR/$name"
+  mkdir -p "$template_dir"
+  
+  # Create template.yaml
+  cat > "$template_dir/template.yaml" <<EOF
+---
+kind: compose
+metadata:
+  name: $name Test
+  description: $description
+  version: 0.1.0
+  author: Test Suite
+  date: '2025-01-09'
+$spec_content
+EOF
+  
+  # Create compose.yaml.j2
+  echo "$template_content" > "$template_dir/compose.yaml.j2"
+  
+  echo "$template_dir"
+}
+
+# Function to run a test
+run_test() {
+  local test_name=$1
+  local template_path=$2
+  local expected_to_fail=$3
+  
+  echo -e "${YELLOW}Test: $test_name${NC}"
+  echo -e "${BLUE}Template path: $template_path${NC}"
+  echo ""
+  
+  # Run validation with debug mode
+  if python3 -m cli --log-level DEBUG compose validate --path "$template_path" 2>&1; then
+    if [ "$expected_to_fail" = "true" ]; then
+      echo -e "${RED}✗ UNEXPECTED: Test passed but was expected to fail${NC}"
+      return 1
+    else
+      echo -e "${GREEN}✓ Test passed as expected${NC}"
+      return 0
+    fi
+  else
+    if [ "$expected_to_fail" = "true" ]; then
+      echo -e "${GREEN}✓ Test failed as expected${NC}"
+      return 0
+    else
+      echo -e "${RED}✗ UNEXPECTED: Test failed but was expected to pass${NC}"
+      return 1
+    fi
+  fi
+}
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 1: Undefined Variable Error${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_1=$(create_test_template \
+  "undefined-variable" \
+  "Template with undefined variable" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:{{ nginx_version }}
+    container_name: {{ undefined_variable }}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      nginx_version:
+        type: str
+        description: Nginx version
+        default: latest
+')
+
+run_test "Undefined Variable" "$TEMPLATE_1" "true"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 2: Jinja2 Syntax Error - Missing endif${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_2=$(create_test_template \
+  "syntax-error-endif" \
+  "Template with missing endif" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:latest
+    {% if enable_ports %}
+    ports:
+      - "80:80"
+    # Missing {% endif %}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      enable_ports:
+        type: bool
+        description: Enable ports
+        default: true
+')
+
+run_test "Syntax Error - Missing endif" "$TEMPLATE_2" "true"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 3: Jinja2 Syntax Error - Unclosed bracket${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_3=$(create_test_template \
+  "syntax-error-bracket" \
+  "Template with unclosed bracket" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:{{ version
+    container_name: {{ service_name }}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      version:
+        type: str
+        description: Version
+        default: latest
+')
+
+run_test "Syntax Error - Unclosed Bracket" "$TEMPLATE_3" "true"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 4: Filter Error - Unknown filter${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_4=$(create_test_template \
+  "filter-error" \
+  "Template with unknown filter" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:{{ version | unknown_filter }}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      version:
+        type: str
+        description: Version
+        default: latest
+')
+
+run_test "Filter Error - Unknown Filter" "$TEMPLATE_4" "true"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 5: Valid Template - Should Pass${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_5=$(create_test_template \
+  "valid-template" \
+  "Valid template that should pass validation" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:{{ version }}
+    container_name: {{ service_name }}
+    {% if enable_ports %}
+    ports:
+      - "{{ port }}:80"
+    {% endif %}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      version:
+        type: str
+        description: Version
+        default: latest
+      enable_ports:
+        type: bool
+        description: Enable ports
+        default: true
+      port:
+        type: int
+        description: External port
+        default: 8080
+')
+
+run_test "Valid Template" "$TEMPLATE_5" "false"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 6: Nested Variable with Typo${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_6=$(create_test_template \
+  "typo-variable" \
+  "Template with typo in variable name" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:latest
+    environment:
+      - SERVICE_NAME={{ servce_name }}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+')
+
+run_test "Typo in Variable Name" "$TEMPLATE_6" "true"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test 7: Template with Default Filter${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+
+TEMPLATE_7=$(create_test_template \
+  "default-filter" \
+  "Template using default filter - should pass" \
+  'version: "3.8"
+services:
+  {{ service_name }}:
+    image: nginx:{{ version | default("latest") }}
+    container_name: {{ container_name | default(service_name) }}
+' \
+  'spec:
+  general:
+    vars:
+      service_name:
+        type: str
+        description: Service name
+        default: myservice
+      version:
+        type: str
+        description: Version
+        default: ""
+')
+
+run_test "Default Filter Usage" "$TEMPLATE_7" "false"
+echo ""
+
+echo -e "${BLUE}========================================${NC}"
+echo -e "${BLUE}Test Summary${NC}"
+echo -e "${BLUE}========================================${NC}"
+echo ""
+echo -e "${GREEN}All tests completed!${NC}"
+echo -e "${YELLOW}Check the output above for detailed error messages.${NC}"
+echo ""
+echo -e "${BLUE}Test directory: $TEST_DIR${NC}"
+echo -e "${YELLOW}Note: Test directory has been preserved for manual inspection${NC}"

+ 51 - 26
tests/test-compose-templates.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
-# Test script for validating all compose templates with dry-run
-# This script iterates through all templates and runs a non-interactive dry-run
+# Test script for validating all compose templates
+# This script iterates through all templates and runs validation
 # Output is GitHub Actions friendly and easy to parse
 
 set -euo pipefail
@@ -57,41 +57,66 @@ print_status() {
     esac
 }
 
-# Function to test a single template
-test_template() {
+# Function to validate and test a single template
+validate_template() {
     local template_id=$1
     local template_name=$2
     
     TOTAL=$((TOTAL + 1))
     
-    print_status "INFO" "Testing: ${template_id}"
+    echo -e "${BLUE}────────────────────────────────────────${NC}"
+    print_status "INFO" "Testing: ${template_id} (${template_name})"
     
-    # Run the generate command with dry-run and no-interactive
-    # Capture stderr for error reporting
-    local temp_stderr=$(mktemp)
-    if python3 -m cli compose generate "${template_id}" \
+    # Step 1: Run validation
+    echo -e "  ${YELLOW}→${NC} Running validation..."
+    local temp_output=$(mktemp)
+    if ! python3 -m cli compose validate "${template_id}" \
+        > "${temp_output}" 2>&1; then
+        echo -e "  ${RED}✗${NC} Validation failed"
+        print_status "ERROR" "${template_id} - Validation failed"
+        FAILED=$((FAILED + 1))
+        FAILED_TEMPLATES+=("${template_id}")
+        
+        # Show error message (first few lines)
+        if [[ -s "${temp_output}" ]]; then
+            echo "  └─ Validation error:"
+            head -n 5 "${temp_output}" | sed 's/^/     /'
+        fi
+        rm -f "${temp_output}"
+        return 1
+    fi
+    echo -e "  ${GREEN}✓${NC} Validation passed"
+    rm -f "${temp_output}"
+    
+    # Step 2: Run dry-run generation with quiet mode
+    echo -e "  ${YELLOW}→${NC} Running dry-run generation..."
+    local temp_gen=$(mktemp)
+    if ! python3 -m cli compose generate "${template_id}" \
         --dry-run \
         --no-interactive \
-        > /dev/null 2>"${temp_stderr}"; then
-        print_status "SUCCESS" "${template_id}"
-        PASSED=$((PASSED + 1))
-        rm -f "${temp_stderr}"
-        return 0
-    else
-        print_status "ERROR" "${template_id}"
+        --quiet \
+        > "${temp_gen}" 2>&1; then
+        echo -e "  ${RED}✗${NC} Generation failed"
+        print_status "ERROR" "${template_id} - Generation failed"
         FAILED=$((FAILED + 1))
         FAILED_TEMPLATES+=("${template_id}")
         
-        # Show error message from stderr
-        if [[ -s "${temp_stderr}" ]]; then
-            local error_msg=$(cat "${temp_stderr}" | tr '\n' ' ')
-            if [[ -n "${error_msg}" ]]; then
-                echo "  └─ ${error_msg}"
-            fi
+        # Show error message (first few lines)
+        if [[ -s "${temp_gen}" ]]; then
+            echo "  └─ Generation error:"
+            head -n 5 "${temp_gen}" | sed 's/^/     /'
         fi
-        rm -f "${temp_stderr}"
+        rm -f "${temp_gen}"
         return 1
     fi
+    echo -e "  ${GREEN}✓${NC} Generation passed"
+    rm -f "${temp_gen}"
+    
+    # Both validation and generation passed
+    print_status "SUCCESS" "${template_id}"
+    PASSED=$((PASSED + 1))
+    echo ""
+    return 0
 }
 
 # Main execution
@@ -99,7 +124,7 @@ main() {
     cd "${PROJECT_ROOT}"
     
     echo "=========================================="
-    echo "Compose Template Dry-Run Tests"
+    echo "Compose Template Validation Tests"
     echo "=========================================="
     print_status "INFO" "Working directory: ${PROJECT_ROOT}"
     echo ""
@@ -121,8 +146,8 @@ main() {
     
     # Iterate through each template
     while IFS=$'\t' read -r template_id template_name tags version library; do
-        # Continue even if test fails (don't let set -e stop us)
-        test_template "${template_id}" "${template_name}" || true
+        # Continue even if validation fails (don't let set -e stop us)
+        validate_template "${template_id}" "${template_name}" || true
     done <<< "${templates}"
     
     # Print summary