Sfoglia il codice sorgente

started developing new functions

xcad 4 mesi fa
parent
commit
e84d80371f

+ 9 - 1
CHANGELOG.md

@@ -10,10 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 - Variable file support with `--var-file` flag (#1331) - Load variables from YAML file for non-interactive deployments
 - Variable override support for `show` command with `--var` and `--var-file` flags (#1421) - Preview variable overrides before generating
+- Terraform template support (#1422) - Manage Terraform configurations with schema 1.0
+- Kubernetes template support (#1423) - Manage Kubernetes configurations with schema 1.0
+- Helm template support (#1424) - Manage Helm charts with schema 1.0
+- Ansible template support (#1426) - Manage Ansible playbooks with schema 1.0
+- Packer template support (#1427) - Manage Packer templates with schema 1.0
+- Alphabetically sorted commands in help output with grouped panels for better organization
+- Separate help panels for "Template Commands" and "Configuration Commands"
 
 ### Changed
 - Removed Jinja2 `| default()` filter extraction and merging (#1410) - All defaults must now be defined in template/module specs
-- Refactored code quality (#1364) for all core modules from single files to package structure with specific submodules 
+- Refactored code quality (#1364) for all core modules from single files to package structure with specific submodules
+- Improved debug logging to capture module discovery and registration during initialization
 
 ## [0.0.7] - 2025-10-28
 

+ 32 - 7
cli/__main__.py

@@ -15,20 +15,32 @@ from pathlib import Path
 import click
 from rich.console import Console
 from typer import Option, Typer
+from typer.core import TyperGroup
 
 import cli.modules
 from cli import __version__
 from cli.core import repo
+from cli.core.display import DisplayManager
 from cli.core.registry import registry
 
-# Using standard Python exceptions instead of custom ones
+
+class OrderedGroup(TyperGroup):
+    """Typer Group that lists commands in alphabetical order."""
+
+    def list_commands(self, ctx: click.Context) -> list[str]:
+        return sorted(super().list_commands(ctx))
+
 
 app = Typer(
     help="CLI tool for managing infrastructure boilerplates.\n\n[dim]Easily generate, customize, and deploy templates for Docker Compose, Terraform, Kubernetes, and more.\n\n [white]Made with 💜 by [bold]Christian Lempa[/bold]",
     add_completion=True,
     rich_markup_mode="rich",
+    pretty_exceptions_enable=False,
+    no_args_is_help=True,
+    cls=OrderedGroup,
 )
 console = Console()
+display = DisplayManager()
 
 
 def setup_logging(log_level: str = "WARNING") -> None:
@@ -151,7 +163,7 @@ def _register_module_classes(logger: logging.Logger) -> tuple[list, list[str]]:
             error_info = f"Registration failed for '{module_cls.__name__}': {e!s}"
             failed_registrations.append(error_info)
             logger.warning(error_info)
-            console.print(f"[yellow]Warning:[/yellow] {error_info}")
+            display.warning(error_info)
 
     return module_classes, failed_registrations
 
@@ -180,6 +192,8 @@ def init_app() -> None:
         RuntimeError: If application initialization fails
     """
     logger = logging.getLogger(__name__)
+    failed_imports = []
+    failed_registrations = []
 
     try:
         # Auto-discover and import all modules
@@ -215,25 +229,36 @@ def init_app() -> None:
 
 def run() -> None:
     """Run the CLI application."""
+    # Configure logging early if --log-level is provided
+    if "--log-level" in sys.argv:
+        try:
+            log_level_index = sys.argv.index("--log-level") + 1
+            if log_level_index < len(sys.argv):
+                log_level = sys.argv[log_level_index]
+                logging.disable(logging.NOTSET)
+                setup_logging(log_level)
+        except (ValueError, IndexError):
+            pass  # Let Typer handle argument parsing errors
+
     try:
         init_app()
         app()
     except (ValueError, RuntimeError) as e:
         # Handle configuration and initialization errors cleanly
-        console.print(f"[bold red]Error:[/bold red] {e}")
+        display.error(str(e))
         sys.exit(1)
     except ImportError as e:
         # Handle module import errors with detailed info
-        console.print(f"[bold red]Module Import Error:[/bold red] {e}")
+        display.error(f"Module Import Error: {e}")
         sys.exit(1)
     except KeyboardInterrupt:
         # Handle Ctrl+C gracefully
-        console.print("\n[yellow]Operation cancelled by user[/yellow]")
+        display.warning("Operation cancelled by user")
         sys.exit(130)
     except Exception as e:
         # Handle unexpected errors - show simplified message
-        console.print(f"[bold red]Unexpected error:[/bold red] {e}")
-        console.print("[dim]Use --log-level DEBUG for more details[/dim]")
+        display.error(str(e))
+        display.info("Use --log-level DEBUG for more details")
         sys.exit(1)
 
 

+ 128 - 4
cli/core/display/__init__.py

@@ -1,22 +1,146 @@
 """Display module for CLI output rendering.
 
-This package provides centralized display management with specialized managers
-for different types of output (variables, templates, status messages, tables).
+This package provides centralized display management with mixin-based architecture.
+DisplayManager inherits from multiple mixins to provide a flat, cohesive API.
 """
 
 from __future__ import annotations
 
 from rich.console import Console
 
-from .display_manager import DisplayManager
+from .display_base import BaseDisplay
+from .display_icons import IconManager
 from .display_settings import DisplaySettings
-from .icon_manager import IconManager
+from .display_status import StatusDisplay
+from .display_table import TableDisplay
+from .display_template import TemplateDisplay
+from .display_variable import VariableDisplay
 
 # Console instances for stdout and stderr
 console = Console()
 console_err = Console(stderr=True)
 
 
+class DisplayManager:
+    """Main display coordinator using composition.
+
+    This class composes specialized display components to provide a unified API.
+    Each component handles a specific concern (status, tables, templates, variables).
+
+    Design Principles:
+    - Composition over inheritance
+    - Explicit dependencies
+    - Clear separation of concerns
+    - Easy to test and extend
+    """
+
+    def __init__(self, quiet: bool = False, settings: DisplaySettings | None = None):
+        """Initialize DisplayManager with composed display components.
+
+        Args:
+            quiet: If True, suppress all non-error output
+            settings: Optional DisplaySettings instance for customization
+        """
+        self.quiet = quiet
+        self.settings = settings or DisplaySettings()
+
+        # Create base display component (includes utilities)
+        self.base = BaseDisplay(self.settings, quiet)
+
+        # Create specialized display components
+        self.status = StatusDisplay(self.settings, quiet, self.base)
+        self.variables = VariableDisplay(self.settings, self.base)
+        self.templates = TemplateDisplay(self.settings, self.base, self.variables)
+        self.tables = TableDisplay(self.settings, self.base)
+
+    # ===== Delegate to base display =====
+    def text(self, text: str, style: str | None = None) -> None:
+        """Display plain text."""
+        return self.base.text(text, style)
+
+    def heading(self, text: str, style: str | None = None) -> None:
+        """Display a heading."""
+        return self.base.heading(text, style)
+
+    def table(
+        self,
+        headers: list[str] | None = None,
+        rows: list[tuple] | None = None,
+        title: str | None = None,
+        show_header: bool = True,
+        borderless: bool = False,
+    ) -> None:
+        """Display a table."""
+        return self.base.table(headers, rows, title, show_header, borderless)
+
+    def tree(self, root_label: str, nodes: dict | list) -> None:
+        """Display a tree."""
+        return self.base.tree(root_label, nodes)
+
+    def code(self, code_text: str, language: str | None = None) -> None:
+        """Display code."""
+        return self.base.code(code_text, language)
+
+    def progress(self, *columns):
+        """Create a progress bar."""
+        return self.base.progress(*columns)
+
+    def file_tree(
+        self,
+        root_label: str,
+        files: list,
+        file_info_fn: callable,
+        title: str | None = None,
+    ) -> None:
+        """Display a file tree structure."""
+        return self.base.file_tree(root_label, files, file_info_fn, title)
+
+    # ===== Formatting utilities =====
+    def truncate(self, value: str, max_length: int | None = None) -> str:
+        """Truncate string value."""
+        return self.base.truncate(value, max_length)
+
+    def format_file_size(self, size_bytes: int) -> str:
+        """Format file size in human-readable format."""
+        return self.base.format_file_size(size_bytes)
+
+    def data_table(
+        self,
+        columns: list[dict],
+        rows: list,
+        title: str | None = None,
+        row_formatter: callable | None = None,
+    ) -> None:
+        """Display a data table with configurable columns."""
+        return self.tables.data_table(columns, rows, title, row_formatter)
+
+    # ===== Delegate to status display =====
+    def error(self, message: str, context: str | None = None) -> None:
+        """Display an error message."""
+        return self.status.error(message, context)
+
+    def warning(self, message: str, context: str | None = None) -> None:
+        """Display a warning message."""
+        return self.status.warning(message, context)
+
+    def success(self, message: str, context: str | None = None) -> None:
+        """Display a success message."""
+        return self.status.success(message, context)
+
+    def info(self, message: str, context: str | None = None) -> None:
+        """Display an info message."""
+        return self.status.info(message, context)
+
+    def skipped(self, message: str, reason: str | None = None) -> None:
+        """Display skipped message."""
+        return self.status.skipped(message, reason)
+
+    # ===== Helper methods =====
+    def get_lock_icon(self) -> str:
+        """Get lock icon."""
+        return self.base.get_lock_icon()
+
+
 # Export public API
 __all__ = [
     "DisplayManager",

+ 304 - 0
cli/core/display/display_base.py

@@ -0,0 +1,304 @@
+"""Base display methods for DisplayManager."""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+from rich.console import Console
+from rich.progress import Progress
+from rich.syntax import Syntax
+from rich.table import Table
+from rich.tree import Tree
+
+from .display_icons import IconManager
+from .display_settings import DisplaySettings
+
+logger = logging.getLogger(__name__)
+console = Console()
+
+
+class BaseDisplay:
+    """Base display methods and utilities.
+
+    Provides fundamental display methods (text, heading, table, tree, code, progress)
+    and utility/helper methods for formatting.
+    """
+
+    def __init__(self, settings: DisplaySettings, quiet: bool = False):
+        """Initialize BaseDisplay.
+
+        Args:
+            settings: Display settings for formatting
+            quiet: If True, suppress non-error output
+        """
+        self.settings = settings
+        self.quiet = quiet
+
+    def heading(self, text: str, style: str | None = None) -> None:
+        """Display a standardized heading.
+
+        Args:
+            text: Heading text
+            style: Optional style override (defaults to STYLE_HEADER from settings)
+        """
+        if style is None:
+            style = self.settings.STYLE_HEADER
+        console.print(f"[{style}]{text}[/{style}]")
+
+    def text(self, text: str, style: str | None = None) -> None:
+        """Display plain text with optional styling.
+
+        Args:
+            text: Text to display
+            style: Optional Rich style markup
+        """
+        if style:
+            console.print(f"[{style}]{text}[/{style}]")
+        else:
+            console.print(text)
+
+    def table(
+        self,
+        headers: list[str] | None = None,
+        rows: list[tuple] | None = None,
+        title: str | None = None,
+        show_header: bool = True,
+        borderless: bool = False,
+    ) -> None:
+        """Display a standardized table.
+
+        Args:
+            headers: Column headers (if None, no headers)
+            rows: List of tuples, one per row
+            title: Optional table title
+            show_header: Whether to show header row
+            borderless: If True, use borderless style (box=None)
+        """
+        table = Table(
+            title=title,
+            show_header=show_header and headers is not None,
+            header_style=self.settings.STYLE_TABLE_HEADER,
+            box=None,
+            padding=self.settings.PADDING_TABLE_NORMAL if borderless else (0, 1),
+        )
+
+        # Add columns
+        if headers:
+            for header in headers:
+                table.add_column(header)
+        elif rows and len(rows) > 0:
+            # No headers, but need columns for data
+            for _ in range(len(rows[0])):
+                table.add_column()
+
+        # Add rows
+        if rows:
+            for row in rows:
+                table.add_row(*[str(cell) for cell in row])
+
+        console.print(table)
+
+    def tree(self, root_label: str, nodes: dict | list | Tree) -> None:
+        """Display a tree structure.
+
+        Args:
+            root_label: Label for the root node
+            nodes: Hierarchical structure (dict, list, or pre-built Tree)
+        """
+        if isinstance(nodes, Tree):
+            console.print(nodes)
+        else:
+            tree = Tree(root_label)
+            self._build_tree_nodes(tree, nodes)
+            console.print(tree)
+
+    def _build_tree_nodes(self, parent, nodes):
+        """Recursively build tree nodes.
+
+        Args:
+            parent: Parent tree node
+            nodes: Dict or list of child nodes
+        """
+        if isinstance(nodes, dict):
+            for key, value in nodes.items():
+                if isinstance(value, (dict, list)):
+                    branch = parent.add(str(key))
+                    self._build_tree_nodes(branch, value)
+                else:
+                    parent.add(f"{key}: {value}")
+        elif isinstance(nodes, list):
+            for item in nodes:
+                if isinstance(item, (dict, list)):
+                    self._build_tree_nodes(parent, item)
+                else:
+                    parent.add(str(item))
+
+    def _print_tree(self, tree) -> None:
+        """Print a pre-built Rich Tree object.
+
+        Args:
+            tree: Rich Tree object to print
+        """
+        console.print(tree)
+
+    def _print_table(self, table) -> None:
+        """Print a pre-built Rich Table object.
+
+        Enforces consistent header styling for all tables.
+
+        Args:
+            table: Rich Table object to print
+        """
+        # Enforce consistent header style for all tables
+        table.header_style = self.settings.STYLE_TABLE_HEADER
+        console.print(table)
+
+    def code(self, code_text: str, language: str | None = None) -> None:
+        """Display code with optional syntax highlighting.
+
+        Args:
+            code_text: Code to display
+            language: Programming language for syntax highlighting
+        """
+        if language:
+            syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
+            console.print(syntax)
+        else:
+            # Plain code block without highlighting
+            console.print(f"[dim]{code_text}[/dim]")
+
+    def progress(self, *columns):
+        """Create a Rich Progress context manager with standardized console.
+
+        Args:
+            *columns: Progress columns (e.g., SpinnerColumn(), TextColumn())
+
+        Returns:
+            Progress context manager
+
+        Example:
+            with display.progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
+                task = progress.add_task("Processing...", total=None)
+                # do work
+                progress.remove_task(task)
+        """
+        return Progress(*columns, console=console)
+
+
+    # ===== Formatting Utilities =====
+
+
+    def truncate(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"
+
+    def file_tree(
+        self,
+        root_label: str,
+        files: list,
+        file_info_fn: callable,
+        title: str | None = None,
+    ) -> None:
+        """Display a file tree structure.
+
+        Args:
+            root_label: Label for root node (e.g., "📁 my-project")
+            files: List of file items to display
+            file_info_fn: Function that takes a file and returns (path, display_name, color, extra_text)
+                         - path: Path object for directory structure
+                         - display_name: Name to show for the file
+                         - color: Rich color for the filename
+                         - extra_text: Optional additional text (e.g., "(will overwrite)")
+            title: Optional heading to display before tree
+        """
+        if title:
+            self.heading(title)
+
+        tree = Tree(root_label)
+        tree_nodes = {Path("."): tree}
+
+        for file_item in sorted(files, key=lambda f: file_info_fn(f)[0]):
+            path, display_name, color, extra_text = file_info_fn(file_item)
+            parts = path.parts
+            current_path = Path(".")
+            current_node = 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)
+
+        console.print(tree)
+
+    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 get_lock_icon(self) -> str:
+        """Get the lock icon for sensitive variables.
+
+        Returns:
+            Lock icon unicode character
+        """
+        return IconManager.lock()

+ 0 - 0
cli/core/display/icon_manager.py → cli/core/display/display_icons.py


+ 0 - 482
cli/core/display/display_manager.py

@@ -1,482 +0,0 @@
-"""Main display coordinator for the CLI."""
-
-from __future__ import annotations
-
-import logging
-from pathlib import Path
-from typing import TYPE_CHECKING
-
-from jinja2 import Template as Jinja2Template
-from rich.console import Console
-from rich.progress import Progress
-from rich.syntax import Syntax
-from rich.table import Table
-from rich.tree import Tree
-
-from .display_settings import DisplaySettings
-from .icon_manager import IconManager
-from .status_display import StatusDisplayManager
-from .table_display import TableDisplayManager
-from .template_display import TemplateDisplayManager
-from .variable_display import VariableDisplayManager
-
-if TYPE_CHECKING:
-    from ..exceptions import TemplateRenderError
-    from ..template import Template
-
-logger = logging.getLogger(__name__)
-console = Console()
-
-
-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 heading(self, text: str, style: str | None = None) -> None:
-        """Display a standardized heading.
-
-        Args:
-            text: Heading text
-            style: Optional style override (defaults to STYLE_HEADER from settings)
-        """
-        if style is None:
-            style = self.settings.STYLE_HEADER
-        console.print(f"[{style}]{text}[/{style}]")
-
-    def text(self, text: str, style: str | None = None) -> None:
-        """Display plain text with optional styling.
-
-        Args:
-            text: Text to display
-            style: Optional Rich style markup
-        """
-        if style:
-            console.print(f"[{style}]{text}[/{style}]")
-        else:
-            console.print(text)
-
-    def error(self, text: str, style: str | None = None) -> None:
-        """Display error text to stderr.
-
-        Args:
-            text: Error text to display
-            style: Optional Rich style markup (defaults to red)
-        """
-        console_err = Console(stderr=True)
-        if style is None:
-            style = "red"
-        console_err.print(f"[{style}]{text}[/{style}]")
-
-    def warning(self, text: str, style: str | None = None) -> None:
-        """Display warning text to stderr.
-
-        Args:
-            text: Warning text to display
-            style: Optional Rich style markup (defaults to yellow)
-        """
-        console_err = Console(stderr=True)
-        if style is None:
-            style = "yellow"
-        console_err.print(f"[{style}]{text}[/{style}]")
-
-    def table(
-        self,
-        headers: list[str] | None = None,
-        rows: list[tuple] | None = None,
-        title: str | None = None,
-        show_header: bool = True,
-        borderless: bool = False,
-    ) -> None:
-        """Display a standardized table.
-
-        Args:
-            headers: Column headers (if None, no headers)
-            rows: List of tuples, one per row
-            title: Optional table title
-            show_header: Whether to show header row
-            borderless: If True, use borderless style (box=None)
-        """
-        table = Table(
-            title=title,
-            show_header=show_header and headers is not None,
-            header_style=self.settings.STYLE_TABLE_HEADER,
-            box=None,
-            padding=self.settings.PADDING_TABLE_NORMAL if borderless else (0, 1),
-        )
-
-        # Add columns
-        if headers:
-            for header in headers:
-                table.add_column(header)
-        elif rows and len(rows) > 0:
-            # No headers, but need columns for data
-            for _ in range(len(rows[0])):
-                table.add_column()
-
-        # Add rows
-        if rows:
-            for row in rows:
-                table.add_row(*[str(cell) for cell in row])
-
-        console.print(table)
-
-    def tree(self, root_label: str, nodes: dict | list) -> None:
-        """Display a tree structure.
-
-        Args:
-            root_label: Label for the root node
-            nodes: Hierarchical structure (dict or list)
-        """
-        tree = Tree(root_label)
-        self._build_tree_nodes(tree, nodes)
-        console.print(tree)
-
-    def _build_tree_nodes(self, parent, nodes):
-        """Recursively build tree nodes.
-
-        Args:
-            parent: Parent tree node
-            nodes: Dict or list of child nodes
-        """
-        if isinstance(nodes, dict):
-            for key, value in nodes.items():
-                if isinstance(value, (dict, list)):
-                    branch = parent.add(str(key))
-                    self._build_tree_nodes(branch, value)
-                else:
-                    parent.add(f"{key}: {value}")
-        elif isinstance(nodes, list):
-            for item in nodes:
-                if isinstance(item, (dict, list)):
-                    self._build_tree_nodes(parent, item)
-                else:
-                    parent.add(str(item))
-
-    def _print_tree(self, tree) -> None:
-        """Print a pre-built Rich Tree object.
-
-        Args:
-            tree: Rich Tree object to print
-        """
-        console.print(tree)
-
-    def _print_table(self, table) -> None:
-        """Print a pre-built Rich Table object.
-
-        Enforces consistent header styling for all tables.
-
-        Args:
-            table: Rich Table object to print
-        """
-        # Enforce consistent header style for all tables
-        table.header_style = self.settings.STYLE_TABLE_HEADER
-        console.print(table)
-
-    def code(self, code_text: str, language: str | None = None) -> None:
-        """Display code with optional syntax highlighting.
-
-        Args:
-            code_text: Code to display
-            language: Programming language for syntax highlighting
-        """
-        if language:
-            syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
-            console.print(syntax)
-        else:
-            # Plain code block without highlighting
-            console.print(f"[dim]{code_text}[/dim]")
-
-    def progress(self, *columns):
-        """Create a Rich Progress context manager with standardized console.
-
-        Args:
-            *columns: Progress columns (e.g., SpinnerColumn(), TextColumn())
-
-        Returns:
-            Progress context manager
-
-        Example:
-            with display.progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
-                task = progress.add_task("Processing...", total=None)
-                # do work
-                progress.remove_task(task)
-        """
-        return Progress(*columns, console=console)
-
-    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:
-            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)

+ 143 - 0
cli/core/display/display_status.py

@@ -0,0 +1,143 @@
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+from rich.console import Console
+
+from .display_icons import IconManager
+from .display_settings import DisplaySettings
+
+if TYPE_CHECKING:
+    from .display_base import BaseDisplay
+
+logger = logging.getLogger(__name__)
+console_err = Console(stderr=True)  # Keep for error output
+
+
+class StatusDisplay:
+    """Status messages and error display.
+
+    Provides methods for displaying success, error, warning,
+    and informational messages with consistent formatting.
+    """
+
+    def __init__(self, settings: DisplaySettings, quiet: bool, base: BaseDisplay):
+        """Initialize StatusDisplay.
+
+        Args:
+            settings: Display settings for formatting
+            quiet: If True, suppress non-error output
+            base: BaseDisplay instance
+        """
+        self.settings = settings
+        self.quiet = quiet
+        self.base = base
+
+    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
+        use_stderr = level in ("error", "warning")
+        should_print = use_stderr or not self.quiet
+
+        if not should_print:
+            return
+
+        settings = self.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 in {"error", "warning"}
+                else f"{context}: {message}"
+            )
+        else:
+            text = (
+                f"{level.capitalize()}: {message}"
+                if level in {"error", "warning"}
+                else message
+            )
+
+        formatted_text = f"[{color}]{icon} {text}[/{color}]"
+        if use_stderr:
+            console_err.print(formatted_text)
+        else:
+            self.base.text(formatted_text)
+
+        # 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 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 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 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 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 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:
+            self.base.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
+        else:
+            self.base.text(f"\n{icon} {message} (skipped)", style="dim")
+

+ 71 - 26
cli/core/display/table_display.py → cli/core/display/display_table.py

@@ -6,28 +6,73 @@ from typing import TYPE_CHECKING
 from rich.table import Table
 from rich.tree import Tree
 
-from .icon_manager import IconManager
+from .display_icons import IconManager
+from .display_settings import DisplaySettings
 
 if TYPE_CHECKING:
-    from . import DisplayManager
+    from .display_base import BaseDisplay
 
 logger = logging.getLogger(__name__)
 
 
-class TableDisplayManager:
-    """Handles table rendering.
+class TableDisplay:
+    """Table rendering.
 
-    This manager is responsible for displaying various types of tables
-    including templates lists, status tables, and summaries.
+    Provides methods for displaying data tables with flexible formatting.
     """
 
-    def __init__(self, parent: DisplayManager):
-        """Initialize TableDisplayManager.
+    def __init__(self, settings: DisplaySettings, base: BaseDisplay):
+        """Initialize TableDisplay.
 
         Args:
-            parent: Reference to parent DisplayManager for accessing shared resources
+            settings: Display settings for formatting
+            base: BaseDisplay instance for utility methods
         """
-        self.parent = parent
+        self.settings = settings
+        self.base = base
+
+    def data_table(
+        self,
+        columns: list[dict],
+        rows: list,
+        title: str | None = None,
+        row_formatter: callable | None = None,
+    ) -> None:
+        """Display a data table with configurable columns and formatting.
+
+        Args:
+            columns: List of column definitions, each dict with:
+                     - name: Column header text
+                     - style: Optional Rich style (e.g., "bold", "cyan")
+                     - no_wrap: Optional bool to prevent text wrapping
+                     - justify: Optional justify ("left", "right", "center")
+            rows: List of data rows (dicts, tuples, or objects)
+            title: Optional table title
+            row_formatter: Optional function(row) -> tuple to transform row data
+        """
+        table = Table(title=title, show_header=True)
+
+        # Add columns
+        for col in columns:
+            table.add_column(
+                col["name"],
+                style=col.get("style"),
+                no_wrap=col.get("no_wrap", False),
+                justify=col.get("justify", "left"),
+            )
+
+        # Add rows
+        for row in rows:
+            if row_formatter:
+                formatted_row = row_formatter(row)
+            elif isinstance(row, dict):
+                formatted_row = tuple(str(row.get(col["name"], "")) for col in columns)
+            else:
+                formatted_row = tuple(str(cell) for cell in row)
+
+            table.add_row(*formatted_row)
+
+        self.base._print_table(table)
 
     def render_templates_table(
         self, templates: list, module_name: str, title: str
@@ -52,7 +97,7 @@ class TableDisplayManager:
         table.add_column("Schema", no_wrap=True)
         table.add_column("Library", no_wrap=True)
 
-        settings = self.parent.settings
+        settings = self.settings
 
         for template in templates:
             name = template.metadata.name or settings.TEXT_UNNAMED_TEMPLATE
@@ -67,16 +112,16 @@ class TableDisplayManager:
                 else "1.0"
             )
 
-            # Use helper for library display
+            # Format library with icon and color
             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
-            )
+            icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
+            color = "yellow" if library_type == "static" else "blue"
+            library_display = f"[{color}]{icon} {library_name}[/{color}]"
 
             table.add_row(template.id, name, tags, version, schema, library_display)
 
-        self.parent._print_table(table)
+        self.base._print_table(table)
 
     def render_status_table(
         self,
@@ -102,7 +147,7 @@ class TableDisplayManager:
                 name, f"[{status_style}]{status_icon} {message}[/{status_style}]"
             )
 
-        self.parent._print_table(table)
+        self.base._print_table(table)
 
     def render_summary_table(self, title: str, items: dict[str, str]) -> None:
         """Display a simple two-column summary table.
@@ -111,7 +156,7 @@ class TableDisplayManager:
             title: Table title
             items: Dictionary of key-value pairs to display
         """
-        settings = self.parent.settings
+        settings = self.settings
         table = Table(
             title=title,
             show_header=False,
@@ -124,7 +169,7 @@ class TableDisplayManager:
         for key, value in items.items():
             table.add_row(key, value)
 
-        self.parent._print_table(table)
+        self.base._print_table(table)
 
     def render_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
         """Display a table of file operations with sizes and statuses.
@@ -132,7 +177,7 @@ class TableDisplayManager:
         Args:
             files: List of tuples (file_path, size_bytes, status)
         """
-        settings = self.parent.settings
+        settings = self.settings
         table = Table(
             show_header=True,
             header_style=settings.STYLE_TABLE_HEADER,
@@ -144,10 +189,10 @@ class TableDisplayManager:
         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)
+            size_str = self.base.format_file_size(size_bytes)
             table.add_row(str(file_path), size_str, status)
 
-        self.parent._print_table(table)
+        self.base._print_table(table)
 
     def _build_section_label(
         self,
@@ -193,10 +238,10 @@ class TableDisplayManager:
         label = f"[green]{var_name}[/green] [dim]({var_type})[/dim]"
 
         if var_default is not None and var_default != "":
-            settings = self.parent.settings
+            settings = self.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 = self.base.truncate(
                     display_val, settings.VALUE_MAX_LENGTH_DEFAULT
                 )
             label += (
@@ -233,7 +278,7 @@ class TableDisplayManager:
             show_all: If True, show all details including descriptions
         """
         if not spec:
-            self.parent.text(
+            self.base.text(
                 f"No configuration found for module '{module_name}'", style="yellow"
             )
             return
@@ -258,4 +303,4 @@ class TableDisplayManager:
             if section_vars:
                 self._add_section_variables(section_node, section_vars, show_all)
 
-        self.parent._print_tree(tree)
+        self.base._print_tree(tree)

+ 40 - 33
cli/core/display/template_display.py → cli/core/display/display_template.py

@@ -3,27 +3,38 @@ from __future__ import annotations
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-from .icon_manager import IconManager
+from .display_icons import IconManager
+from .display_settings import DisplaySettings
 
 if TYPE_CHECKING:
     from ..template import Template
-    from . import DisplayManager
+    from .display_base import BaseDisplay
+    from .display_variable import VariableDisplay
 
 
-class TemplateDisplayManager:
-    """Handles all template-related rendering.
+class TemplateDisplay:
+    """Template-related rendering.
 
-    This manager is responsible for displaying template information,
+    Provides methods for displaying template information,
     file trees, and metadata.
     """
 
-    def __init__(self, parent: DisplayManager):
-        """Initialize TemplateDisplayManager.
+    def __init__(
+        self,
+        settings: DisplaySettings,
+        base: BaseDisplay,
+        variables: VariableDisplay,
+    ):
+        """Initialize TemplateDisplay.
 
         Args:
-            parent: Reference to parent DisplayManager for accessing shared resources
+            settings: Display settings for formatting
+            base: BaseDisplay instance
+            variables: VariableDisplay instance for rendering variables
         """
-        self.parent = parent
+        self.settings = settings
+        self.base = base
+        self.variables = variables
 
     def render_template(self, template: Template, template_id: str) -> None:
         """Display template information panel and variables table.
@@ -34,7 +45,7 @@ class TemplateDisplayManager:
         """
         self.render_template_header(template, template_id)
         self.render_file_tree(template)
-        self.parent.variables.render_variables_table(template)
+        self.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.
@@ -43,7 +54,7 @@ class TemplateDisplayManager:
             template: Template instance
             template_id: ID of the template
         """
-        settings = self.parent.settings
+        settings = self.settings
 
         template_name = template.metadata.name or settings.TEXT_UNNAMED_TEMPLATE
         version = (
@@ -56,18 +67,18 @@ class TemplateDisplayManager:
         )
         description = template.metadata.description or settings.TEXT_NO_DESCRIPTION
 
-        # Get library information and format with helper
+        # Get library information and format with icon/color
         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
-        )
+        icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
+        color = "yellow" if library_type == "static" else "blue"
+        library_display = f"[{color}]{icon} {library_name}[/{color}]"
 
-        self.parent.text(
+        self.base.text(
             f"{template_name} ({template_id} - [cyan]{version}[/cyan] - [magenta]schema {schema}[/magenta]) {library_display}",
             style=settings.STYLE_HEADER,
         )
-        self.parent.text(description)
+        self.base.text(description)
 
     def render_file_tree(self, template: Template) -> None:
         """Display the file structure of a template.
@@ -75,8 +86,8 @@ class TemplateDisplayManager:
         Args:
             template: Template instance
         """
-        self.parent.text("")
-        self.parent.heading("Template File Structure")
+        self.base.text("")
+        self.base.heading("Template File Structure")
 
         def get_template_file_info(template_file):
             display_name = (
@@ -86,14 +97,12 @@ class TemplateDisplayManager:
             )
             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:
-            self.parent._print_tree(file_tree)
+        if template.template_files:
+            self.base.file_tree(
+                f"{IconManager.folder()} [white]{template.id}[/white]",
+                template.template_files,
+                get_template_file_info,
+            )
 
     def render_file_generation_confirmation(
         self,
@@ -108,8 +117,8 @@ class TemplateDisplayManager:
             files: Dictionary of file paths to content
             existing_files: List of existing files that will be overwritten
         """
-        self.parent.text("")
-        self.parent.text("Files to be generated:", style="bold")
+        self.base.text("")
+        self.base.text("Files to be generated:", style="bold")
 
         def get_file_generation_info(file_path_str):
             file_path = Path(file_path_str)
@@ -121,11 +130,9 @@ class TemplateDisplayManager:
             else:
                 return (file_path, file_name, "green", None)
 
-        file_tree = self.parent._render_file_tree_internal(
+        self.base.file_tree(
             f"{IconManager.folder()} [cyan]{output_dir.resolve()}[/cyan]",
             files.keys(),
             get_file_generation_info,
         )
-
-        self.parent._print_tree(file_tree)
-        self.parent.text("")
+        self.base.text("")

+ 26 - 23
cli/core/display/variable_display.py → cli/core/display/display_variable.py

@@ -4,27 +4,30 @@ from typing import TYPE_CHECKING
 
 from rich.table import Table
 
-from .icon_manager import IconManager
+from .display_icons import IconManager
+from .display_settings import DisplaySettings
 
 if TYPE_CHECKING:
     from ..template import Template
-    from . import DisplayManager
+    from .display_base import BaseDisplay
 
 
-class VariableDisplayManager:
-    """Handles all variable-related rendering.
+class VariableDisplay:
+    """Variable-related rendering.
 
-    This manager is responsible for displaying variables, sections,
+    Provides methods for displaying variables, sections,
     and their values with appropriate formatting based on context.
     """
 
-    def __init__(self, parent: DisplayManager):
-        """Initialize VariableDisplayManager.
+    def __init__(self, settings: DisplaySettings, base: BaseDisplay):
+        """Initialize VariableDisplay.
 
         Args:
-            parent: Reference to parent DisplayManager for accessing shared resources
+            settings: Display settings for formatting
+            base: BaseDisplay instance
         """
-        self.parent = parent
+        self.settings = settings
+        self.base = base
 
     def render_variable_value(
         self,
@@ -61,7 +64,7 @@ class VariableDisplayManager:
             and hasattr(variable, "_original_stored")
             and variable.original_value != variable.value
         ):
-            settings = self.parent.settings
+            settings = self.settings
             orig = self._format_value(
                 variable,
                 variable.original_value,
@@ -81,7 +84,7 @@ class VariableDisplayManager:
             return f"{orig} [bold {settings.COLOR_WARNING}]{IconManager.arrow_right()} {curr}[/bold {settings.COLOR_WARNING}]"
 
         # Default formatting
-        settings = self.parent.settings
+        settings = self.settings
         value = variable.get_display_value(
             mask_sensitive=True,
             max_length=settings.VALUE_MAX_LENGTH_DEFAULT,
@@ -102,7 +105,7 @@ class VariableDisplayManager:
         Returns:
             Formatted value string
         """
-        settings = self.parent.settings
+        settings = self.settings
 
         if variable.sensitive:
             return settings.SENSITIVE_MASK
@@ -110,7 +113,7 @@ class VariableDisplayManager:
             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)
+        return self.base.truncate(val_str, max_length)
 
     def render_section(self, title: str, description: str | None) -> None:
         """Display a section header.
@@ -119,15 +122,15 @@ class VariableDisplayManager:
             title: Section title
             description: Optional section description
         """
-        settings = self.parent.settings
+        settings = self.settings
         if description:
-            self.parent.text(
+            self.base.text(
                 f"\n{title} - {description}",
                 style=f"{settings.STYLE_SECTION_TITLE} {settings.STYLE_SECTION_DESC}",
             )
         else:
-            self.parent.text(f"\n{title}", style=settings.STYLE_SECTION_TITLE)
-        self.parent.text(
+            self.base.text(f"\n{title}", style=settings.STYLE_SECTION_TITLE)
+        self.base.text(
             settings.SECTION_SEPARATOR_CHAR * settings.SECTION_SEPARATOR_LENGTH,
             style=settings.COLOR_MUTED,
         )
@@ -142,7 +145,7 @@ class VariableDisplayManager:
         Returns:
             Formatted header text with Rich markup
         """
-        settings = self.parent.settings
+        settings = self.settings
         # Show (disabled) label if section has a toggle and is not enabled
         disabled_text = (
             settings.LABEL_DISABLED
@@ -171,7 +174,7 @@ class VariableDisplayManager:
         Returns:
             Tuple of (var_display, type, default_val, description, row_style)
         """
-        settings = self.parent.settings
+        settings = self.settings
 
         # Build row style
         row_style = (
@@ -210,9 +213,9 @@ class VariableDisplayManager:
         if not (template.variables and template.variables.has_sections()):
             return
 
-        settings = self.parent.settings
-        self.parent.text("")
-        self.parent.heading("Template Variables")
+        settings = self.settings
+        self.base.text("")
+        self.base.heading("Template Variables")
 
         variables_table = Table(
             show_header=True, header_style=settings.STYLE_TABLE_HEADER
@@ -267,4 +270,4 @@ class VariableDisplayManager:
                     var_display, var_type, default_val, description, style=row_style
                 )
 
-        self.parent._print_table(variables_table)
+        self.base._print_table(variables_table)

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

@@ -1,307 +0,0 @@
-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
-
-from .icon_manager import IconManager
-
-if TYPE_CHECKING:
-    from ..exceptions import TemplateRenderError
-    from . import DisplayManager
-
-logger = logging.getLogger(__name__)
-console_err = Console(stderr=True)  # Keep for error output
-
-
-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
-        """
-        # Errors and warnings always go to stderr, even in quiet mode
-        # Success and info respect quiet mode and go to stdout
-        use_stderr = level in ("error", "warning")
-        should_print = use_stderr or 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 in {"error", "warning"}
-                else f"{context}: {message}"
-            )
-        else:
-            text = (
-                f"{level.capitalize()}: {message}"
-                if level in {"error", "warning"}
-                else message
-            )
-
-        formatted_text = f"[{color}]{icon} {text}[/{color}]"
-        if use_stderr:
-            console_err.print(formatted_text)
-        else:
-            self.parent.text(formatted_text)
-
-        # 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
-        """
-        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
-        """
-        icon = IconManager.get_status_icon("skipped")
-        if reason:
-            self.parent.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
-        else:
-            self.parent.text(f"\n{icon} {message} (skipped)", style="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
-        """
-        icon = IconManager.get_status_icon("warning")
-        self.parent.text(f"\n{icon} {message}", style="yellow")
-
-        if details:
-            for detail in details:
-                self.parent.text(f"  {detail}", style="yellow")
-
-        return Confirm.ask("Continue?", default=default)
-
-    def _display_error_header(self, icon: str, context: str | None) -> None:
-        """Display error header with optional context."""
-        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()
-
-    def _display_error_location(self, error: TemplateRenderError) -> None:
-        """Display error file path and location."""
-        if not error.file_path:
-            return
-
-        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}")
-
-    def _display_code_context(self, error: TemplateRenderError) -> None:
-        """Display code context with syntax highlighting."""
-        if not error.context_lines:
-            return
-
-        console_err.print("[bold cyan]Code Context:[/bold cyan]")
-        context_text = "\n".join(error.context_lines)
-
-        # Determine lexer for syntax highlighting
-        lexer = self._get_lexer_for_file(error.file_path)
-
-        # Try to display with syntax highlighting, fallback to plain on error
-        try:
-            self._display_syntax_panel(context_text, lexer)
-        except Exception:
-            console_err.print(Panel(context_text, border_style="red", padding=(1, 2)))
-
-        console_err.print()
-
-    def _get_lexer_for_file(self, file_path: str | None) -> str | None:
-        """Determine lexer based on file extension."""
-        if not file_path:
-            return None
-
-        file_ext = Path(file_path).suffix
-        if file_ext == ".j2":
-            base_name = Path(file_path).stem
-            base_ext = Path(base_name).suffix
-            return "jinja2" if not base_ext else None
-        return None
-
-    def _display_syntax_panel(self, text: str, lexer: str | None) -> None:
-        """Display text in a panel with optional syntax highlighting."""
-        if lexer:
-            syntax = Syntax(text, lexer, line_numbers=False, theme="monokai")
-            console_err.print(Panel(syntax, border_style="red", padding=(1, 2)))
-        else:
-            console_err.print(Panel(text, border_style="red", padding=(1, 2)))
-
-    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)
-        """
-        # Display error header
-        icon = IconManager.get_status_icon("error")
-        self._display_error_header(icon, context)
-
-        # Display error location
-        self._display_error_location(error)
-
-        # Display error message
-        console_err.print(
-            f"[red]Message:[/red] {str(error.original_error) if error.original_error else str(error)}"
-        )
-        console_err.print()
-
-        # Display code context
-        self._display_code_context(error)
-
-        # 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()

+ 2 - 2
cli/core/input/prompt_manager.py

@@ -101,7 +101,7 @@ class PromptHandler:
             if not section.variables:
                 continue
 
-            self.display.display_section(section.title, section.description)
+            self.display.section(section.title, section.description)
             section_enabled = self._handle_section_toggle(section, collected)
 
             for var_name, variable in section.variables.items():
@@ -197,7 +197,7 @@ class PromptHandler:
 
     def _show_validation_error(self, message: str) -> None:
         """Display validation feedback consistently."""
-        self.display.display_validation_error(message)
+        self.display.error(message)
 
     def _prompt_string(
         self, prompt_text: str, default: Any = None, is_sensitive: bool = False

+ 171 - 113
cli/core/module/base_commands.py

@@ -6,16 +6,17 @@ import logging
 import os
 from pathlib import Path
 
-from rich.prompt import Confirm
+from jinja2 import Template as Jinja2Template
 from typer import Exit
 
 from ..config import ConfigManager
-from ..display import DisplayManager
+from ..display import DisplayManager, IconManager
 from ..exceptions import (
     TemplateRenderError,
     TemplateSyntaxError,
     TemplateValidationError,
 )
+from ..input import InputManager
 from ..template import Template
 from ..validators import get_validator_registry
 from .helpers import (
@@ -54,10 +55,32 @@ def list_templates(module_instance, raw: bool = False) -> list:
                 print(f"{template.id}\t{name}\t{tags}\t{version}\t{library}")
         else:
             # Output rich table format
-            module_instance.display.display_templates_table(
-                filtered_templates,
-                module_instance.name,
-                f"{module_instance.name.capitalize()} templates",
+            def format_template_row(template):
+                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"
+                library_name = template.metadata.library or ""
+                library_type = template.metadata.library_type or "git"
+                # Format library with icon and color
+                icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
+                color = "yellow" if library_type == "static" else "blue"
+                library_display = f"[{color}]{icon} {library_name}[/{color}]"
+                return (template.id, name, tags, version, schema, library_display)
+
+            module_instance.display.data_table(
+                columns=[
+                    {"name": "ID", "style": "bold", "no_wrap": True},
+                    {"name": "Name"},
+                    {"name": "Tags"},
+                    {"name": "Version", "no_wrap": True},
+                    {"name": "Schema", "no_wrap": True},
+                    {"name": "Library", "no_wrap": True},
+                ],
+                rows=filtered_templates,
+                title=f"{module_instance.name.capitalize()} templates",
+                row_formatter=format_template_row,
             )
     else:
         logger.info(f"No templates found for module '{module_instance.name}'")
@@ -80,16 +103,38 @@ def search_templates(module_instance, query: str) -> list:
         logger.info(
             f"Found {len(filtered_templates)} templates matching '{query}' for module '{module_instance.name}'"
         )
-        module_instance.display.display_templates_table(
-            filtered_templates,
-            module_instance.name,
-            f"{module_instance.name.capitalize()} templates matching '{query}'",
+        def format_template_row(template):
+            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"
+            library_name = template.metadata.library or ""
+            library_type = template.metadata.library_type or "git"
+            # Format library with icon and color
+            icon = IconManager.UI_LIBRARY_STATIC if library_type == "static" else IconManager.UI_LIBRARY_GIT
+            color = "yellow" if library_type == "static" else "blue"
+            library_display = f"[{color}]{icon} {library_name}[/{color}]"
+            return (template.id, name, tags, version, schema, library_display)
+
+        module_instance.display.data_table(
+            columns=[
+                {"name": "ID", "style": "bold", "no_wrap": True},
+                {"name": "Name"},
+                {"name": "Tags"},
+                {"name": "Version", "no_wrap": True},
+                {"name": "Schema", "no_wrap": True},
+                {"name": "Library", "no_wrap": True},
+            ],
+            rows=filtered_templates,
+            title=f"{module_instance.name.capitalize()} templates matching '{query}'",
+            row_formatter=format_template_row,
         )
     else:
         logger.info(
             f"No templates found matching '{query}' for module '{module_instance.name}'"
         )
-        module_instance.display.display_warning(
+        module_instance.display.warning(
             f"No templates found matching '{query}'",
             context=f"module '{module_instance.name}'",
         )
@@ -105,7 +150,7 @@ def show_template(
     template = module_instance._load_template_by_id(id)
 
     if not template:
-        module_instance.display.display_error(
+        module_instance.display.error(
             f"Template '{id}' not found", context=f"module '{module_instance.name}'"
         )
         return
@@ -125,7 +170,12 @@ def show_template(
         if reset_vars:
             logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
 
-    module_instance.display.display_template(template, id)
+    # Display template header
+    module_instance.display.templates.render_template_header(template, id)
+    # Display file tree
+    module_instance.display.templates.render_file_tree(template)
+    # Display variables table
+    module_instance.display.variables.render_variables_table(template)
 
 
 def check_output_directory(
@@ -149,16 +199,13 @@ def check_output_directory(
     # Warn if directory is not empty
     if dir_not_empty:
         if interactive:
-            details = []
+            display.warning(f"Directory '{output_dir}' is not empty.")
             if existing_files:
-                details.append(f"{len(existing_files)} file(s) will be overwritten.")
+                display.text(f"  {len(existing_files)} file(s) will be overwritten.")
 
-            if not display.display_warning_with_confirmation(
-                f"Directory '{output_dir}' is not empty.",
-                details if details else None,
-                default=False,
-            ):
-                display.display_info("Generation cancelled")
+            input_mgr = InputManager()
+            if not input_mgr.confirm("Continue?", default=False):
+                display.info("Generation cancelled")
                 return None
         else:
             # Non-interactive mode: show warning but continue
@@ -182,18 +229,17 @@ def get_generation_confirmation(
     if not interactive:
         return True
 
-    display.display_file_generation_confirmation(
+    # Use templates.render_file_generation_confirmation directly for now
+    display.templates.render_file_generation_confirmation(
         output_dir, rendered_files, existing_files if existing_files else None
     )
 
     # Final confirmation (only if we didn't already ask about overwriting)
-    if (
-        not dir_not_empty
-        and not dry_run
-        and not Confirm.ask("Generate these files?", default=True)
-    ):
-        display.display_info("Generation cancelled")
-        return False
+    if not dir_not_empty and not dry_run:
+        input_mgr = InputManager()
+        if not input_mgr.confirm("Generate these files?", default=True):
+            display.info("Generation cancelled")
+            return False
 
     return True
 
@@ -201,20 +247,20 @@ def get_generation_confirmation(
 def _check_directory_permissions(output_dir: Path, display: DisplayManager) -> None:
     """Check directory existence and write permissions."""
     if output_dir.exists():
-        display.display_success(f"Output directory exists: [cyan]{output_dir}[/cyan]")
+        display.success(f"Output directory exists: [cyan]{output_dir}[/cyan]")
         if os.access(output_dir, os.W_OK):
-            display.display_success("Write permission verified")
+            display.success("Write permission verified")
         else:
-            display.display_warning("Write permission may be denied")
+            display.warning("Write permission may be denied")
     else:
-        display.display_info(
+        display.info(
             f"  [dim]→[/dim] Would create output directory: [cyan]{output_dir}[/cyan]"
         )
         parent = output_dir.parent
         if parent.exists() and os.access(parent, os.W_OK):
-            display.display_success("Parent directory writable")
+            display.success("Parent directory writable")
         else:
-            display.display_warning("Parent directory may not be writable")
+            display.warning("Parent directory may not be writable")
 
 
 def _collect_subdirectories(rendered_files: dict[str, str]) -> set[Path]:
@@ -271,11 +317,9 @@ def execute_dry_run(
     display: DisplayManager,
 ) -> None:
     """Execute dry run mode with comprehensive simulation."""
-    display.display_info("")
-    display.display_info(
-        "[bold cyan]Dry Run Mode - Simulating File Generation[/bold cyan]"
-    )
-    display.display_info("")
+    display.info("")
+    display.info("[bold cyan]Dry Run Mode - Simulating File Generation[/bold cyan]")
+    display.info("")
 
     # Simulate directory creation
     display.heading("Directory Operations")
@@ -284,48 +328,53 @@ def execute_dry_run(
     # Collect and display subdirectories
     subdirs = _collect_subdirectories(rendered_files)
     if subdirs:
-        display.display_info(
-            f"  [dim]→[/dim] Would create {len(subdirs)} subdirectory(ies)"
-        )
+        display.info(f"  [dim]→[/dim] Would create {len(subdirs)} subdirectory(ies)")
         for subdir in sorted(subdirs):
-            display.display_info(f"    [dim]📁[/dim] {subdir}/")
+            display.info(f"    [dim]📁[/dim] {subdir}/")
 
-    display.display_info("")
+    display.info("")
 
     # Display file operations in a table
     display.heading("File Operations")
     file_operations, total_size, new_files, overwrite_files = _analyze_file_operations(
         output_dir, rendered_files
     )
-    display.display_file_operation_table(file_operations)
-    display.display_info("")
+    # Use data_table for file operations
+    display.data_table(
+        columns=[
+            {"name": "File", "no_wrap": False},
+            {"name": "Size", "justify": "right", "style": "dim"},
+            {"name": "Status", "style": "yellow"},
+        ],
+        rows=file_operations,
+        row_formatter=lambda row: (str(row[0]), display.format_file_size(row[1]), row[2]),
+    )
+    display.info("")
 
     # Summary statistics
     size_str = _format_size(total_size)
-    summary_items = {
-        "Total files:": str(len(rendered_files)),
-        "New files:": str(new_files),
-        "Files to overwrite:": str(overwrite_files),
-        "Total size:": size_str,
-    }
-    display.display_summary_table("Summary", summary_items)
-    display.display_info("")
+    summary_rows = [
+        ("Total files:", str(len(rendered_files))),
+        ("New files:", str(new_files)),
+        ("Files to overwrite:", str(overwrite_files)),
+        ("Total size:", size_str),
+    ]
+    display.table(headers=None, rows=summary_rows, title="Summary", show_header=False, borderless=True)
+    display.info("")
 
     # Show file contents if requested
     if show_files:
-        display.display_info("[bold cyan]Generated File Contents:[/bold cyan]")
-        display.display_info("")
+        display.info("[bold cyan]Generated File Contents:[/bold cyan]")
+        display.info("")
         for file_path, content in sorted(rendered_files.items()):
-            display.display_info(f"[cyan]File:[/cyan] {file_path}")
-            display.display_info(f"{'─' * 80}")
-            display.display_info(content)
-            display.display_info("")  # Add blank line after content
-        display.display_info("")
-
-    display.display_success("Dry run complete - no files were written")
-    display.display_info(
-        f"[dim]Files would have been generated in '{output_dir}'[/dim]"
-    )
+            display.info(f"[cyan]File:[/cyan] {file_path}")
+            display.info(f"{'─' * 80}")
+            display.info(content)
+            display.info("")  # Add blank line after content
+        display.info("")
+
+    display.success("Dry run complete - no files were written")
+    display.info(f"[dim]Files would have been generated in '{output_dir}'[/dim]")
     logger.info(
         f"Dry run completed for template '{id}' - {len(rendered_files)} files, {total_size} bytes"
     )
@@ -346,10 +395,10 @@ def write_generated_files(
         with open(full_path, "w", encoding="utf-8") as f:
             f.write(content)
         if not quiet:
-            display.display_success(f"Generated file: {file_path}")
+            display.success(f"Generated file: {file_path}")
 
     if not quiet:
-        display.display_success(f"Template generated successfully in '{output_dir}'")
+        display.success(f"Template generated successfully in '{output_dir}'")
     logger.info(f"Template written to directory: {output_dir}")
 
 
@@ -389,7 +438,7 @@ def _render_template(template, id: str, display: DisplayManager, interactive: bo
     )
 
     if not rendered_files:
-        display.display_error(
+        display.error(
             "Template rendering returned no files",
             context="template generation",
         )
@@ -433,8 +482,13 @@ def generate_template(
     template = _prepare_template(module_instance, id, var_file, var, display)
 
     if not quiet:
-        module_instance.display.display_template(template, id)
-        module_instance.display.display_info("")
+        # Display template header
+        module_instance.display.templates.render_template_header(template, id)
+        # Display file tree
+        module_instance.display.templates.render_file_tree(template)
+        # Display variables table
+        module_instance.display.variables.render_variables_table(template)
+        module_instance.display.info("")
 
     try:
         rendered_files, variable_values = _render_template(
@@ -471,13 +525,21 @@ def generate_template(
 
         # Display next steps (not in quiet mode)
         if template.metadata.next_steps and not quiet:
-            display.display_next_steps(template.metadata.next_steps, variable_values)
+            display.heading("Next Steps")
+            try:
+                next_steps_template = Jinja2Template(template.metadata.next_steps)
+                rendered_next_steps = next_steps_template.render(variable_values)
+                display.text(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
+                display.text(template.metadata.next_steps)
 
     except TemplateRenderError as e:
-        display.display_template_render_error(e, context=f"template '{id}'")
+        display.error(str(e), context=f"template '{id}'")
         raise Exit(code=1) from None
     except Exception as e:
-        display.display_error(str(e), context=f"generating template '{id}'")
+        display.error(str(e), context=f"generating template '{id}'")
         raise Exit(code=1) from None
 
 
@@ -505,19 +567,19 @@ def _load_template_for_validation(module_instance, template_id: str, path: str |
     if path:
         template_path = Path(path).resolve()
         if not template_path.exists():
-            module_instance.display.display_error(f"Path does not exist: {path}")
+            module_instance.display.error(f"Path does not exist: {path}")
             raise Exit(code=1) from None
         if not template_path.is_dir():
-            module_instance.display.display_error(f"Path is not a directory: {path}")
+            module_instance.display.error(f"Path is not a directory: {path}")
             raise Exit(code=1) from None
 
-        module_instance.display.display_info(
+        module_instance.display.info(
             f"[bold]Validating template from path:[/bold] [cyan]{template_path}[/cyan]"
         )
         try:
             return Template(template_path, library_name="local")
         except Exception as e:
-            module_instance.display.display_error(
+            module_instance.display.error(
                 f"Failed to load template from path '{path}': {e}"
             )
             raise Exit(code=1) from None
@@ -525,12 +587,12 @@ def _load_template_for_validation(module_instance, template_id: str, path: str |
     if template_id:
         try:
             template = module_instance._load_template_by_id(template_id)
-            module_instance.display.display_info(
+            module_instance.display.info(
                 f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]"
             )
             return template
         except Exception as e:
-            module_instance.display.display_error(
+            module_instance.display.error(
                 f"Failed to load template '{template_id}': {e}"
             )
             raise Exit(code=1) from None
@@ -546,7 +608,7 @@ def _validate_single_template(
         # Jinja2 validation
         _ = template.used_variables
         _ = template.variables
-        module_instance.display.display_success("Jinja2 validation passed")
+        module_instance.display.success("Jinja2 validation passed")
 
         # Semantic validation
         if semantic:
@@ -557,16 +619,14 @@ def _validate_single_template(
             _display_validation_details(module_instance, template, semantic)
 
     except TemplateRenderError as e:
-        module_instance.display.display_template_render_error(
-            e, context=f"template '{template_id}'"
-        )
+        module_instance.display.error(str(e), context=f"template '{template_id}'")
         raise Exit(code=1) from None
     except (TemplateSyntaxError, TemplateValidationError, ValueError) as e:
-        module_instance.display.display_error(f"Validation failed for '{template_id}':")
-        module_instance.display.display_info(f"\n{e}")
+        module_instance.display.error(f"Validation failed for '{template_id}':")
+        module_instance.display.info(f"\n{e}")
         raise Exit(code=1) from None
     except Exception as e:
-        module_instance.display.display_error(
+        module_instance.display.error(
             f"Unexpected error validating '{template_id}': {e}"
         )
         raise Exit(code=1) from None
@@ -574,8 +634,8 @@ def _validate_single_template(
 
 def _run_semantic_validation(module_instance, template, verbose: bool) -> None:
     """Run semantic validation on rendered template files."""
-    module_instance.display.display_info("")
-    module_instance.display.display_info(
+    module_instance.display.info("")
+    module_instance.display.info(
         "[bold cyan]Running semantic validation...[/bold cyan]"
     )
 
@@ -588,38 +648,36 @@ def _run_semantic_validation(module_instance, template, verbose: bool) -> None:
         result = registry.validate_file(content, file_path)
 
         if result.errors or result.warnings or (verbose and result.info):
-            module_instance.display.display_info(f"\n[cyan]File:[/cyan] {file_path}")
+            module_instance.display.info(f"\n[cyan]File:[/cyan] {file_path}")
             result.display(f"{file_path}")
 
             if result.errors:
                 has_semantic_errors = True
 
     if has_semantic_errors:
-        module_instance.display.display_error("Semantic validation found errors")
+        module_instance.display.error("Semantic validation found errors")
         raise Exit(code=1) from None
 
-    module_instance.display.display_success("Semantic validation passed")
+    module_instance.display.success("Semantic validation passed")
 
 
 def _display_validation_details(module_instance, template, semantic: bool) -> None:
     """Display verbose validation details."""
-    module_instance.display.display_info(
-        f"\n[dim]Template path: {template.template_dir}[/dim]"
-    )
-    module_instance.display.display_info(
+    module_instance.display.info(f"\n[dim]Template path: {template.template_dir}[/dim]")
+    module_instance.display.info(
         f"[dim]Found {len(template.used_variables)} variables[/dim]"
     )
     if semantic:
         debug_mode = logger.isEnabledFor(logging.DEBUG)
         rendered_files, _ = template.render(template.variables, debug=debug_mode)
-        module_instance.display.display_info(
+        module_instance.display.info(
             f"[dim]Generated {len(rendered_files)} files[/dim]"
         )
 
 
 def _validate_all_templates(module_instance, verbose: bool) -> None:
     """Validate all templates in the module."""
-    module_instance.display.display_info(
+    module_instance.display.info(
         f"[bold]Validating all {module_instance.name} templates...[/bold]"
     )
 
@@ -636,34 +694,34 @@ def _validate_all_templates(module_instance, verbose: bool) -> None:
             _ = template.variables
             valid_count += 1
             if verbose:
-                module_instance.display.display_success(template.id)
+                module_instance.display.success(template.id)
         except ValueError as e:
             invalid_count += 1
             errors.append((template.id, str(e)))
             if verbose:
-                module_instance.display.display_error(template.id)
+                module_instance.display.error(template.id)
         except Exception as e:
             invalid_count += 1
             errors.append((template.id, f"Load error: {e}"))
             if verbose:
-                module_instance.display.display_warning(template.id)
+                module_instance.display.warning(template.id)
 
     # Display summary
-    summary_items = {
-        "Total templates:": str(total),
-        "[green]Valid:[/green]": str(valid_count),
-        "[red]Invalid:[/red]": str(invalid_count),
-    }
-    module_instance.display.display_summary_table("Validation Summary", summary_items)
+    summary_rows = [
+        ("Total templates:", str(total)),
+        ("[green]Valid:[/green]", str(valid_count)),
+        ("[red]Invalid:[/red]", str(invalid_count)),
+    ]
+    module_instance.display.table(headers=None, rows=summary_rows, title="Validation Summary", show_header=False, borderless=True)
 
     if errors:
-        module_instance.display.display_info("")
-        module_instance.display.display_error("Validation Errors:")
+        module_instance.display.info("")
+        module_instance.display.error("Validation Errors:")
         for template_id, error_msg in errors:
-            module_instance.display.display_info(
+            module_instance.display.info(
                 f"\n[yellow]Template:[/yellow] [cyan]{template_id}[/cyan]"
             )
-            module_instance.display.display_info(f"[dim]{error_msg}[/dim]")
+            module_instance.display.info(f"[dim]{error_msg}[/dim]")
         raise Exit(code=1)
 
-    module_instance.display.display_success("All templates are valid!")
+    module_instance.display.success("All templates are valid!")

+ 6 - 1
cli/core/module/base_module.py

@@ -337,5 +337,10 @@ class Module(ABC):
         )(module_instance.config_list)
         module_app.add_typer(defaults_app, name="defaults")
 
-        app.add_typer(module_app, name=cls.name, help=cls.description)
+        app.add_typer(
+            module_app,
+            name=cls.name,
+            help=cls.description,
+            rich_help_panel="Template Commands",
+        )
         logger.info(f"Module '{cls.name}' CLI commands registered")

+ 34 - 37
cli/core/module/config_commands.py

@@ -4,10 +4,10 @@ from __future__ import annotations
 
 import logging
 
-from rich.prompt import Confirm
 from typer import Exit
 
-from ..config import ConfigManager
+from cli.core.config import ConfigManager
+from cli.core.input import InputManager
 
 logger = logging.getLogger(__name__)
 
@@ -20,11 +20,11 @@ def config_get(module_instance, var_name: str | None = None) -> None:
         # Get specific variable default
         value = config.get_default_value(module_instance.name, var_name)
         if value is not None:
-            module_instance.display.display_info(
+            module_instance.display.info(
                 f"[green]{var_name}[/green] = [yellow]{value}[/yellow]"
             )
         else:
-            module_instance.display.display_warning(
+            module_instance.display.warning(
                 f"No default set for variable '{var_name}'",
                 context=f"module '{module_instance.name}'",
             )
@@ -32,15 +32,15 @@ def config_get(module_instance, var_name: str | None = None) -> None:
         # Show all defaults (flat list)
         defaults = config.get_defaults(module_instance.name)
         if defaults:
-            module_instance.display.display_info(
+            module_instance.display.info(
                 f"[bold]Config defaults for module '{module_instance.name}':[/bold]"
             )
             for config_var_name, var_value in defaults.items():
-                module_instance.display.display_info(
+                module_instance.display.info(
                     f"  [green]{config_var_name}[/green] = [yellow]{var_value}[/yellow]"
                 )
         else:
-            module_instance.display.display_warning(
+            module_instance.display.warning(
                 f"No defaults configured for module '{module_instance.name}'"
             )
 
@@ -60,20 +60,20 @@ def config_set(module_instance, var_name: str, value: str | None = None) -> None
         actual_var_name = var_name
         actual_value = value
     else:
-        module_instance.display.display_error(
+        module_instance.display.error(
             f"Missing value for variable '{var_name}'", context="config set"
         )
-        module_instance.display.display_info(
+        module_instance.display.info(
             "[dim]Usage: defaults set VAR_NAME VALUE or defaults set VAR_NAME=VALUE[/dim]"
         )
         raise Exit(code=1)
 
     # Set the default value
     config.set_default_value(module_instance.name, actual_var_name, actual_value)
-    module_instance.display.display_success(
+    module_instance.display.success(
         f"Set default: [cyan]{actual_var_name}[/cyan] = [yellow]{actual_value}[/yellow]"
     )
-    module_instance.display.display_info(
+    module_instance.display.info(
         "[dim]This will be used as the default value when generating templates with this module.[/dim]"
     )
 
@@ -84,7 +84,7 @@ def config_remove(module_instance, var_name: str) -> None:
     defaults = config.get_defaults(module_instance.name)
 
     if not defaults:
-        module_instance.display.display_warning(
+        module_instance.display.warning(
             f"No defaults configured for module '{module_instance.name}'"
         )
         return
@@ -92,11 +92,9 @@ def config_remove(module_instance, var_name: str) -> None:
     if var_name in defaults:
         del defaults[var_name]
         config.set_defaults(module_instance.name, defaults)
-        module_instance.display.display_success(f"Removed default for '{var_name}'")
+        module_instance.display.success(f"Removed default for '{var_name}'")
     else:
-        module_instance.display.display_error(
-            f"No default found for variable '{var_name}'"
-        )
+        module_instance.display.error(f"No default found for variable '{var_name}'")
 
 
 def config_clear(
@@ -107,7 +105,7 @@ def config_clear(
     defaults = config.get_defaults(module_instance.name)
 
     if not defaults:
-        module_instance.display.display_warning(
+        module_instance.display.warning(
             f"No defaults configured for module '{module_instance.name}'"
         )
         return
@@ -117,11 +115,9 @@ def config_clear(
         if var_name in defaults:
             del defaults[var_name]
             config.set_defaults(module_instance.name, defaults)
-            module_instance.display.display_success(f"Cleared default for '{var_name}'")
+            module_instance.display.success(f"Cleared default for '{var_name}'")
         else:
-            module_instance.display.display_error(
-                f"No default found for variable '{var_name}'"
-            )
+            module_instance.display.error(f"No default found for variable '{var_name}'")
     else:
         # Clear all defaults
         if not force:
@@ -134,21 +130,18 @@ def config_clear(
                     f"  [green]{clear_var_name}[/green] = [yellow]{var_value}[/yellow]"
                 )
 
-            module_instance.display.display_warning(
-                "Warning: This will clear ALL defaults"
-            )
-            module_instance.display.display_info("")
+            module_instance.display.warning("Warning: This will clear ALL defaults")
+            module_instance.display.info("")
             for line in detail_lines:
-                module_instance.display.display_info(line)
-            module_instance.display.display_info("")
-            if not Confirm.ask("[bold red]Are you sure?[/bold red]", default=False):
-                module_instance.display.display_info(
-                    "[green]Operation cancelled.[/green]"
-                )
+                module_instance.display.info(line)
+            module_instance.display.info("")
+            input_mgr = InputManager()
+            if not input_mgr.confirm("Are you sure?", default=False):
+                module_instance.display.info("[green]Operation cancelled.[/green]")
                 return
 
         config.clear_defaults(module_instance.name)
-        module_instance.display.display_success(
+        module_instance.display.success(
             f"Cleared all defaults for module '{module_instance.name}'"
         )
 
@@ -161,16 +154,20 @@ def config_list(module_instance) -> None:
     defaults = config.get_defaults(module_instance.name)
 
     if not defaults:
-        module_instance.display.display_warning(
+        module_instance.display.warning(
             f"No defaults configured for module '{module_instance.name}'"
         )
         return
 
     # Display defaults using DisplayManager
-    module_instance.display.display_info(
+    module_instance.display.info(
         f"[bold]Defaults for module '{module_instance.name}':[/bold]\n"
     )
 
-    # Convert defaults to display format (key: value pairs)
-    items = {f"{var_name}:": str(var_value) for var_name, var_value in defaults.items()}
-    module_instance.display.display_summary_table("", items)
+    # Convert defaults to display format (rows for table)
+    rows = [
+        (f"{var_name}:", str(var_value)) for var_name, var_value in defaults.items()
+    ]
+    module_instance.display.table(
+        headers=None, rows=rows, title="", show_header=False, borderless=True
+    )

+ 1 - 1
cli/core/module/helpers.py

@@ -142,7 +142,7 @@ def apply_var_file(
             if successful:
                 logger.debug(f"Applied var-file overrides for: {', '.join(successful)}")
     except (FileNotFoundError, ValueError) as e:
-        display.display_error(
+        display.error(
             f"Failed to load variable file: {e}",
             context="variable file loading",
         )

+ 2 - 2
cli/core/prompt.py

@@ -107,7 +107,7 @@ class PromptHandler:
                 continue
 
             # Display section header
-            self.display.display_section(section.title, section.description)
+            self.display.section(section.title, section.description)
 
             # Handle toggle and determine if section is enabled
             section_enabled = self._handle_section_toggle(section, collected)
@@ -206,7 +206,7 @@ class PromptHandler:
 
     def _show_validation_error(self, message: str) -> None:
         """Display validation feedback consistently."""
-        self.display.display_validation_error(message)
+        self.display.error(message)
 
     def _prompt_string(
         self, prompt_text: str, default: Any = None, is_sensitive: bool = False

+ 14 - 14
cli/core/repo.py

@@ -207,9 +207,9 @@ def _process_library_update(
 
     if verbose:
         if success:
-            display.display_success(f"{name}: {message}")
+            display.success(f"{name}: {message}")
         else:
-            display.display_error(f"{name}: {message}")
+            display.error(f"{name}: {message}")
 
     return (name, message, success)
 
@@ -249,7 +249,7 @@ def update(
     libraries = config.get_libraries()
 
     if not libraries:
-        display.display_warning("No libraries configured")
+        display.warning("No libraries configured")
         display.text(
             "Libraries are auto-configured on first run with a default library."
         )
@@ -405,7 +405,7 @@ def add(
     try:
         if library_type == "git":
             if not url:
-                display.display_error("--url is required for git libraries")
+                display.error("--url is required for git libraries")
                 return
             config.add_library(
                 name,
@@ -417,24 +417,24 @@ def add(
             )
         elif library_type == "static":
             if not path:
-                display.display_error("--path is required for static libraries")
+                display.error("--path is required for static libraries")
                 return
             config.add_library(name, library_type="static", path=path, enabled=enabled)
         else:
-            display.display_error(
+            display.error(
                 f"Invalid library type: {library_type}. Must be 'git' or 'static'."
             )
             return
 
-        display.display_success(f"Added {library_type} library '{name}'")
+        display.success(f"Added {library_type} library '{name}'")
 
         if library_type == "git" and sync and enabled:
             display.text(f"\nSyncing library '{name}'...")
             update(library_name=name, verbose=True)
         elif library_type == "static":
-            display.display_info(f"Static library points to: {path}")
+            display.info(f"Static library points to: {path}")
     except ConfigError as e:
-        display.display_error(str(e))
+        display.error(str(e))
 
 
 @app.command()
@@ -450,7 +450,7 @@ def remove(
     try:
         # Remove from config
         config.remove_library(name)
-        display.display_success(f"Removed library '{name}' from configuration")
+        display.success(f"Removed library '{name}' from configuration")
 
         # Delete local files unless --keep-files is specified
         if not keep_files:
@@ -459,14 +459,14 @@ def remove(
 
             if library_path.exists():
                 shutil.rmtree(library_path)
-                display.display_success(f"Deleted local files at {library_path}")
+                display.success(f"Deleted local files at {library_path}")
             else:
-                display.display_info(f"No local files found at {library_path}")
+                display.info(f"No local files found at {library_path}")
     except ConfigError as e:
-        display.display_error(str(e))
+        display.error(str(e))
 
 
 # Register the repo command with the CLI
 def register_cli(parent_app: Typer) -> None:
     """Register the repo command with the parent Typer app."""
-    parent_app.add_typer(app, name="repo")
+    parent_app.add_typer(app, name="repo", rich_help_panel="Configuration Commands")

+ 1 - 0
cli/modules/__init__.py

@@ -0,0 +1 @@
+"""Modules package."""

+ 27 - 0
cli/modules/ansible/__init__.py

@@ -0,0 +1,27 @@
+"""Ansible module with multi-schema support."""
+
+from ...core.module import Module
+from ...core.registry import registry
+
+# Import schema specifications
+from .spec_v1_0 import spec as spec_1_0
+
+# Schema version mapping
+SCHEMAS = {
+    "1.0": spec_1_0,
+}
+
+# Default spec points to latest version
+spec = spec_1_0
+
+
+class AnsibleModule(Module):
+    """Ansible module."""
+
+    name = "ansible"
+    description = "Manage Ansible playbooks"
+    schema_version = "1.0"  # Current schema version supported by this module
+    schemas = SCHEMAS  # Available schema versions
+
+
+registry.register(AnsibleModule)

+ 17 - 0
cli/modules/ansible/spec_v1_0.py

@@ -0,0 +1,17 @@
+"""Ansible module schema version 1.0 - Original specification."""
+
+from collections import OrderedDict
+
+spec = OrderedDict(
+    {
+        "general": {
+            "title": "General",
+            "vars": {
+                "playbook_name": {
+                    "description": "Ansible playbook name",
+                    "type": "str",
+                },
+            },
+        },
+    }
+)

+ 27 - 0
cli/modules/helm/__init__.py

@@ -0,0 +1,27 @@
+"""Helm module with multi-schema support."""
+
+from ...core.module import Module
+from ...core.registry import registry
+
+# Import schema specifications
+from .spec_v1_0 import spec as spec_1_0
+
+# Schema version mapping
+SCHEMAS = {
+    "1.0": spec_1_0,
+}
+
+# Default spec points to latest version
+spec = spec_1_0
+
+
+class HelmModule(Module):
+    """Helm module."""
+
+    name = "helm"
+    description = "Manage Helm charts"
+    schema_version = "1.0"  # Current schema version supported by this module
+    schemas = SCHEMAS  # Available schema versions
+
+
+registry.register(HelmModule)

+ 17 - 0
cli/modules/helm/spec_v1_0.py

@@ -0,0 +1,17 @@
+"""Helm module schema version 1.0 - Original specification."""
+
+from collections import OrderedDict
+
+spec = OrderedDict(
+    {
+        "general": {
+            "title": "General",
+            "vars": {
+                "release_name": {
+                    "description": "Helm release name",
+                    "type": "str",
+                },
+            },
+        },
+    }
+)

+ 27 - 0
cli/modules/kubernetes/__init__.py

@@ -0,0 +1,27 @@
+"""Kubernetes module with multi-schema support."""
+
+from ...core.module import Module
+from ...core.registry import registry
+
+# Import schema specifications
+from .spec_v1_0 import spec as spec_1_0
+
+# Schema version mapping
+SCHEMAS = {
+    "1.0": spec_1_0,
+}
+
+# Default spec points to latest version
+spec = spec_1_0
+
+
+class KubernetesModule(Module):
+    """Kubernetes module."""
+
+    name = "kubernetes"
+    description = "Manage Kubernetes configurations"
+    schema_version = "1.0"  # Current schema version supported by this module
+    schemas = SCHEMAS  # Available schema versions
+
+
+registry.register(KubernetesModule)

+ 18 - 0
cli/modules/kubernetes/spec_v1_0.py

@@ -0,0 +1,18 @@
+"""Kubernetes module schema version 1.0 - Original specification."""
+
+from collections import OrderedDict
+
+spec = OrderedDict(
+    {
+        "general": {
+            "title": "General",
+            "vars": {
+                "namespace": {
+                    "description": "Kubernetes namespace",
+                    "type": "str",
+                    "default": "default",
+                },
+            },
+        },
+    }
+)

+ 27 - 0
cli/modules/packer/__init__.py

@@ -0,0 +1,27 @@
+"""Packer module with multi-schema support."""
+
+from ...core.module import Module
+from ...core.registry import registry
+
+# Import schema specifications
+from .spec_v1_0 import spec as spec_1_0
+
+# Schema version mapping
+SCHEMAS = {
+    "1.0": spec_1_0,
+}
+
+# Default spec points to latest version
+spec = spec_1_0
+
+
+class PackerModule(Module):
+    """Packer module."""
+
+    name = "packer"
+    description = "Manage Packer templates"
+    schema_version = "1.0"  # Current schema version supported by this module
+    schemas = SCHEMAS  # Available schema versions
+
+
+registry.register(PackerModule)

+ 17 - 0
cli/modules/packer/spec_v1_0.py

@@ -0,0 +1,17 @@
+"""Packer module schema version 1.0 - Original specification."""
+
+from collections import OrderedDict
+
+spec = OrderedDict(
+    {
+        "general": {
+            "title": "General",
+            "vars": {
+                "image_name": {
+                    "description": "Image name",
+                    "type": "str",
+                },
+            },
+        },
+    }
+)

+ 27 - 0
cli/modules/terraform/__init__.py

@@ -0,0 +1,27 @@
+"""Terraform module with multi-schema support."""
+
+from ...core.module import Module
+from ...core.registry import registry
+
+# Import schema specifications
+from .spec_v1_0 import spec as spec_1_0
+
+# Schema version mapping
+SCHEMAS = {
+    "1.0": spec_1_0,
+}
+
+# Default spec points to latest version
+spec = spec_1_0
+
+
+class TerraformModule(Module):
+    """Terraform module."""
+
+    name = "terraform"
+    description = "Manage Terraform configurations"
+    schema_version = "1.0"  # Current schema version supported by this module
+    schemas = SCHEMAS  # Available schema versions
+
+
+registry.register(TerraformModule)

+ 23 - 0
cli/modules/terraform/spec_v1_0.py

@@ -0,0 +1,23 @@
+"""Terraform module schema version 1.0 - Original specification."""
+
+from collections import OrderedDict
+
+spec = OrderedDict(
+    {
+        "general": {
+            "title": "General",
+            "vars": {
+                "resource_name": {
+                    "description": "Resource name prefix",
+                    "type": "str",
+                },
+                "backend_mode": {
+                    "description": "Terraform backend mode",
+                    "type": "enum",
+                    "options": ["local", "http"],
+                    "default": "local",
+                },
+            },
+        },
+    }
+)

+ 25 - 0
library/terraform/twingate-connector/main.tf.j2

@@ -0,0 +1,25 @@
+terraform {
+  required_providers {
+    twingate = {
+      source  = "Twingate/twingate"
+      version = "~> 3.0"
+    }
+  }
+{% if backend_mode == "http" %}
+  backend "http" {
+  }
+{% endif %}
+}
+
+provider "twingate" {
+  network = "{{ twingate_network }}"
+}
+
+resource "twingate_connector" "{{ resource_name }}_connector" {
+  remote_network_id = var.remote_network_id
+  name              = "{{ twingate_connector_name }}"
+}
+
+resource "twingate_connector_tokens" "{{ resource_name }}_tokens" {
+  connector_id = twingate_connector.{{ resource_name }}_connector.id
+}

+ 16 - 0
library/terraform/twingate-connector/outputs.tf.j2

@@ -0,0 +1,16 @@
+output "connector_id" {
+  description = "Twingate Connector ID"
+  value       = twingate_connector.{{ resource_name }}_connector.id
+}
+
+output "access_token" {
+  description = "Twingate Connector Access Token"
+  value       = twingate_connector_tokens.{{ resource_name }}_tokens.access_token
+  sensitive   = true
+}
+
+output "refresh_token" {
+  description = "Twingate Connector Refresh Token"
+  value       = twingate_connector_tokens.{{ resource_name }}_tokens.refresh_token
+  sensitive   = true
+}

+ 31 - 0
library/terraform/twingate-connector/template.yaml

@@ -0,0 +1,31 @@
+---
+kind: terraform
+schema: "1.0"
+metadata:
+  name: Twingate Connector
+  description: >
+    Twingate Connector deployment using Terraform. The Connector establishes secure connections between your 
+    private network and Twingate service, enabling zero-trust network access.
+
+
+    Project: https://www.twingate.com/
+
+    Documentation: https://docs.twingate.com/docs/architecture/connectors
+
+    GitHub: https://github.com/twingate/twingate-connector
+  version: 1.0.0
+  author: Christian Lempa
+  date: '2025-10-30'
+  tags:
+    - zero-trust
+    - remote-access
+spec:
+  general:
+    vars:
+      twingate_network:
+        type: str
+        description: Twingate network name
+      twingate_connector_name:
+        type: str
+        description: Connector name
+        default: connector-01

+ 4 - 0
library/terraform/twingate-connector/variables.tf.j2

@@ -0,0 +1,4 @@
+variable "remote_network_id" {
+  description = "Twingate Remote Network ID"
+  type        = string
+}