Просмотр исходного кода

Merge feature/1364-section-iteration-helper into release/v0.1.0

xcad 4 месяцев назад
Родитель
Сommit
c4ec0035d3

+ 43 - 0
AGENTS.md

@@ -151,6 +151,49 @@ External code should NEVER directly call `IconManager` or `console.print`, inste
 - `DisplayManager` provides a **centralized interface** for ALL CLI output rendering (Use `display_***` methods from `DisplayManager` for ALL output)
 - `IconManager` provides **Nerd Font icons** internally for DisplayManager, don't use Emojis or direct console access
 
+**DisplayManager Architecture** (Refactored for Single Responsibility Principle):
+
+`DisplayManager` acts as a facade that delegates to specialized manager classes:
+
+1. **VariableDisplayManager** - Handles all variable-related rendering
+   - `render_variable_value()` - Variable value formatting with context awareness
+   - `render_section()` - Section header display
+   - `render_variables_table()` - Complete variables table with dependencies
+
+2. **TemplateDisplayManager** - Handles all template-related rendering
+   - `render_template()` - Main template display coordinator
+   - `render_template_header()` - Template metadata display
+   - `render_file_tree()` - Template file structure visualization
+   - `render_file_generation_confirmation()` - Files preview before generation
+
+3. **StatusDisplayManager** - Handles status messages and error display
+   - `display_message()` - Core message formatting with level-based routing
+   - `display_error()`, `display_warning()`, `display_success()`, `display_info()` - Convenience methods
+   - `display_template_render_error()` - Detailed render error display
+   - `display_warning_with_confirmation()` - Interactive warning prompts
+
+4. **TableDisplayManager** - Handles table rendering
+   - `render_templates_table()` - Templates list with library indicators
+   - `render_status_table()` - Status tables with success/error indicators
+   - `render_config_tree()` - Configuration tree visualization
+
+**Usage Pattern:**
+```python
+# External code uses DisplayManager methods (backward compatible)
+display = DisplayManager()
+display.display_template(template, template_id)
+
+# Internally, DisplayManager delegates to specialized managers
+# display.templates.render_template(template, template_id)
+```
+
+**Design Principles:**
+- External code calls `DisplayManager` methods only
+- `DisplayManager` delegates to specialized managers internally
+- Each specialized manager has a single, focused responsibility
+- Backward compatibility maintained through delegation methods
+- All managers can access parent DisplayManager via `self.parent`
+
 ## Templates
 
 Templates are directory-based. Each template is a directory containing all the necessary files and subdirectories for the boilerplate.

+ 56 - 10
cli/core/collection.py

@@ -6,6 +6,7 @@ import logging
 
 from .variable import Variable
 from .section import VariableSection
+from .exceptions import VariableValidationError, VariableError
 
 logger = logging.getLogger(__name__)
 
@@ -138,7 +139,7 @@ class VariableCollection:
             )
             error_msg = "\n".join(errors)
             logger.error(error_msg)
-            raise ValueError(error_msg)
+            raise VariableValidationError("__collection__", error_msg)
 
     def _validate_section_toggle(self, section: VariableSection) -> None:
         """Validate that toggle variable is of type bool if it exists.
@@ -161,9 +162,9 @@ class VariableCollection:
             return
 
         if toggle_var.type != "bool":
-            raise ValueError(
-                f"Section '{section.key}' toggle variable '{section.toggle}' must be type 'bool', "
-                f"but is type '{toggle_var.type}'"
+            raise VariableValidationError(
+                section.toggle,
+                f"Section '{section.key}' toggle variable must be type 'bool', but is type '{toggle_var.type}'"
             )
 
     @staticmethod
@@ -283,7 +284,7 @@ class VariableCollection:
                 if expected_value is None:
                     # Old format: validate section exists
                     if var_or_section not in self._sections:
-                        raise ValueError(
+                        raise VariableError(
                             f"Section '{section_key}' depends on '{var_or_section}', but '{var_or_section}' does not exist"
                         )
                 else:
@@ -331,7 +332,7 @@ class VariableCollection:
                         if has_cycle(dep_name):
                             return True
                     elif dep_name in rec_stack:
-                        raise ValueError(
+                        raise VariableError(
                             f"Circular dependency detected: '{section_key}' depends on '{dep_name}', "
                             f"which creates a cycle"
                         )
@@ -414,10 +415,14 @@ class VariableCollection:
         """
         reset_vars = []
         
+        # Pre-compute satisfaction states to avoid repeated lookups
+        section_states = {
+            key: (self.is_section_satisfied(key), section.is_enabled())
+            for key, section in self._sections.items()
+        }
+        
         for section_key, section in self._sections.items():
-            # Check if section dependencies are satisfied
-            section_satisfied = self.is_section_satisfied(section_key)
-            is_enabled = section.is_enabled()
+            section_satisfied, is_enabled = section_states[section_key]
             
             for var_name, variable in section.variables.items():
                 # Only process bool variables
@@ -525,6 +530,47 @@ class VariableCollection:
 
         return result
 
+    def iter_active_sections(
+        self,
+        include_disabled: bool = False,
+        include_unsatisfied: bool = False,
+    ):
+        """Iterate over sections respecting dependencies and toggles.
+
+        This is the centralized iterator for processing sections with proper
+        filtering. It eliminates duplicate iteration logic across the codebase.
+
+        Args:
+            include_disabled: If True, include sections that are disabled via toggle
+            include_unsatisfied: If True, include sections with unsatisfied dependencies
+
+        Yields:
+            Tuple of (section_key, section) for each active section
+
+        Examples:
+            # Only enabled sections with satisfied dependencies (default)
+            for key, section in variables.iter_active_sections():
+                process(section)
+
+            # Include disabled sections but skip unsatisfied dependencies
+            for key, section in variables.iter_active_sections(include_disabled=True):
+                process(section)
+        """
+        for section_key, section in self._sections.items():
+            # Check dependencies first
+            if not include_unsatisfied and not self.is_section_satisfied(section_key):
+                logger.debug(
+                    f"Skipping section '{section_key}' - dependencies not satisfied"
+                )
+                continue
+
+            # Check enabled status
+            if not include_disabled and not section.is_enabled():
+                logger.debug(f"Skipping section '{section_key}' - section is disabled")
+                continue
+
+            yield section_key, section
+
     def get_sections(self) -> Dict[str, VariableSection]:
         """Get all sections in the collection."""
         return self._sections.copy()
@@ -770,7 +816,7 @@ class VariableCollection:
         if errors:
             error_msg = "Variable validation failed: " + ", ".join(errors)
             logger.error(error_msg)
-            raise ValueError(error_msg)
+            raise VariableValidationError("__multiple__", ", ".join(errors))
 
     def merge(
         self,

+ 0 - 970
cli/core/display.py

@@ -1,970 +0,0 @@
-from __future__ import annotations
-
-import logging
-from pathlib import Path
-from typing import TYPE_CHECKING
-
-from rich.console import Console
-from rich.table import Table
-from rich.tree import Tree
-
-if TYPE_CHECKING:
-    from .exceptions import TemplateRenderError
-    from .template import Template
-
-logger = logging.getLogger(__name__)
-console = Console()
-console_err = Console(stderr=True)
-
-
-class IconManager:
-    """Centralized icon management system for consistent CLI display.
-
-    This class provides standardized icons for file types, status indicators,
-    and UI elements. Icons use Nerd Font glyphs for consistent display.
-
-    Categories:
-        - File types: .yaml, .j2, .json, .md, etc.
-        - Status: success, warning, error, info, skipped
-        - UI elements: folders, config, locks, etc.
-    """
-
-    # File Type Icons
-    FILE_FOLDER = "\uf07b"  #
-    FILE_DEFAULT = "\uf15b"  #
-    FILE_YAML = "\uf15c"  #
-    FILE_JSON = "\ue60b"  #
-    FILE_MARKDOWN = "\uf48a"  #
-    FILE_JINJA2 = "\ue235"  #
-    FILE_DOCKER = "\uf308"  #
-    FILE_COMPOSE = "\uf308"  #
-    FILE_SHELL = "\uf489"  #
-    FILE_PYTHON = "\ue73c"  #
-    FILE_TEXT = "\uf15c"  #
-
-    # Status Indicators
-    STATUS_SUCCESS = "\uf00c"  #  (check)
-    STATUS_ERROR = "\uf00d"  #  (times/x)
-    STATUS_WARNING = "\uf071"  #  (exclamation-triangle)
-    STATUS_INFO = "\uf05a"  #  (info-circle)
-    STATUS_SKIPPED = "\uf05e"  #  (ban/circle-slash)
-
-    # UI Elements
-    UI_CONFIG = "\ue5fc"  #
-    UI_LOCK = "\uf084"  #
-    UI_SETTINGS = "\uf013"  #
-    UI_ARROW_RIGHT = "\uf061"  #  (arrow-right)
-    UI_BULLET = "\uf111"  #  (circle)
-    UI_LIBRARY_GIT = "\uf418"  #  (git icon)
-    UI_LIBRARY_STATIC = "\uf07c"  #  (folder icon)
-
-    @classmethod
-    def get_file_icon(cls, file_path: str | Path) -> str:
-        """Get the appropriate icon for a file based on its extension or name.
-
-        Args:
-            file_path: Path to the file (can be string or Path object)
-
-        Returns:
-            Unicode icon character for the file type
-
-        Examples:
-            >>> IconManager.get_file_icon("config.yaml")
-            '\uf15c'
-            >>> IconManager.get_file_icon("template.j2")
-            '\ue235'
-        """
-        if isinstance(file_path, str):
-            file_path = Path(file_path)
-
-        file_name = file_path.name.lower()
-        suffix = file_path.suffix.lower()
-
-        # Check for Docker Compose files
-        compose_names = {
-            "docker-compose.yml",
-            "docker-compose.yaml",
-            "compose.yml",
-            "compose.yaml",
-        }
-        if file_name in compose_names or file_name.startswith("docker-compose"):
-            return cls.FILE_DOCKER
-
-        # Check by extension
-        extension_map = {
-            ".yaml": cls.FILE_YAML,
-            ".yml": cls.FILE_YAML,
-            ".json": cls.FILE_JSON,
-            ".md": cls.FILE_MARKDOWN,
-            ".j2": cls.FILE_JINJA2,
-            ".sh": cls.FILE_SHELL,
-            ".py": cls.FILE_PYTHON,
-            ".txt": cls.FILE_TEXT,
-        }
-
-        return extension_map.get(suffix, cls.FILE_DEFAULT)
-
-    @classmethod
-    def get_status_icon(cls, status: str) -> str:
-        """Get the appropriate icon for a status indicator.
-
-        Args:
-            status: Status type (success, error, warning, info, skipped)
-
-        Returns:
-            Unicode icon character for the status
-
-        Examples:
-            >>> IconManager.get_status_icon("success")
-            '✓'
-            >>> IconManager.get_status_icon("warning")
-            '⚠'
-        """
-        status_map = {
-            "success": cls.STATUS_SUCCESS,
-            "error": cls.STATUS_ERROR,
-            "warning": cls.STATUS_WARNING,
-            "info": cls.STATUS_INFO,
-            "skipped": cls.STATUS_SKIPPED,
-        }
-        return status_map.get(status.lower(), cls.STATUS_INFO)
-
-    @classmethod
-    def folder(cls) -> str:
-        """Get the folder icon."""
-        return cls.FILE_FOLDER
-
-    @classmethod
-    def config(cls) -> str:
-        """Get the config icon."""
-        return cls.UI_CONFIG
-
-    @classmethod
-    def lock(cls) -> str:
-        """Get the lock icon (for sensitive variables)."""
-        return cls.UI_LOCK
-
-    @classmethod
-    def arrow_right(cls) -> str:
-        """Get the right arrow icon (for showing transitions/changes)."""
-        return cls.UI_ARROW_RIGHT
-
-
-class DisplayManager:
-    """Handles all rich rendering for the CLI.
-
-    This class is responsible for ALL display output in the CLI, including:
-    - Status messages (success, error, warning, info)
-    - Tables (templates, summaries, results)
-    - Trees (file structures, configurations)
-    - Confirmation dialogs and prompts
-    - Headers and sections
-
-    Design Principles:
-    - All display logic should go through DisplayManager methods
-    - IconManager is ONLY used internally by 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
-    ) -> None:
-        """Display a table of templates with library type indicators.
-
-        Args:
-            templates: List of Template objects
-            module_name: Name of the module
-            title: Title for the table
-        """
-        if not templates:
-            logger.info(f"No templates found for module '{module_name}'")
-            return
-
-        logger.info(f"Listing {len(templates)} templates for module '{module_name}'")
-        table = Table(title=title)
-        table.add_column("ID", style="bold", no_wrap=True)
-        table.add_column("Name")
-        table.add_column("Tags")
-        table.add_column("Version", no_wrap=True)
-        table.add_column("Schema", no_wrap=True)
-        table.add_column("Library", no_wrap=True)
-
-        for template in templates:
-            name = template.metadata.name or "Unnamed Template"
-            tags_list = template.metadata.tags or []
-            tags = ", ".join(tags_list) if tags_list else "-"
-            version = (
-                str(template.metadata.version) if template.metadata.version else ""
-            )
-            schema = template.schema_version if hasattr(template, 'schema_version') else "1.0"
-
-            # Show library with type indicator and color
-            library_name = template.metadata.library or ""
-            library_type = template.metadata.library_type or "git"
-
-            if library_type == "static":
-                # Static libraries: yellow/amber color with folder icon
-                library_display = (
-                    f"[yellow]{IconManager.UI_LIBRARY_STATIC} {library_name}[/yellow]"
-                )
-            else:
-                # Git libraries: blue color with git icon
-                library_display = (
-                    f"[blue]{IconManager.UI_LIBRARY_GIT} {library_name}[/blue]"
-                )
-
-            # Display qualified ID if present (e.g., "alloy.default")
-            display_id = template.id
-
-            table.add_row(display_id, name, tags, version, schema, library_display)
-
-        console.print(table)
-
-    def display_template_details(
-        self, template: Template, template_id: str
-    ) -> None:
-        """Display template information panel and variables table.
-
-        Args:
-            template: Template instance to display
-            template_id: ID of the template
-        """
-        self._display_template_header(template, template_id)
-        self._display_file_tree(template)
-        self._display_variables_table(template)
-
-    def display_section_header(self, title: str, description: str | None) -> None:
-        """Display a section header."""
-        if description:
-            console.print(
-                f"\n[bold cyan]{title}[/bold cyan] [dim]- {description}[/dim]"
-            )
-        else:
-            console.print(f"\n[bold cyan]{title}[/bold cyan]")
-        console.print("─" * 40, style="dim")
-
-    def display_validation_error(self, message: str) -> None:
-        """Display a validation error message."""
-        self.display_message("error", message)
-
-    def display_message(
-        self, level: str, message: str, context: str | None = None
-    ) -> None:
-        """Display a message with consistent formatting.
-
-        Args:
-            level: Message level (error, warning, success, info)
-            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")
-
-        # Format message based on context
-        if context:
-            text = (
-                f"{level.capitalize()} in {context}: {message}"
-                if level == "error" or level == "warning"
-                else f"{context}: {message}"
-            )
-        else:
-            text = (
-                f"{level.capitalize()}: {message}"
-                if level == "error" or level == "warning"
-                else message
-            )
-
-        output_console.print(f"[{color}]{icon} {text}[/{color}]")
-
-        # Log appropriately
-        log_message = f"{context}: {message}" if context else message
-        log_methods = {
-            "error": logger.error,
-            "warning": logger.warning,
-            "success": logger.info,
-            "info": logger.info,
-        }
-        log_methods.get(level, logger.info)(log_message)
-
-    def display_error(self, message: str, context: str | None = None) -> None:
-        """Display an error message."""
-        self.display_message("error", message, context)
-
-    def display_warning(self, message: str, context: str | None = None) -> None:
-        """Display a warning message."""
-        self.display_message("warning", message, context)
-
-    def display_success(self, message: str, context: str | None = None) -> None:
-        """Display a success message."""
-        self.display_message("success", message, context)
-
-    def display_info(self, message: str, context: str | None = None) -> None:
-        """Display an informational message."""
-        self.display_message("info", message, context)
-
-    def display_version_incompatibility(
-        self, template_id: str, required_version: str, current_version: str
-    ) -> None:
-        """Display a version incompatibility error with upgrade instructions.
-
-        Args:
-            template_id: ID of the incompatible template
-            required_version: Minimum CLI version required by template
-            current_version: Current CLI version
-        """
-        console_err.print()
-        console_err.print(
-            f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
-        )
-        console_err.print()
-        console_err.print(
-            f"Template '[cyan]{template_id}[/cyan]' requires CLI version [green]{required_version}[/green] or higher."
-        )
-        console_err.print(f"Current CLI version: [yellow]{current_version}[/yellow]")
-        console_err.print()
-        console_err.print("[bold]Upgrade Instructions:[/bold]")
-        console_err.print(
-            f"  {IconManager.UI_ARROW_RIGHT} Run: [cyan]pip install --upgrade boilerplates[/cyan]"
-        )
-        console_err.print(
-            f"  {IconManager.UI_ARROW_RIGHT} Or install specific version: [cyan]pip install boilerplates=={required_version}[/cyan]"
-        )
-        console_err.print()
-
-        logger.error(
-            f"Template '{template_id}' requires CLI version {required_version}, "
-            f"current version is {current_version}"
-        )
-
-    def _display_template_header(self, template: Template, template_id: str) -> None:
-        """Display the header for a template with library information."""
-        template_name = template.metadata.name or "Unnamed Template"
-        version = (
-            str(template.metadata.version)
-            if template.metadata.version
-            else "Not specified"
-        )
-        schema = template.schema_version if hasattr(template, 'schema_version') else "1.0"
-        description = template.metadata.description or "No description available"
-
-        # Get library information
-        library_name = template.metadata.library or ""
-        library_type = template.metadata.library_type or "git"
-
-        # Format library display with icon and color
-        if library_type == "static":
-            library_display = (
-                f"[yellow]{IconManager.UI_LIBRARY_STATIC} {library_name}[/yellow]"
-            )
-        else:
-            library_display = (
-                f"[blue]{IconManager.UI_LIBRARY_GIT} {library_name}[/blue]"
-            )
-
-        console.print(
-            f"[bold blue]{template_name} ({template_id} - [cyan]{version}[/cyan] - [magenta]schema {schema}[/magenta]) {library_display}[/bold blue]"
-        )
-        console.print(description)
-
-    def _build_file_tree(
-        self, root_label: str, files: list, get_file_info: callable
-    ) -> Tree:
-        """Build a file tree structure.
-
-        Args:
-            root_label: Label for root node
-            files: List of files to display
-            get_file_info: Function that takes a file and returns (path, display_name, color, extra_text)
-
-        Returns:
-            Tree object ready for display
-        """
-        file_tree = Tree(root_label)
-        tree_nodes = {Path("."): file_tree}
-
-        for file_item in sorted(files, key=lambda f: get_file_info(f)[0]):
-            path, display_name, color, extra_text = get_file_info(file_item)
-            parts = path.parts
-            current_path = Path(".")
-            current_node = file_tree
-
-            # Build directory structure
-            for part in parts[:-1]:
-                current_path = current_path / part
-                if current_path not in tree_nodes:
-                    new_node = current_node.add(
-                        f"{IconManager.folder()} [white]{part}[/white]"
-                    )
-                    tree_nodes[current_path] = new_node
-                current_node = tree_nodes[current_path]
-
-            # Add file
-            icon = IconManager.get_file_icon(display_name)
-            file_label = f"{icon} [{color}]{display_name}[/{color}]"
-            if extra_text:
-                file_label += f" {extra_text}"
-            current_node.add(file_label)
-
-        return file_tree
-
-    def _display_file_tree(self, template: Template) -> None:
-        """Display the file structure of a template."""
-        console.print()
-        console.print("[bold blue]Template File Structure:[/bold blue]")
-
-        def get_template_file_info(template_file):
-            display_name = (
-                template_file.output_path.name
-                if hasattr(template_file, "output_path")
-                else template_file.relative_path.name
-            )
-            return (template_file.relative_path, display_name, "white", None)
-
-        file_tree = self._build_file_tree(
-            f"{IconManager.folder()} [white]{template.id}[/white]",
-            template.template_files,
-            get_template_file_info,
-        )
-
-        if file_tree.children:
-            console.print(file_tree)
-
-    def _display_variables_table(
-        self, template: Template
-    ) -> None:
-        """Display a table of variables for a template.
-
-        All variables and sections are always shown. Disabled sections/variables
-        are displayed with dimmed styling.
-
-        Args:
-            template: Template instance
-        """
-        if not (template.variables and template.variables.has_sections()):
-            return
-
-        console.print()
-        console.print("[bold blue]Template Variables:[/bold blue]")
-
-        variables_table = Table(show_header=True, header_style="bold blue")
-        variables_table.add_column("Variable", style="white", no_wrap=True)
-        variables_table.add_column("Type", style="magenta")
-        variables_table.add_column("Default", style="green")
-        variables_table.add_column("Description", style="white")
-
-        first_section = True
-        for section in template.variables.get_sections().values():
-            if not section.variables:
-                continue
-
-            if not first_section:
-                variables_table.add_row("", "", "", "", style="bright_black")
-            first_section = False
-
-            # Check if section is enabled AND dependencies are satisfied
-            is_enabled = section.is_enabled()
-            dependencies_satisfied = template.variables.is_section_satisfied(
-                section.key
-            )
-            is_dimmed = not (is_enabled and dependencies_satisfied)
-
-            # Only show (disabled) if section has no dependencies (dependencies make it obvious)
-            # Empty list means no dependencies (same as None)
-            has_dependencies = section.needs and len(section.needs) > 0
-            disabled_text = (
-                " (disabled)" if (is_dimmed and not has_dependencies) else ""
-            )
-
-            # For disabled sections, make entire heading bold and dim (don't include colored markup inside)
-            if is_dimmed:
-                # Build text without internal markup, then wrap entire thing in bold bright_black (dimmed appearance)
-                required_part = " (required)" if section.required else ""
-                header_text = f"[bold bright_black]{section.title}{required_part}{disabled_text}[/bold bright_black]"
-            else:
-                # For enabled sections, include the colored markup
-                required_text = (
-                    " [yellow](required)[/yellow]" if section.required else ""
-                )
-                header_text = (
-                    f"[bold]{section.title}{required_text}{disabled_text}[/bold]"
-                )
-            variables_table.add_row(header_text, "", "", "")
-            for var_name, variable in section.variables.items():
-                # Skip toggle variable in required sections (always enabled, no need to show)
-                if section.required and section.toggle and var_name == section.toggle:
-                    continue
-
-                # Check if variable's needs are satisfied
-                var_satisfied = template.variables.is_variable_satisfied(var_name)
-
-                # Dim the variable if section is dimmed OR variable needs are not satisfied
-                row_style = "bright_black" if (is_dimmed or not var_satisfied) else None
-
-                # Build default value display
-                # Special case: disabled bool variables show as "original → False"
-                if (is_dimmed or not var_satisfied) and variable.type == "bool":
-                    # Show that disabled bool variables are forced to False
-                    if hasattr(variable, "_original_disabled") and variable._original_disabled is not False:
-                        orig_val = str(variable._original_disabled)
-                        default_val = f"{orig_val} {IconManager.arrow_right()} False"
-                    else:
-                        default_val = "False"
-                # If origin is 'config' and original value differs from current, show: original → config_value
-                # BUT only for enabled variables (don't show arrow for disabled ones)
-                elif (
-                    not (is_dimmed or not var_satisfied)
-                    and variable.origin == "config"
-                    and hasattr(variable, "_original_stored")
-                    and variable.original_value != variable.value
-                ):
-                    # Format original value (use same display logic, but shorter)
-                    if variable.sensitive:
-                        orig_display = "********"
-                    elif (
-                        variable.original_value is None or variable.original_value == ""
-                    ):
-                        orig_display = "[dim](none)[/dim]"
-                    else:
-                        orig_val_str = str(variable.original_value)
-                        orig_display = (
-                            orig_val_str[:15] + "..."
-                            if len(orig_val_str) > 15
-                            else orig_val_str
-                        )
-
-                    # Get current (config) value display (without showing "(none)" since we have the arrow)
-                    config_display = variable.get_display_value(
-                        mask_sensitive=True, max_length=15, show_none=False
-                    )
-                    if (
-                        not config_display
-                    ):  # If still empty after show_none=False, show actual value
-                        config_display = (
-                            str(variable.value) if variable.value else "(empty)"
-                        )
-
-                    # Highlight the arrow and config value in bold yellow to show it's a custom override
-                    default_val = f"{orig_display} [bold yellow]{IconManager.arrow_right()} {config_display}[/bold yellow]"
-                else:
-                    # Use variable's native get_display_value() method (shows "(none)" for empty)
-                    default_val = variable.get_display_value(
-                        mask_sensitive=True, max_length=30, show_none=True
-                    )
-
-                # Add lock icon for sensitive variables
-                sensitive_icon = f" {IconManager.lock()}" if variable.sensitive else ""
-                # Add required indicator for required variables
-                required_indicator = (
-                    " [yellow](required)[/yellow]" if variable.required else ""
-                )
-                var_display = f"  {var_name}{sensitive_icon}{required_indicator}"
-
-                variables_table.add_row(
-                    var_display,
-                    variable.type or "str",
-                    default_val,
-                    variable.description or "",
-                    style=row_style,
-                )
-
-        console.print(variables_table)
-
-    def display_file_generation_confirmation(
-        self,
-        output_dir: Path,
-        files: dict[str, str],
-        existing_files: list[Path] | None = None,
-    ) -> None:
-        """Display files to be generated with confirmation prompt."""
-        console.print()
-        console.print("[bold]Files to be generated:[/bold]")
-
-        def get_file_generation_info(file_path_str):
-            file_path = Path(file_path_str)
-            file_name = file_path.parts[-1] if file_path.parts else file_path.name
-            full_path = output_dir / file_path
-
-            if existing_files and full_path in existing_files:
-                return (file_path, file_name, "yellow", "[red](will overwrite)[/red]")
-            else:
-                return (file_path, file_name, "green", None)
-
-        file_tree = self._build_file_tree(
-            f"{IconManager.folder()} [cyan]{output_dir.resolve()}[/cyan]",
-            files.keys(),
-            get_file_generation_info,
-        )
-
-        console.print(file_tree)
-        console.print()
-
-    def display_config_tree(
-        self, spec: dict, module_name: str, show_all: bool = False
-    ) -> None:
-        """Display configuration spec as a tree view.
-
-        Args:
-            spec: The configuration spec dictionary
-            module_name: Name of the module
-            show_all: If True, show all details including descriptions
-        """
-        if not spec:
-            console.print(
-                f"[yellow]No configuration found for module '{module_name}'[/yellow]"
-            )
-            return
-
-        # Create root tree node
-        tree = Tree(
-            f"[bold blue]{IconManager.config()} {str.capitalize(module_name)} Configuration[/bold blue]"
-        )
-
-        for section_name, section_data in spec.items():
-            if not isinstance(section_data, dict):
-                continue
-
-            # Determine if this is a section with variables
-            # Guard against None from empty YAML sections
-            section_vars = section_data.get("vars") or {}
-            section_desc = section_data.get("description", "")
-            section_required = section_data.get("required", False)
-            section_toggle = section_data.get("toggle", None)
-            section_needs = section_data.get("needs", None)
-
-            # Build section label
-            section_label = f"[cyan]{section_name}[/cyan]"
-            if section_required:
-                section_label += " [yellow](required)[/yellow]"
-            if section_toggle:
-                section_label += f" [dim](toggle: {section_toggle})[/dim]"
-            if section_needs:
-                needs_str = (
-                    ", ".join(section_needs)
-                    if isinstance(section_needs, list)
-                    else section_needs
-                )
-                section_label += f" [dim](needs: {needs_str})[/dim]"
-
-            if show_all and section_desc:
-                section_label += f"\n  [dim]{section_desc}[/dim]"
-
-            section_node = tree.add(section_label)
-
-            # Add variables
-            if section_vars:
-                for var_name, var_data in section_vars.items():
-                    if isinstance(var_data, dict):
-                        var_type = var_data.get("type", "string")
-                        var_default = var_data.get("default", "")
-                        var_desc = var_data.get("description", "")
-                        var_sensitive = var_data.get("sensitive", False)
-
-                        # Build variable label
-                        var_label = f"[green]{var_name}[/green] [dim]({var_type})[/dim]"
-
-                        if var_default is not None and var_default != "":
-                            display_val = (
-                                "********" if var_sensitive else str(var_default)
-                            )
-                            if not var_sensitive and len(display_val) > 30:
-                                display_val = display_val[:27] + "..."
-                            var_label += f" = [yellow]{display_val}[/yellow]"
-
-                        if show_all and var_desc:
-                            var_label += f"\n    [dim]{var_desc}[/dim]"
-
-                        section_node.add(var_label)
-                    else:
-                        # Simple key-value pair
-                        section_node.add(
-                            f"[green]{var_name}[/green] = [yellow]{var_data}[/yellow]"
-                        )
-
-        console.print(tree)
-
-    def display_next_steps(self, next_steps: str, variable_values: dict) -> None:
-        """Display next steps after template generation, rendering them as a Jinja2 template.
-
-        Args:
-            next_steps: The next_steps string from template metadata (may contain Jinja2 syntax)
-            variable_values: Dictionary of variable values to use for rendering
-        """
-        if not next_steps:
-            return
-
-        console.print("\n[bold cyan]Next Steps:[/bold cyan]")
-
-        try:
-            from jinja2 import Template as Jinja2Template
-
-            next_steps_template = Jinja2Template(next_steps)
-            rendered_next_steps = next_steps_template.render(variable_values)
-            console.print(rendered_next_steps)
-        except Exception as e:
-            logger.warning(f"Failed to render next_steps as template: {e}")
-            # Fallback to plain text if rendering fails
-            console.print(next_steps)
-
-    def display_status_table(
-        self,
-        title: str,
-        rows: list[tuple[str, str, bool]],
-        columns: tuple[str, str] = ("Item", "Status"),
-    ) -> None:
-        """Display a status table with success/error indicators.
-
-        Args:
-            title: Table title
-            rows: List of tuples (name, message, success_bool)
-            columns: Column headers (name_header, status_header)
-        """
-        table = Table(title=title, show_header=True)
-        table.add_column(columns[0], style="cyan", no_wrap=True)
-        table.add_column(columns[1])
-
-        for name, message, success in rows:
-            status_style = "green" if success else "red"
-            status_icon = IconManager.get_status_icon("success" if success else "error")
-            table.add_row(
-                name, f"[{status_style}]{status_icon} {message}[/{status_style}]"
-            )
-
-        console.print(table)
-
-    def display_summary_table(self, title: str, items: dict[str, str]) -> None:
-        """Display a simple two-column summary table.
-
-        Args:
-            title: Table title
-            items: Dictionary of key-value pairs to display
-        """
-        table = Table(title=title, show_header=False, box=None, padding=(0, 2))
-        table.add_column(style="bold")
-        table.add_column()
-
-        for key, value in items.items():
-            table.add_row(key, value)
-
-        console.print(table)
-
-    def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
-        """Display a table of file operations with sizes and statuses.
-
-        Args:
-            files: List of tuples (file_path, size_bytes, status)
-        """
-        table = Table(
-            show_header=True, header_style="bold cyan", box=None, padding=(0, 1)
-        )
-        table.add_column("File", style="white", no_wrap=False)
-        table.add_column("Size", justify="right", style="dim")
-        table.add_column("Status", style="yellow")
-
-        for file_path, size_bytes, status in files:
-            # Format size
-            if size_bytes < 1024:
-                size_str = f"{size_bytes}B"
-            elif size_bytes < 1024 * 1024:
-                size_str = f"{size_bytes / 1024:.1f}KB"
-            else:
-                size_str = f"{size_bytes / (1024 * 1024):.1f}MB"
-
-            table.add_row(str(file_path), size_str, status)
-
-        console.print(table)
-
-    def display_heading(
-        self, text: str, icon_type: str | None = None, style: str = "bold"
-    ) -> None:
-        """Display a heading with optional icon.
-
-        Args:
-            text: Heading text
-            icon_type: Type of icon to display (e.g., 'folder', 'file', 'config')
-            style: Rich style to apply
-        """
-        if icon_type:
-            icon = self._get_icon_by_type(icon_type)
-            console.print(f"[{style}]{icon} {text}[/{style}]")
-        else:
-            console.print(f"[{style}]{text}[/{style}]")
-
-    def display_warning_with_confirmation(
-        self, message: str, details: list[str] | None = None, default: bool = False
-    ) -> bool:
-        """Display a warning message with optional details and get confirmation.
-
-        Args:
-            message: Warning message to display
-            details: Optional list of detail lines to show
-            default: Default value for confirmation
-
-        Returns:
-            True if user confirms, False otherwise
-        """
-        icon = IconManager.get_status_icon("warning")
-        console.print(f"\n[yellow]{icon} {message}[/yellow]")
-
-        if details:
-            for detail in details:
-                console.print(f"[yellow]  {detail}[/yellow]")
-
-        from rich.prompt import Confirm
-
-        return Confirm.ask("Continue?", default=default)
-
-    def display_skipped(self, message: str, reason: str | None = None) -> None:
-        """Display a skipped/disabled message.
-
-        Args:
-            message: The main message to display
-            reason: Optional reason why it was skipped
-        """
-        icon = IconManager.get_status_icon("skipped")
-        if reason:
-            console.print(f"\n[dim]{icon} {message} (skipped - {reason})[/dim]")
-        else:
-            console.print(f"\n[dim]{icon} {message} (skipped)[/dim]")
-
-    def get_lock_icon(self) -> str:
-        """Get the lock icon for sensitive variables.
-
-        Returns:
-            Lock icon unicode character
-        """
-        return IconManager.lock()
-
-    def _get_icon_by_type(self, icon_type: str) -> str:
-        """Get icon by semantic type name.
-
-        Args:
-            icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
-
-        Returns:
-            Icon unicode character
-        """
-        icon_map = {
-            "folder": IconManager.folder(),
-            "file": IconManager.FILE_DEFAULT,
-            "config": IconManager.config(),
-            "lock": IconManager.lock(),
-            "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()

+ 526 - 0
cli/core/display/__init__.py

@@ -0,0 +1,526 @@
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+from rich.tree import Tree
+
+from .variable_display import VariableDisplayManager
+from .template_display import TemplateDisplayManager
+from .status_display import StatusDisplayManager
+from .table_display import TableDisplayManager
+
+if TYPE_CHECKING:
+    from ..exceptions import TemplateRenderError
+    from ..template import Template
+
+logger = logging.getLogger(__name__)
+console = Console()
+console_err = Console(stderr=True)
+
+
+class DisplaySettings:
+    """Centralized display configuration settings.
+    
+    This class holds all configurable display parameters including colors,
+    styles, layouts, and formatting options. Modify these values to customize
+    the CLI appearance.
+    """
+
+    # === Color Scheme ===
+    COLOR_ERROR = "red"
+    COLOR_WARNING = "yellow"
+    COLOR_SUCCESS = "green"
+    COLOR_INFO = "blue"
+    COLOR_MUTED = "dim"
+    
+    # Library type colors
+    COLOR_LIBRARY_GIT = "blue"
+    COLOR_LIBRARY_STATIC = "yellow"
+
+    # === Style Constants ===
+    STYLE_HEADER = "bold blue"
+    STYLE_HEADER_ALT = "bold cyan"
+    STYLE_DISABLED = "bright_black"
+    STYLE_SECTION_TITLE = "bold cyan"
+    STYLE_SECTION_DESC = "dim"
+    
+    # Table styles
+    STYLE_TABLE_HEADER = "bold blue"
+    STYLE_VAR_COL_NAME = "white"
+    STYLE_VAR_COL_TYPE = "magenta"
+    STYLE_VAR_COL_DEFAULT = "green"
+    STYLE_VAR_COL_DESC = "white"
+
+    # === Text Labels ===
+    LABEL_REQUIRED = " [yellow](required)[/yellow]"
+    LABEL_DISABLED = " (disabled)"
+    TEXT_EMPTY_VALUE = "(none)"
+    TEXT_EMPTY_OVERRIDE = "(empty)"
+    TEXT_UNNAMED_TEMPLATE = "Unnamed Template"
+    TEXT_NO_DESCRIPTION = "No description available"
+    TEXT_VERSION_NOT_SPECIFIED = "Not specified"
+    
+    # === Value Formatting ===
+    SENSITIVE_MASK = "********"
+    TRUNCATION_SUFFIX = "..."
+    VALUE_MAX_LENGTH_SHORT = 15
+    VALUE_MAX_LENGTH_DEFAULT = 30
+    
+    # === Layout Constants ===
+    SECTION_SEPARATOR_CHAR = "─"
+    SECTION_SEPARATOR_LENGTH = 40
+    VAR_NAME_INDENT = "  "  # 2 spaces
+    
+    # === Size Formatting ===
+    SIZE_KB_THRESHOLD = 1024
+    SIZE_MB_THRESHOLD = 1024 * 1024
+    SIZE_DECIMAL_PLACES = 1
+    
+    # === Table Padding ===
+    PADDING_PANEL = (1, 2)
+    PADDING_TABLE_COMPACT = (0, 1)
+    PADDING_TABLE_NORMAL = (0, 2)
+
+
+class IconManager:
+    """Centralized icon management system for consistent CLI display.
+
+    This class provides standardized icons for file types, status indicators,
+    and UI elements. Icons use Nerd Font glyphs for consistent display.
+
+    Categories:
+        - File types: .yaml, .j2, .json, .md, etc.
+        - Status: success, warning, error, info, skipped
+        - UI elements: folders, config, locks, etc.
+    """
+
+    # File Type Icons
+    FILE_FOLDER = "\uf07b"  #
+    FILE_DEFAULT = "\uf15b"  #
+    FILE_YAML = "\uf15c"  #
+    FILE_JSON = "\ue60b"  #
+    FILE_MARKDOWN = "\uf48a"  #
+    FILE_JINJA2 = "\ue235"  #
+    FILE_DOCKER = "\uf308"  #
+    FILE_COMPOSE = "\uf308"  #
+    FILE_SHELL = "\uf489"  #
+    FILE_PYTHON = "\ue73c"  #
+    FILE_TEXT = "\uf15c"  #
+
+    # Status Indicators
+    STATUS_SUCCESS = "\uf00c"  #  (check)
+    STATUS_ERROR = "\uf00d"  #  (times/x)
+    STATUS_WARNING = "\uf071"  #  (exclamation-triangle)
+    STATUS_INFO = "\uf05a"  #  (info-circle)
+    STATUS_SKIPPED = "\uf05e"  #  (ban/circle-slash)
+
+    # UI Elements
+    UI_CONFIG = "\ue5fc"  #
+    UI_LOCK = "\uf084"  #
+    UI_SETTINGS = "\uf013"  #
+    UI_ARROW_RIGHT = "\uf061"  #  (arrow-right)
+    UI_BULLET = "\uf111"  #  (circle)
+    UI_LIBRARY_GIT = "\uf418"  #  (git icon)
+    UI_LIBRARY_STATIC = "\uf07c"  #  (folder icon)
+
+    @classmethod
+    def get_file_icon(cls, file_path: str | Path) -> str:
+        """Get the appropriate icon for a file based on its extension or name.
+
+        Args:
+            file_path: Path to the file (can be string or Path object)
+
+        Returns:
+            Unicode icon character for the file type
+
+        Examples:
+            >>> IconManager.get_file_icon("config.yaml")
+            '\uf15c'
+            >>> IconManager.get_file_icon("template.j2")
+            '\ue235'
+        """
+        if isinstance(file_path, str):
+            file_path = Path(file_path)
+
+        file_name = file_path.name.lower()
+        suffix = file_path.suffix.lower()
+
+        # Check for Docker Compose files
+        compose_names = {
+            "docker-compose.yml",
+            "docker-compose.yaml",
+            "compose.yml",
+            "compose.yaml",
+        }
+        if file_name in compose_names or file_name.startswith("docker-compose"):
+            return cls.FILE_DOCKER
+
+        # Check by extension
+        extension_map = {
+            ".yaml": cls.FILE_YAML,
+            ".yml": cls.FILE_YAML,
+            ".json": cls.FILE_JSON,
+            ".md": cls.FILE_MARKDOWN,
+            ".j2": cls.FILE_JINJA2,
+            ".sh": cls.FILE_SHELL,
+            ".py": cls.FILE_PYTHON,
+            ".txt": cls.FILE_TEXT,
+        }
+
+        return extension_map.get(suffix, cls.FILE_DEFAULT)
+
+    @classmethod
+    def get_status_icon(cls, status: str) -> str:
+        """Get the appropriate icon for a status indicator.
+
+        Args:
+            status: Status type (success, error, warning, info, skipped)
+
+        Returns:
+            Unicode icon character for the status
+
+        Examples:
+            >>> IconManager.get_status_icon("success")
+            '✓'
+            >>> IconManager.get_status_icon("warning")
+            '⚠'
+        """
+        status_map = {
+            "success": cls.STATUS_SUCCESS,
+            "error": cls.STATUS_ERROR,
+            "warning": cls.STATUS_WARNING,
+            "info": cls.STATUS_INFO,
+            "skipped": cls.STATUS_SKIPPED,
+        }
+        return status_map.get(status.lower(), cls.STATUS_INFO)
+
+    @classmethod
+    def folder(cls) -> str:
+        """Get the folder icon."""
+        return cls.FILE_FOLDER
+
+    @classmethod
+    def config(cls) -> str:
+        """Get the config icon."""
+        return cls.UI_CONFIG
+
+    @classmethod
+    def lock(cls) -> str:
+        """Get the lock icon (for sensitive variables)."""
+        return cls.UI_LOCK
+
+    @classmethod
+    def arrow_right(cls) -> str:
+        """Get the right arrow icon (for showing transitions/changes)."""
+        return cls.UI_ARROW_RIGHT
+
+
+class DisplayManager:
+    """Main display coordinator with shared resources.
+    
+    This class acts as a facade that delegates to specialized display managers.
+    External code should use DisplayManager methods which provide backward
+    compatibility while internally using the specialized managers.
+
+    Design Principles:
+    - All display logic should go through DisplayManager methods
+    - IconManager is ONLY used internally by display managers
+    - External code should never directly call IconManager or console.print
+    - Consistent formatting across all display types
+    """
+
+    def __init__(self, quiet: bool = False, settings: DisplaySettings | None = None):
+        """Initialize DisplayManager with specialized sub-managers.
+
+        Args:
+            quiet: If True, suppress all non-error output
+            settings: Optional DisplaySettings instance for customization
+        """
+        self.quiet = quiet
+        self.settings = settings or DisplaySettings()
+
+        # Initialize specialized managers
+        self.variables = VariableDisplayManager(self)
+        self.templates = TemplateDisplayManager(self)
+        self.status = StatusDisplayManager(self)
+        self.tables = TableDisplayManager(self)
+    
+    # ===== Shared Helper Methods =====
+    
+    def _format_library_display(self, library_name: str, library_type: str) -> str:
+        """Format library name with appropriate icon and color.
+        
+        Args:
+            library_name: Name of the library
+            library_type: Type of library ('static' or 'git')
+            
+        Returns:
+            Formatted library display string with Rich markup
+        """
+        if library_type == "static":
+            color = self.settings.COLOR_LIBRARY_STATIC
+            icon = IconManager.UI_LIBRARY_STATIC
+        else:
+            color = self.settings.COLOR_LIBRARY_GIT
+            icon = IconManager.UI_LIBRARY_GIT
+        
+        return f"[{color}]{icon} {library_name}[/{color}]"
+    
+    def _truncate_value(self, value: str, max_length: int | None = None) -> str:
+        """Truncate a string value if it exceeds maximum length.
+        
+        Args:
+            value: String value to truncate
+            max_length: Maximum length (uses default if None)
+            
+        Returns:
+            Truncated string with suffix if needed
+        """
+        if max_length is None:
+            max_length = self.settings.VALUE_MAX_LENGTH_DEFAULT
+        
+        if max_length > 0 and len(value) > max_length:
+            return value[: max_length - len(self.settings.TRUNCATION_SUFFIX)] + self.settings.TRUNCATION_SUFFIX
+        return value
+    
+    def _format_file_size(self, size_bytes: int) -> str:
+        """Format file size in human-readable format (B, KB, MB).
+        
+        Args:
+            size_bytes: Size in bytes
+            
+        Returns:
+            Formatted size string (e.g., "1.5KB", "2.3MB")
+        """
+        if size_bytes < self.settings.SIZE_KB_THRESHOLD:
+            return f"{size_bytes}B"
+        elif size_bytes < self.settings.SIZE_MB_THRESHOLD:
+            kb = size_bytes / self.settings.SIZE_KB_THRESHOLD
+            return f"{kb:.{self.settings.SIZE_DECIMAL_PLACES}f}KB"
+        else:
+            mb = size_bytes / self.settings.SIZE_MB_THRESHOLD
+            return f"{mb:.{self.settings.SIZE_DECIMAL_PLACES}f}MB"
+
+    # ===== Backward Compatibility Delegation Methods =====
+    # These methods delegate to specialized managers for backward compatibility
+
+    def display_templates_table(
+        self, templates: list, module_name: str, title: str
+    ) -> None:
+        """Delegate to TableDisplayManager."""
+        return self.tables.render_templates_table(templates, module_name, title)
+
+    def display_template(self, template: "Template", template_id: str) -> None:
+        """Delegate to TemplateDisplayManager."""
+        return self.templates.render_template(template, template_id)
+
+    def display_section(self, title: str, description: str | None) -> None:
+        """Delegate to VariableDisplayManager."""
+        return self.variables.render_section(title, description)
+
+    def display_validation_error(self, message: str) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_validation_error(message)
+
+    def display_message(
+        self, level: str, message: str, context: str | None = None
+    ) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_message(level, message, context)
+
+    def display_error(self, message: str, context: str | None = None) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_error(message, context)
+
+    def display_warning(self, message: str, context: str | None = None) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_warning(message, context)
+
+    def display_success(self, message: str, context: str | None = None) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_success(message, context)
+
+    def display_info(self, message: str, context: str | None = None) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_info(message, context)
+
+    def display_version_incompatibility(
+        self, template_id: str, required_version: str, current_version: str
+    ) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_version_incompatibility(
+            template_id, required_version, current_version
+        )
+
+    def display_file_generation_confirmation(
+        self,
+        output_dir: Path,
+        files: dict[str, str],
+        existing_files: list[Path] | None = None,
+    ) -> None:
+        """Delegate to TemplateDisplayManager."""
+        return self.templates.render_file_generation_confirmation(
+            output_dir, files, existing_files
+        )
+
+    def display_config_tree(
+        self, spec: dict, module_name: str, show_all: bool = False
+    ) -> None:
+        """Delegate to TableDisplayManager."""
+        return self.tables.render_config_tree(spec, module_name, show_all)
+
+    def display_status_table(
+        self,
+        title: str,
+        rows: list[tuple[str, str, bool]],
+        columns: tuple[str, str] = ("Item", "Status"),
+    ) -> None:
+        """Delegate to TableDisplayManager."""
+        return self.tables.render_status_table(title, rows, columns)
+
+    def display_summary_table(self, title: str, items: dict[str, str]) -> None:
+        """Delegate to TableDisplayManager."""
+        return self.tables.render_summary_table(title, items)
+
+    def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
+        """Delegate to TableDisplayManager."""
+        return self.tables.render_file_operation_table(files)
+
+    def display_warning_with_confirmation(
+        self, message: str, details: list[str] | None = None, default: bool = False
+    ) -> bool:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_warning_with_confirmation(message, details, default)
+
+    def display_skipped(self, message: str, reason: str | None = None) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_skipped(message, reason)
+
+    def display_template_render_error(
+        self, error: "TemplateRenderError", context: str | None = None
+    ) -> None:
+        """Delegate to StatusDisplayManager."""
+        return self.status.display_template_render_error(error, context)
+
+    # ===== Internal Helper Methods =====
+
+    def _render_file_tree_internal(
+        self, root_label: str, files: list, get_file_info: callable
+    ) -> Tree:
+        """Render a file tree structure.
+
+        Args:
+            root_label: Label for root node
+            files: List of files to display
+            get_file_info: Function that takes a file and returns (path, display_name, color, extra_text)
+
+        Returns:
+            Tree object ready for display
+        """
+        file_tree = Tree(root_label)
+        tree_nodes = {Path("."): file_tree}
+
+        for file_item in sorted(files, key=lambda f: get_file_info(f)[0]):
+            path, display_name, color, extra_text = get_file_info(file_item)
+            parts = path.parts
+            current_path = Path(".")
+            current_node = file_tree
+
+            # Build directory structure
+            for part in parts[:-1]:
+                current_path = current_path / part
+                if current_path not in tree_nodes:
+                    new_node = current_node.add(
+                        f"{IconManager.folder()} [white]{part}[/white]"
+                    )
+                    tree_nodes[current_path] = new_node
+                current_node = tree_nodes[current_path]
+
+            # Add file
+            icon = IconManager.get_file_icon(display_name)
+            file_label = f"{icon} [{color}]{display_name}[/{color}]"
+            if extra_text:
+                file_label += f" {extra_text}"
+            current_node.add(file_label)
+
+        return file_tree
+
+    # ===== Additional Methods =====
+
+    def display_heading(
+        self, text: str, icon_type: str | None = None, style: str = "bold"
+    ) -> None:
+        """Display a heading with optional icon.
+
+        Args:
+            text: Heading text
+            icon_type: Type of icon to display (e.g., 'folder', 'file', 'config')
+            style: Rich style to apply
+        """
+        if icon_type:
+            icon = self._get_icon_by_type(icon_type)
+            console.print(f"[{style}]{icon} {text}[/{style}]")
+        else:
+            console.print(f"[{style}]{text}[/{style}]")
+
+    def get_lock_icon(self) -> str:
+        """Get the lock icon for sensitive variables.
+
+        Returns:
+            Lock icon unicode character
+        """
+        return IconManager.lock()
+
+    def _get_icon_by_type(self, icon_type: str) -> str:
+        """Get icon by semantic type name.
+
+        Args:
+            icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
+
+        Returns:
+            Icon unicode character
+        """
+        icon_map = {
+            "folder": IconManager.folder(),
+            "file": IconManager.FILE_DEFAULT,
+            "config": IconManager.config(),
+            "lock": IconManager.lock(),
+            "arrow": IconManager.arrow_right(),
+        }
+        return icon_map.get(icon_type, "")
+
+    def display_next_steps(self, next_steps: str, variable_values: dict) -> None:
+        """Display next steps after template generation, rendering them as a Jinja2 template.
+
+        Args:
+            next_steps: The next_steps string from template metadata (may contain Jinja2 syntax)
+            variable_values: Dictionary of variable values to use for rendering
+        """
+        if not next_steps:
+            return
+
+        console.print("\n[bold cyan]Next Steps:[/bold cyan]")
+
+        try:
+            from jinja2 import Template as Jinja2Template
+
+            next_steps_template = Jinja2Template(next_steps)
+            rendered_next_steps = next_steps_template.render(variable_values)
+            console.print(rendered_next_steps)
+        except Exception as e:
+            logger.warning(f"Failed to render next_steps as template: {e}")
+            # Fallback to plain text if rendering fails
+            console.print(next_steps)
+
+
+# Export public API
+__all__ = [
+    "DisplayManager",
+    "DisplaySettings",
+    "IconManager",
+    "console",
+    "console_err",
+]

+ 305 - 0
cli/core/display/status_display.py

@@ -0,0 +1,305 @@
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+from rich.panel import Panel
+from rich.prompt import Confirm
+from rich.syntax import Syntax
+
+if TYPE_CHECKING:
+    from . import DisplayManager
+    from ..exceptions import TemplateRenderError
+
+logger = logging.getLogger(__name__)
+console = Console()
+console_err = Console(stderr=True)
+
+
+class StatusDisplayManager:
+    """Handles status messages and error display.
+    
+    This manager is responsible for displaying success, error, warning,
+    and informational messages with consistent formatting.
+    """
+
+    def __init__(self, parent: "DisplayManager"):
+        """Initialize StatusDisplayManager.
+        
+        Args:
+            parent: Reference to parent DisplayManager for accessing shared resources
+        """
+        self.parent = parent
+
+    def display_message(
+        self, level: str, message: str, context: str | None = None
+    ) -> None:
+        """Display a message with consistent formatting.
+
+        Args:
+            level: Message level (error, warning, success, info)
+            message: The message to display
+            context: Optional context information
+        """
+        from . import IconManager
+        
+        # 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.parent.quiet
+
+        if not should_print:
+            return
+
+        settings = self.parent.settings
+        icon = IconManager.get_status_icon(level)
+        colors = {
+            "error": settings.COLOR_ERROR,
+            "warning": settings.COLOR_WARNING,
+            "success": settings.COLOR_SUCCESS,
+            "info": settings.COLOR_INFO,
+        }
+        color = colors.get(level, "white")
+
+        # Format message based on context
+        if context:
+            text = (
+                f"{level.capitalize()} in {context}: {message}"
+                if level == "error" or level == "warning"
+                else f"{context}: {message}"
+            )
+        else:
+            text = (
+                f"{level.capitalize()}: {message}"
+                if level == "error" or level == "warning"
+                else message
+            )
+
+        output_console.print(f"[{color}]{icon} {text}[/{color}]")
+
+        # Log appropriately
+        log_message = f"{context}: {message}" if context else message
+        log_methods = {
+            "error": logger.error,
+            "warning": logger.warning,
+            "success": logger.info,
+            "info": logger.info,
+        }
+        log_methods.get(level, logger.info)(log_message)
+
+    def display_error(self, message: str, context: str | None = None) -> None:
+        """Display an error message.
+        
+        Args:
+            message: Error message
+            context: Optional context
+        """
+        self.display_message("error", message, context)
+
+    def display_warning(self, message: str, context: str | None = None) -> None:
+        """Display a warning message.
+        
+        Args:
+            message: Warning message
+            context: Optional context
+        """
+        self.display_message("warning", message, context)
+
+    def display_success(self, message: str, context: str | None = None) -> None:
+        """Display a success message.
+        
+        Args:
+            message: Success message
+            context: Optional context
+        """
+        self.display_message("success", message, context)
+
+    def display_info(self, message: str, context: str | None = None) -> None:
+        """Display an informational message.
+        
+        Args:
+            message: Info message
+            context: Optional context
+        """
+        self.display_message("info", message, context)
+
+    def display_validation_error(self, message: str) -> None:
+        """Display a validation error message.
+        
+        Args:
+            message: Validation error message
+        """
+        self.display_message("error", message)
+
+    def display_version_incompatibility(
+        self, template_id: str, required_version: str, current_version: str
+    ) -> None:
+        """Display a version incompatibility error with upgrade instructions.
+
+        Args:
+            template_id: ID of the incompatible template
+            required_version: Minimum CLI version required by template
+            current_version: Current CLI version
+        """
+        from . import IconManager
+        
+        console_err.print()
+        console_err.print(
+            f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
+        )
+        console_err.print()
+        console_err.print(
+            f"Template '[cyan]{template_id}[/cyan]' requires CLI version [green]{required_version}[/green] or higher."
+        )
+        console_err.print(f"Current CLI version: [yellow]{current_version}[/yellow]")
+        console_err.print()
+        console_err.print("[bold]Upgrade Instructions:[/bold]")
+        console_err.print(
+            f"  {IconManager.UI_ARROW_RIGHT} Run: [cyan]pip install --upgrade boilerplates[/cyan]"
+        )
+        console_err.print(
+            f"  {IconManager.UI_ARROW_RIGHT} Or install specific version: [cyan]pip install boilerplates=={required_version}[/cyan]"
+        )
+        console_err.print()
+
+        logger.error(
+            f"Template '{template_id}' requires CLI version {required_version}, "
+            f"current version is {current_version}"
+        )
+
+    def display_skipped(self, message: str, reason: str | None = None) -> None:
+        """Display a skipped/disabled message.
+
+        Args:
+            message: The main message to display
+            reason: Optional reason why it was skipped
+        """
+        from . import IconManager
+        
+        icon = IconManager.get_status_icon("skipped")
+        if reason:
+            console.print(f"\n[dim]{icon} {message} (skipped - {reason})[/dim]")
+        else:
+            console.print(f"\n[dim]{icon} {message} (skipped)[/dim]")
+
+    def display_warning_with_confirmation(
+        self, message: str, details: list[str] | None = None, default: bool = False
+    ) -> bool:
+        """Display a warning message with optional details and get confirmation.
+
+        Args:
+            message: Warning message to display
+            details: Optional list of detail lines to show
+            default: Default value for confirmation
+
+        Returns:
+            True if user confirms, False otherwise
+        """
+        from . import IconManager
+        
+        icon = IconManager.get_status_icon("warning")
+        console.print(f"\n[yellow]{icon} {message}[/yellow]")
+
+        if details:
+            for detail in details:
+                console.print(f"[yellow]  {detail}[/yellow]")
+
+        return Confirm.ask("Continue?", default=default)
+
+    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 . import IconManager
+        
+        # Always display errors to stderr
+        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()

+ 226 - 0
cli/core/display/table_display.py

@@ -0,0 +1,226 @@
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+from rich.table import Table
+from rich.tree import Tree
+
+if TYPE_CHECKING:
+    from . import DisplayManager
+
+logger = logging.getLogger(__name__)
+console = Console()
+
+
+class TableDisplayManager:
+    """Handles table rendering.
+    
+    This manager is responsible for displaying various types of tables
+    including templates lists, status tables, and summaries.
+    """
+
+    def __init__(self, parent: "DisplayManager"):
+        """Initialize TableDisplayManager.
+        
+        Args:
+            parent: Reference to parent DisplayManager for accessing shared resources
+        """
+        self.parent = parent
+
+    def render_templates_table(
+        self, templates: list, module_name: str, title: str
+    ) -> None:
+        """Display a table of templates with library type indicators.
+
+        Args:
+            templates: List of Template objects
+            module_name: Name of the module
+            title: Title for the table
+        """
+        if not templates:
+            logger.info(f"No templates found for module '{module_name}'")
+            return
+
+        logger.info(f"Listing {len(templates)} templates for module '{module_name}'")
+        table = Table(title=title)
+        table.add_column("ID", style="bold", no_wrap=True)
+        table.add_column("Name")
+        table.add_column("Tags")
+        table.add_column("Version", no_wrap=True)
+        table.add_column("Schema", no_wrap=True)
+        table.add_column("Library", no_wrap=True)
+
+        settings = self.parent.settings
+        
+        for template in templates:
+            name = template.metadata.name or settings.TEXT_UNNAMED_TEMPLATE
+            tags_list = template.metadata.tags or []
+            tags = ", ".join(tags_list) if tags_list else "-"
+            version = str(template.metadata.version) if template.metadata.version else ""
+            schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
+
+            # Use helper for library display
+            library_name = template.metadata.library or ""
+            library_type = template.metadata.library_type or "git"
+            library_display = self.parent._format_library_display(library_name, library_type)
+
+            table.add_row(template.id, name, tags, version, schema, library_display)
+
+        console.print(table)
+
+    def render_status_table(
+        self,
+        title: str,
+        rows: list[tuple[str, str, bool]],
+        columns: tuple[str, str] = ("Item", "Status"),
+    ) -> None:
+        """Display a status table with success/error indicators.
+
+        Args:
+            title: Table title
+            rows: List of tuples (name, message, success_bool)
+            columns: Column headers (name_header, status_header)
+        """
+        from . import IconManager
+        
+        table = Table(title=title, show_header=True)
+        table.add_column(columns[0], style="cyan", no_wrap=True)
+        table.add_column(columns[1])
+
+        for name, message, success in rows:
+            status_style = "green" if success else "red"
+            status_icon = IconManager.get_status_icon(
+                "success" if success else "error"
+            )
+            table.add_row(
+                name, f"[{status_style}]{status_icon} {message}[/{status_style}]"
+            )
+
+        console.print(table)
+
+    def render_summary_table(self, title: str, items: dict[str, str]) -> None:
+        """Display a simple two-column summary table.
+
+        Args:
+            title: Table title
+            items: Dictionary of key-value pairs to display
+        """
+        settings = self.parent.settings
+        table = Table(title=title, show_header=False, box=None, padding=settings.PADDING_TABLE_NORMAL)
+        table.add_column(style="bold")
+        table.add_column()
+
+        for key, value in items.items():
+            table.add_row(key, value)
+
+        console.print(table)
+
+    def render_file_operation_table(
+        self, files: list[tuple[str, int, str]]
+    ) -> None:
+        """Display a table of file operations with sizes and statuses.
+
+        Args:
+            files: List of tuples (file_path, size_bytes, status)
+        """
+        settings = self.parent.settings
+        table = Table(
+            show_header=True, header_style=settings.STYLE_HEADER_ALT, box=None, padding=settings.PADDING_TABLE_COMPACT
+        )
+        table.add_column("File", style="white", no_wrap=False)
+        table.add_column("Size", justify="right", style=settings.COLOR_MUTED)
+        table.add_column("Status", style=settings.COLOR_WARNING)
+
+        for file_path, size_bytes, status in files:
+            size_str = self.parent._format_file_size(size_bytes)
+            table.add_row(str(file_path), size_str, status)
+
+        console.print(table)
+
+    def render_config_tree(
+        self, spec: dict, module_name: str, show_all: bool = False
+    ) -> None:
+        """Display configuration spec as a tree view.
+
+        Args:
+            spec: The configuration spec dictionary
+            module_name: Name of the module
+            show_all: If True, show all details including descriptions
+        """
+        from . import IconManager
+        
+        if not spec:
+            console.print(
+                f"[yellow]No configuration found for module '{module_name}'[/yellow]"
+            )
+            return
+
+        # Create root tree node
+        tree = Tree(
+            f"[bold blue]{IconManager.config()} {str.capitalize(module_name)} Configuration[/bold blue]"
+        )
+
+        for section_name, section_data in spec.items():
+            if not isinstance(section_data, dict):
+                continue
+
+            # Determine if this is a section with variables
+            section_vars = section_data.get("vars") or {}
+            section_desc = section_data.get("description", "")
+            section_required = section_data.get("required", False)
+            section_toggle = section_data.get("toggle", None)
+            section_needs = section_data.get("needs", None)
+
+            # Build section label
+            section_label = f"[cyan]{section_name}[/cyan]"
+            if section_required:
+                section_label += " [yellow](required)[/yellow]"
+            if section_toggle:
+                section_label += f" [dim](toggle: {section_toggle})[/dim]"
+            if section_needs:
+                needs_str = (
+                    ", ".join(section_needs)
+                    if isinstance(section_needs, list)
+                    else section_needs
+                )
+                section_label += f" [dim](needs: {needs_str})[/dim]"
+
+            if show_all and section_desc:
+                section_label += f"\n  [dim]{section_desc}[/dim]"
+
+            section_node = tree.add(section_label)
+
+            # Add variables
+            if section_vars:
+                for var_name, var_data in section_vars.items():
+                    if isinstance(var_data, dict):
+                        var_type = var_data.get("type", "string")
+                        var_default = var_data.get("default", "")
+                        var_desc = var_data.get("description", "")
+                        var_sensitive = var_data.get("sensitive", False)
+
+                        # Build variable label
+                        var_label = (
+                            f"[green]{var_name}[/green] [dim]({var_type})[/dim]"
+                        )
+
+                        if var_default is not None and var_default != "":
+                            settings = self.parent.settings
+                            display_val = settings.SENSITIVE_MASK if var_sensitive else str(var_default)
+                            if not var_sensitive:
+                                display_val = self.parent._truncate_value(display_val, settings.VALUE_MAX_LENGTH_DEFAULT)
+                            var_label += f" = [{settings.COLOR_WARNING}]{display_val}[/{settings.COLOR_WARNING}]"
+
+                        if show_all and var_desc:
+                            var_label += f"\n    [dim]{var_desc}[/dim]"
+
+                        section_node.add(var_label)
+                    else:
+                        # Simple key-value pair
+                        section_node.add(
+                            f"[green]{var_name}[/green] = [yellow]{var_data}[/yellow]"
+                        )
+
+        console.print(tree)

+ 133 - 0
cli/core/display/template_display.py

@@ -0,0 +1,133 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+
+if TYPE_CHECKING:
+    from . import DisplayManager
+    from ..template import Template
+
+console = Console()
+
+
+class TemplateDisplayManager:
+    """Handles all template-related rendering.
+    
+    This manager is responsible for displaying template information,
+    file trees, and metadata.
+    """
+
+    def __init__(self, parent: "DisplayManager"):
+        """Initialize TemplateDisplayManager.
+        
+        Args:
+            parent: Reference to parent DisplayManager for accessing shared resources
+        """
+        self.parent = parent
+
+    def render_template(self, template: "Template", template_id: str) -> None:
+        """Display template information panel and variables table.
+
+        Args:
+            template: Template instance to display
+            template_id: ID of the template
+        """
+        self.render_template_header(template, template_id)
+        self.render_file_tree(template)
+        self.parent.variables.render_variables_table(template)
+
+    def render_template_header(self, template: "Template", template_id: str) -> None:
+        """Display the header for a template with library information.
+        
+        Args:
+            template: Template instance
+            template_id: ID of the template
+        """
+        settings = self.parent.settings
+        
+        template_name = template.metadata.name or settings.TEXT_UNNAMED_TEMPLATE
+        version = (
+            str(template.metadata.version)
+            if template.metadata.version
+            else settings.TEXT_VERSION_NOT_SPECIFIED
+        )
+        schema = template.schema_version if hasattr(template, "schema_version") else "1.0"
+        description = template.metadata.description or settings.TEXT_NO_DESCRIPTION
+
+        # Get library information and format with helper
+        library_name = template.metadata.library or ""
+        library_type = template.metadata.library_type or "git"
+        library_display = self.parent._format_library_display(library_name, library_type)
+
+        console.print(
+            f"[{settings.STYLE_HEADER}]{template_name} ({template_id} - [cyan]{version}[/cyan] - [magenta]schema {schema}[/magenta]) {library_display}[/{settings.STYLE_HEADER}]"
+        )
+        console.print(description)
+
+    def render_file_tree(self, template: "Template") -> None:
+        """Display the file structure of a template.
+        
+        Args:
+            template: Template instance
+        """
+        from . import IconManager
+        
+        settings = self.parent.settings
+        console.print()
+        console.print(f"[{settings.STYLE_HEADER}]Template File Structure:[/{settings.STYLE_HEADER}]")
+
+        def get_template_file_info(template_file):
+            display_name = (
+                template_file.output_path.name
+                if hasattr(template_file, "output_path")
+                else template_file.relative_path.name
+            )
+            return (template_file.relative_path, display_name, "white", None)
+
+        file_tree = self.parent._render_file_tree_internal(
+            f"{IconManager.folder()} [white]{template.id}[/white]",
+            template.template_files,
+            get_template_file_info,
+        )
+
+        if file_tree.children:
+            console.print(file_tree)
+
+    def render_file_generation_confirmation(
+        self,
+        output_dir: Path,
+        files: dict[str, str],
+        existing_files: list[Path] | None = None,
+    ) -> None:
+        """Display files to be generated with confirmation prompt.
+        
+        Args:
+            output_dir: Output directory path
+            files: Dictionary of file paths to content
+            existing_files: List of existing files that will be overwritten
+        """
+        from . import IconManager
+        
+        console.print()
+        console.print("[bold]Files to be generated:[/bold]")
+
+        def get_file_generation_info(file_path_str):
+            file_path = Path(file_path_str)
+            file_name = file_path.parts[-1] if file_path.parts else file_path.name
+            full_path = output_dir / file_path
+
+            if existing_files and full_path in existing_files:
+                return (file_path, file_name, "yellow", "[red](will overwrite)[/red]")
+            else:
+                return (file_path, file_name, "green", None)
+
+        file_tree = self.parent._render_file_tree_internal(
+            f"{IconManager.folder()} [cyan]{output_dir.resolve()}[/cyan]",
+            files.keys(),
+            get_file_generation_info,
+        )
+
+        console.print(file_tree)
+        console.print()

+ 234 - 0
cli/core/display/variable_display.py

@@ -0,0 +1,234 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+from rich.table import Table
+
+if TYPE_CHECKING:
+    from . import DisplayManager
+    from ..template import Template
+
+console = Console()
+
+
+class VariableDisplayManager:
+    """Handles all variable-related rendering.
+    
+    This manager is responsible for displaying variables, sections,
+    and their values with appropriate formatting based on context.
+    """
+
+    def __init__(self, parent: "DisplayManager"):
+        """Initialize VariableDisplayManager.
+        
+        Args:
+            parent: Reference to parent DisplayManager for accessing shared resources
+        """
+        self.parent = parent
+
+    def render_variable_value(
+        self,
+        variable,
+        context: str = "default",
+        is_dimmed: bool = False,
+        var_satisfied: bool = True,
+    ) -> str:
+        """Render variable value with appropriate formatting based on context.
+        
+        Args:
+            variable: Variable instance to render
+            context: Display context ("default", "override", "disabled")
+            is_dimmed: Whether the variable should be dimmed
+            var_satisfied: Whether the variable's dependencies are satisfied
+            
+        Returns:
+            Formatted string representation of the variable value
+        """
+        from . import IconManager
+        
+        # Handle disabled bool variables
+        if (is_dimmed or not var_satisfied) and variable.type == "bool":
+            if (
+                hasattr(variable, "_original_disabled")
+                and variable._original_disabled is not False
+            ):
+                return f"{variable._original_disabled} {IconManager.arrow_right()} False"
+            return "False"
+
+        # Handle config overrides with arrow
+        if (
+            variable.origin == "config"
+            and hasattr(variable, "_original_stored")
+            and variable.original_value != variable.value
+        ):
+            settings = self.parent.settings
+            orig = self._format_value(variable, variable.original_value, max_length=settings.VALUE_MAX_LENGTH_SHORT)
+            curr = variable.get_display_value(
+                mask_sensitive=True, max_length=settings.VALUE_MAX_LENGTH_SHORT, show_none=False
+            )
+            if not curr:
+                curr = str(variable.value) if variable.value else settings.TEXT_EMPTY_OVERRIDE
+            return (
+                f"{orig} [bold {settings.COLOR_WARNING}]{IconManager.arrow_right()} {curr}[/bold {settings.COLOR_WARNING}]"
+            )
+
+        # Default formatting
+        settings = self.parent.settings
+        value = variable.get_display_value(
+            mask_sensitive=True, max_length=settings.VALUE_MAX_LENGTH_DEFAULT, show_none=True
+        )
+        if not variable.value:
+            return f"[{settings.COLOR_MUTED}]{value}[/{settings.COLOR_MUTED}]"
+        return value
+
+    def _format_value(self, variable, value, max_length: int | None = None) -> str:
+        """Helper to format a specific value.
+        
+        Args:
+            variable: Variable instance
+            value: Value to format
+            max_length: Maximum length before truncation
+            
+        Returns:
+            Formatted value string
+        """
+        settings = self.parent.settings
+        
+        if variable.sensitive:
+            return settings.SENSITIVE_MASK
+        if value is None or value == "":
+            return f"[{settings.COLOR_MUTED}]({settings.TEXT_EMPTY_VALUE})[/{settings.COLOR_MUTED}]"
+        
+        val_str = str(value)
+        return self.parent._truncate_value(val_str, max_length)
+
+    def render_section(self, title: str, description: str | None) -> None:
+        """Display a section header.
+        
+        Args:
+            title: Section title
+            description: Optional section description
+        """
+        settings = self.parent.settings
+        if description:
+            console.print(
+                f"\n[{settings.STYLE_SECTION_TITLE}]{title}[/{settings.STYLE_SECTION_TITLE}] [{settings.STYLE_SECTION_DESC}]- {description}[/{settings.STYLE_SECTION_DESC}]"
+            )
+        else:
+            console.print(f"\n[{settings.STYLE_SECTION_TITLE}]{title}[/{settings.STYLE_SECTION_TITLE}]")
+        console.print(settings.SECTION_SEPARATOR_CHAR * settings.SECTION_SEPARATOR_LENGTH, style=settings.COLOR_MUTED)
+
+    def _render_section_header(self, section, is_dimmed: bool, has_dependencies: bool) -> str:
+        """Build section header text with appropriate styling.
+        
+        Args:
+            section: VariableSection instance
+            is_dimmed: Whether section is dimmed (disabled)
+            has_dependencies: Whether section has dependency requirements
+            
+        Returns:
+            Formatted header text with Rich markup
+        """
+        settings = self.parent.settings
+        disabled_text = settings.LABEL_DISABLED if (is_dimmed and not has_dependencies) else ""
+        
+        if is_dimmed:
+            required_part = " (required)" if section.required else ""
+            return f"[bold {settings.STYLE_DISABLED}]{section.title}{required_part}{disabled_text}[/bold {settings.STYLE_DISABLED}]"
+        else:
+            required_text = settings.LABEL_REQUIRED if section.required else ""
+            return f"[bold]{section.title}{required_text}{disabled_text}[/bold]"
+    
+    def _render_variable_row(self, var_name: str, variable, is_dimmed: bool, var_satisfied: bool) -> tuple:
+        """Build variable row data for table display.
+        
+        Args:
+            var_name: Variable name
+            variable: Variable instance
+            is_dimmed: Whether containing section is dimmed
+            var_satisfied: Whether variable dependencies are satisfied
+            
+        Returns:
+            Tuple of (var_display, type, default_val, description, row_style)
+        """
+        from . import IconManager
+        
+        settings = self.parent.settings
+        
+        # Build row style
+        row_style = settings.STYLE_DISABLED if (is_dimmed or not var_satisfied) else None
+        
+        # Build default value
+        default_val = self.render_variable_value(
+            variable, is_dimmed=is_dimmed, var_satisfied=var_satisfied
+        )
+        
+        # Build variable display name
+        sensitive_icon = f" {IconManager.lock()}" if variable.sensitive else ""
+        required_indicator = settings.LABEL_REQUIRED if variable.required else ""
+        var_display = f"{settings.VAR_NAME_INDENT}{var_name}{sensitive_icon}{required_indicator}"
+        
+        return (
+            var_display,
+            variable.type or "str",
+            default_val,
+            variable.description or "",
+            row_style,
+        )
+
+    def render_variables_table(self, template: "Template") -> None:
+        """Display a table of variables for a template.
+
+        All variables and sections are always shown. Disabled sections/variables
+        are displayed with dimmed styling.
+
+        Args:
+            template: Template instance
+        """
+        if not (template.variables and template.variables.has_sections()):
+            return
+
+        settings = self.parent.settings
+        console.print()
+        console.print(f"[{settings.STYLE_HEADER}]Template Variables:[/{settings.STYLE_HEADER}]")
+
+        variables_table = Table(show_header=True, header_style=settings.STYLE_TABLE_HEADER)
+        variables_table.add_column("Variable", style=settings.STYLE_VAR_COL_NAME, no_wrap=True)
+        variables_table.add_column("Type", style=settings.STYLE_VAR_COL_TYPE)
+        variables_table.add_column("Default", style=settings.STYLE_VAR_COL_DEFAULT)
+        variables_table.add_column("Description", style=settings.STYLE_VAR_COL_DESC)
+
+        first_section = True
+        for section in template.variables.get_sections().values():
+            if not section.variables:
+                continue
+
+            if not first_section:
+                variables_table.add_row("", "", "", "", style=settings.STYLE_DISABLED)
+            first_section = False
+
+            # Check if section is enabled AND dependencies are satisfied
+            is_enabled = section.is_enabled()
+            dependencies_satisfied = template.variables.is_section_satisfied(section.key)
+            is_dimmed = not (is_enabled and dependencies_satisfied)
+            has_dependencies = section.needs and len(section.needs) > 0
+
+            # Render section header
+            header_text = self._render_section_header(section, is_dimmed, has_dependencies)
+            variables_table.add_row(header_text, "", "", "")
+
+            # Render variables
+            for var_name, variable in section.variables.items():
+                # Skip toggle variable in required sections
+                if section.required and section.toggle and var_name == section.toggle:
+                    continue
+
+                # Check if variable's needs are satisfied
+                var_satisfied = template.variables.is_variable_satisfied(var_name)
+
+                # Build and add row
+                row_data = self._render_variable_row(var_name, variable, is_dimmed, var_satisfied)
+                variables_table.add_row(*row_data)
+
+        console.print(variables_table)

+ 60 - 73
cli/core/module.py

@@ -76,19 +76,34 @@ class Module(ABC):
         self.libraries = LibraryManager()
         self.display = DisplayManager()
 
-    def list(
-        self,
-        raw: bool = Option(
-            False, "--raw", help="Output raw list format instead of rich table"
-        ),
-    ) -> list[Template]:
-        """List all templates."""
-        logger.debug(f"Listing templates for module '{self.name}'")
-        templates = []
+    def _load_all_templates(self, filter_fn=None) -> list[Template]:
+        """Load all templates for this module with optional filtering.
 
+        This centralized method eliminates duplicate template loading logic
+        across list(), search(), and validate() methods.
+
+        Args:
+            filter_fn: Optional function to filter templates. Takes Template and
+                      returns bool. Only templates where filter_fn returns True
+                      are included.
+
+        Returns:
+            List of successfully loaded Template objects
+
+        Example:
+            # Load all templates
+            templates = self._load_all_templates()
+
+            # Load only templates matching a search query
+            templates = self._load_all_templates(
+                lambda t: "nginx" in t.id.lower()
+            )
+        """
+        templates = []
         entries = self.libraries.find(self.name, sort_results=True)
+
         for entry in entries:
-            # Unpack entry - now returns (path, library_name, needs_qualification)
+            # Unpack entry - returns (path, library_name, needs_qualification)
             template_dir = entry[0]
             library_name = entry[1]
             needs_qualification = entry[2] if len(entry) > 2 else False
@@ -116,12 +131,27 @@ class Module(ABC):
                 if needs_qualification:
                     template.set_qualified_id()
 
-                templates.append(template)
+                # Apply filter if provided
+                if filter_fn is None or filter_fn(template):
+                    templates.append(template)
+
             except Exception as exc:
                 logger.error(f"Failed to load template from {template_dir}: {exc}")
                 continue
 
-        filtered_templates = templates
+        return templates
+
+    def list(
+        self,
+        raw: bool = Option(
+            False, "--raw", help="Output raw list format instead of rich table"
+        ),
+    ) -> list[Template]:
+        """List all templates."""
+        logger.debug(f"Listing templates for module '{self.name}'")
+        
+        # Load all templates using centralized helper
+        filtered_templates = self._load_all_templates()
 
         if filtered_templates:
             if raw:
@@ -155,45 +185,11 @@ class Module(ABC):
         logger.debug(
             f"Searching templates for module '{self.name}' with query='{query}'"
         )
-        templates = []
-
-        entries = self.libraries.find(self.name, sort_results=True)
-        for entry in entries:
-            # Unpack entry - now returns (path, library_name, needs_qualification)
-            template_dir = entry[0]
-            library_name = entry[1]
-            needs_qualification = entry[2] if len(entry) > 2 else False
-
-            try:
-                # Get library object to determine type
-                library = next(
-                    (
-                        lib
-                        for lib in self.libraries.libraries
-                        if lib.name == library_name
-                    ),
-                    None,
-                )
-                library_type = library.library_type if library else "git"
-
-                template = Template(
-                    template_dir, library_name=library_name, library_type=library_type
-                )
-
-                # Validate schema version compatibility
-                template._validate_schema_version(self.schema_version, self.name)
-
-                # If template ID needs qualification, set qualified ID
-                if needs_qualification:
-                    template.set_qualified_id()
-
-                templates.append(template)
-            except Exception as exc:
-                logger.error(f"Failed to load template from {template_dir}: {exc}")
-                continue
-
-        # Apply search filtering
-        filtered_templates = [t for t in templates if query.lower() in t.id.lower()]
+        
+        # Load templates with search filter using centralized helper
+        filtered_templates = self._load_all_templates(
+            lambda t: query.lower() in t.id.lower()
+        )
 
         if filtered_templates:
             logger.info(
@@ -256,7 +252,7 @@ class Module(ABC):
             if reset_vars:
                 logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
 
-        self._display_template_details(template, id)
+        self.display.display_template(template, id)
 
     def _apply_variable_defaults(self, template: Template) -> None:
         """Apply config defaults and CLI overrides to template variables.
@@ -756,7 +752,7 @@ class Module(ABC):
                 logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
 
         if not quiet:
-            self._display_template_details(template, id)
+            self.display.display_template(template, id)
             console.print()
 
         # Collect variable values
@@ -1241,32 +1237,33 @@ class Module(ABC):
             # Validate all templates
             console.print(f"[bold]Validating all {self.name} templates...[/bold]\n")
 
-            entries = self.libraries.find(self.name, sort_results=True)
-            total = len(entries)
             valid_count = 0
             invalid_count = 0
             errors = []
 
-            for template_dir, library_name, _ in entries:
-                template_id = template_dir.name
+            # Use centralized helper to load all templates
+            # Note: Exceptions during load are already logged by _load_all_templates
+            all_templates = self._load_all_templates()
+            total = len(all_templates)
+
+            for template in all_templates:
                 try:
-                    template = Template(template_dir, library_name=library_name)
                     # Trigger validation
                     _ = template.used_variables
                     _ = template.variables
                     valid_count += 1
                     if verbose:
-                        self.display.display_success(template_id)
+                        self.display.display_success(template.id)
                 except ValueError as e:
                     invalid_count += 1
-                    errors.append((template_id, str(e)))
+                    errors.append((template.id, str(e)))
                     if verbose:
-                        self.display.display_error(template_id)
+                        self.display.display_error(template.id)
                 except Exception as e:
                     invalid_count += 1
-                    errors.append((template_id, f"Load error: {e}"))
+                    errors.append((template.id, f"Load error: {e}"))
                     if verbose:
-                        self.display.display_warning(template_id)
+                        self.display.display_warning(template.id)
 
             # Summary
             summary_items = {
@@ -1382,13 +1379,3 @@ class Module(ABC):
                 f"Template '{id}' could not be loaded: {exc}"
             ) from exc
 
-    def _display_template_details(
-        self, template: Template, id: str
-    ) -> None:
-        """Display template information panel and variables table.
-
-        Args:
-            template: Template instance to display
-            id: Template ID
-        """
-        self.display.display_template_details(template, id)

+ 3 - 30
cli/core/prompt.py

@@ -37,40 +37,13 @@ class PromptHandler:
             set()
         )  # Track which variables we've already prompted for
 
-        # Process each section
-        for section_key, section in variables.get_sections().items():
+        # Process each section (only satisfied dependencies, include disabled for toggle handling)
+        for section_key, section in variables.iter_active_sections(include_disabled=True):
             if not section.variables:
                 continue
 
-            # Check if dependencies are satisfied
-            if not variables.is_section_satisfied(section_key):
-                # Get list of unsatisfied dependencies for better user feedback
-                unsatisfied_keys = [
-                    dep
-                    for dep in section.needs
-                    if not variables.is_section_satisfied(dep)
-                ]
-                # Convert section keys to titles for user-friendly display
-                unsatisfied_titles = []
-                for dep_key in unsatisfied_keys:
-                    dep_section = variables.get_section(dep_key)
-                    if dep_section:
-                        unsatisfied_titles.append(dep_section.title)
-                    else:
-                        unsatisfied_titles.append(dep_key)
-                dep_names = (
-                    ", ".join(unsatisfied_titles) if unsatisfied_titles else "unknown"
-                )
-                self.display.display_skipped(
-                    section.title, f"requires {dep_names} to be enabled"
-                )
-                logger.debug(
-                    f"Skipping section '{section_key}' - dependencies not satisfied: {dep_names}"
-                )
-                continue
-
             # Always show section header first
-            self.display.display_section_header(section.title, section.description)
+            self.display.display_section(section.title, section.description)
 
             # Track whether this section will be enabled
             section_will_be_enabled = True

+ 5 - 4
cli/core/section.py

@@ -4,6 +4,7 @@ from collections import OrderedDict
 from typing import Any, Dict, List, Optional
 
 from .variable import Variable
+from .exceptions import VariableError
 
 
 class VariableSection:
@@ -16,13 +17,13 @@ class VariableSection:
             data: Dictionary containing section specification with required 'key' and 'title' keys
         """
         if not isinstance(data, dict):
-            raise ValueError("VariableSection data must be a dictionary")
+            raise VariableError("VariableSection data must be a dictionary")
 
         if "key" not in data:
-            raise ValueError("VariableSection data must contain 'key'")
+            raise VariableError("VariableSection data must contain 'key'")
 
         if "title" not in data:
-            raise ValueError("VariableSection data must contain 'title'")
+            raise VariableError("VariableSection data must contain 'title'")
 
         self.key: str = data["key"]
         self.title: str = data["title"]
@@ -46,7 +47,7 @@ class VariableSection:
             elif isinstance(needs_value, list):
                 self.needs: List[str] = needs_value
             else:
-                raise ValueError(
+                raise VariableError(
                     f"Section '{self.key}' has invalid 'needs' value: must be string or list"
                 )
         else:

+ 160 - 149
cli/core/template.py

@@ -29,186 +29,197 @@ from jinja2.exceptions import (
 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.
+class TemplateErrorHandler:
+    """Handles parsing and formatting of template rendering errors.
+    
+    This class provides utilities for:
+    - Extracting error context from template files
+    - Generating helpful suggestions based on Jinja2 errors
+    - Parsing Jinja2 exceptions into structured error information
+    """
+
+    @staticmethod
+    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:
+        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
+            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 []
+        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()
+        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)
+            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()}")
+            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 []
+            return context
+        except (IOError, OSError):
+            return []
 
+    @staticmethod
+    def get_common_jinja_suggestions(error_msg: str, available_vars: set) -> List[str]:
+        """Generate helpful suggestions based on common Jinja2 errors.
 
-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
 
-    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()
 
-    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
 
-    # 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)
 
-        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"
+                )
 
-        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])}"
+                    )
 
-            # 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])}"
+                    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(
-                f"Add '{undefined_var}' to your template.yaml spec with a default value"
+                "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(
-                "Or use the Jinja2 default filter: {{ "
-                + undefined_var
-                + " | default('value') }}"
+                "Verify the template path is relative to the template directory"
             )
-        else:
             suggestions.append(
-                "Check that all variables used in templates are defined in template.yaml"
+                "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 the Jinja2 default filter for optional variables: {{ var | default('value') }}"
+                "Use Jinja2 filters to convert types: {{ var | int }}, {{ var | string }}"
             )
 
-    # 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"
-        )
+        # 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
+        return suggestions
 
+    @classmethod
+    def parse_jinja_error(
+        cls,
+        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.
 
-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
 
-    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 = cls.extract_error_context(file_path, line_number)
+
+        # Generate suggestions based on error type
+        if isinstance(error, UndefinedError):
+            error_msg = f"Undefined variable: {error}"
+            suggestions = cls.get_common_jinja_suggestions(str(error), available_vars)
+        elif isinstance(error, Jinja2TemplateSyntaxError):
+            error_msg = f"Template syntax error: {error}"
+            suggestions = cls.get_common_jinja_suggestions(str(error), available_vars)
+        elif isinstance(error, Jinja2TemplateNotFound):
+            error_msg = f"Template file not found: {error}"
+            suggestions = cls.get_common_jinja_suggestions(str(error), available_vars)
+        else:
+            # Generic Jinja2 error
+            suggestions = cls.get_common_jinja_suggestions(error_msg, available_vars)
 
-    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
+        return error_msg, line_number, column, context_lines, suggestions
 
 
 @dataclass
@@ -760,7 +771,7 @@ class Template:
                 ) as e:
                     # Parse Jinja2 error to extract detailed information
                     error_msg, line_num, col, context_lines, suggestions = (
-                        _parse_jinja_error(
+                        TemplateErrorHandler.parse_jinja_error(
                             e, template_file, self.template_dir, available_vars
                         )
                     )

+ 10 - 5
cli/core/variable.py

@@ -1,10 +1,15 @@
 from __future__ import annotations
 
-from typing import Any, Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
 from urllib.parse import urlparse
 import logging
 import re
 
+from .exceptions import VariableValidationError, VariableError
+
+if TYPE_CHECKING:
+    from .section import VariableSection
+
 logger = logging.getLogger(__name__)
 
 TRUE_VALUES = {"true", "1", "yes", "on"}
@@ -27,10 +32,10 @@ class Variable:
         """
         # Validate input
         if not isinstance(data, dict):
-            raise ValueError("Variable data must be a dictionary")
+            raise VariableError("Variable data must be a dictionary")
 
         if "name" not in data:
-            raise ValueError("Variable data must contain 'name' key")
+            raise VariableError("Variable data must contain 'name' key")
 
         # Track which fields were explicitly provided in source data
         self._explicit_fields: Set[str] = set(data.keys())
@@ -76,7 +81,7 @@ class Variable:
             elif isinstance(needs_value, list):
                 self.needs: List[str] = needs_value
             else:
-                raise ValueError(
+                raise VariableError(
                     f"Variable '{self.name}' has invalid 'needs' value: must be string or list"
                 )
         else:
@@ -87,7 +92,7 @@ class Variable:
             try:
                 self.value = self.convert(self.value)
             except ValueError as exc:
-                raise ValueError(f"Invalid default for variable '{self.name}': {exc}")
+                raise VariableValidationError(self.name, f"Invalid default value: {exc}")
 
     def convert(self, value: Any) -> Any:
         """Validate and convert a raw value based on the variable type.