Przeglądaj źródła

Merge pull request #1305 from ChristianLempa/problem/1297

Standardize DisplayManager and IconManager responsibilities
Christian Lempa 9 miesięcy temu
rodzic
commit
6e19099c2c
5 zmienionych plików z 305 dodań i 336 usunięć
  1. 83 250
      AGENTS.md
  2. 150 1
      cli/core/display.py
  3. 62 68
      cli/core/module.py
  4. 3 5
      cli/core/prompt.py
  5. 7 12
      cli/core/repo.py

+ 83 - 250
AGENTS.md

@@ -6,233 +6,96 @@ Guidance for AI Agents working with this repository.
 
 A sophisticated collection of infrastructure templates (boilerplates) with a Python CLI for management. Supports Terraform, Docker, Ansible, Kubernetes, etc. Built with Typer (CLI) and Jinja2 (templating).
 
-## Repository Structure
-
-- `cli/` - Python CLI application source code
-  - `cli/core/` - Core functionality (app, config, commands, logging)
-  - `cli/modules/` - Technology-specific modules (terraform, docker, compose, config, etc.)
-- `library/` - Template collections organized by module
-  - `library/ansible/` - Ansible playbooks and configurations
-  - `library/compose/` - Docker Compose configurations
-  - `library/docker/` - Docker templates
-  - `library/kubernetes/` - Kubernetes deployments
-  - `library/packer/` - Packer templates
-  - `library/terraform/` - OpenTofu/Terraform templates and examples
-
 ## Development Setup
 
-### Running the CLI
+### Running and Testing
 
 ```bash
-# List available commands
+# Run the CLI application
 python3 -m cli
-
-# List templates for a module
-python3 -m cli compose list
-
-# Debugging commands
+# Debugging and Testing commands
 python3 -m cli --log-level DEBUG compose list
-
-# Generate template to directory named after template (default)
-python3 -m cli compose generate nginx
-
-# Generate template to custom directory
-python3 -m cli compose generate nginx my-nginx-server
-
-# Generate template interactively (default - prompts for variables)
-python3 -m cli compose generate authentik
-
-# Generate template non-interactively (skips prompts, uses defaults and CLI variables)
-python3 -m cli compose generate authentik my-auth --no-interactive
-
-# Generate with variable overrides (non-interactive)
-python3 -m cli compose generate authentik my-auth \
-  --var service_name=auth \
-  --var ports_enabled=false \
-  --var database_type=postgres \
-  --no-interactive
-
-# Show template details
-python3 -m cli compose show authentik
-
-# Managing default values
-python3 -m cli compose defaults set service_name my-app
-python3 -m cli compose defaults get
-python3 -m cli compose defaults list
-
-# Managing library repositories
-python3 -m cli repo list
-python3 -m cli repo update
-python3 -m cli repo add my-lib https://github.com/user/templates --directory library --branch main
-python3 -m cli repo remove my-lib
 ```
 
-## Common Development Tasks
-
-## Release Management
-
-**Process:** Tag-based workflow via `.github/workflows/release.yaml`. Push a semver tag (e.g., `v1.2.3`) to trigger.
+### Linting and Formatting
 
-**Workflow Steps:**
-1. Extracts version from tag
-2. Auto-updates `pyproject.toml` and `cli/__main__.py` with version
-3. Recreates tag pointing to version bump commit
-4. Builds wheel/tarball
-5. Creates GitHub release (marks alpha/beta/rc as pre-release)
+Should **always** happen before pushing anything to the repository.
 
-**Important:** Never manually edit version numbers - they're placeholders (`0.0.0`) that get auto-updated.
-
-**User Installation:**
-```bash
-# Latest
-curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash
-
-# Specific version
-curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --version v1.2.3
-```
+- Use `yamllint` for YAML files and `pylint` for Python code.
+- Use `2` spaces for YAML and Python indentation.
 
-The `install.sh` script downloads the release tarball and installs via pipx. PyPI publishing is currently disabled.
+### Project Management and Git
 
-## Library System
-
-### Git-Based Libraries
-
-Templates are stored in git repositories and synced locally:
-
-- **Location**: `~/.config/boilerplates/libraries/{name}/`
-- **Config**: Stored in `~/.config/boilerplates/config.yaml`
-- **Sync**: Uses sparse-checkout to clone only template directories
-
-### Library Configuration
-
-Libraries are defined in the config file:
-
-```yaml
-libraries:
-  - name: default
-    url: https://github.com/christianlempa/boilerplates.git
-    branch: refactor/boilerplates-v2
-    directory: library  # Directory within repo containing templates
-    enabled: true
-```
+The project is stored in a public GitHub Repository, use issues, and branches for features/bugfixes and open PRs for merging.
 
-**Properties:**
-- `name`: Unique identifier for the library
-- `url`: Git repository URL
-- `branch`: Git branch to use (default: `main`)
-- `directory`: Path within repo where templates are located (use `.` for root)
-- `enabled`: Whether library is active
+**Naming Conventions and Best-Practices:**
+- Branches, PRs: `feature/2314-add-feature`, `problem/1249-fix-bug`
+- Issues should have clear titles and descriptions, link related issues/PRs, and have appropriate labels like (problem, feature, discussion, question).
+- Commit messages should be clear and concise, following the format: `type(scope): subject` (e.g., `fix(compose): correct variable parsing`).
 
-### Sparse-Checkout
+## Architecture
 
-The system uses git sparse-checkout (non-cone mode) to clone only the specified `directory`, avoiding unnecessary files:
+### File Structure
 
-```bash
-# Only clones library/ directory, not root files
-git sparse-checkout init --no-cone
-git sparse-checkout set library/*
-```
-
-### Library Manager
-
-`LibraryManager` loads libraries from config and provides template discovery:
-
-- **Priority**: Libraries are searched in config order (first = highest priority)
-- **Deduplication**: Duplicate template IDs are resolved by priority
-- **Path Resolution**: Automatically handles `directory` config to locate templates
-
-### Config Manager
-
-`ConfigManager` handles all configuration:
-
-- **Location**: `~/.config/boilerplates/config.yaml`
-- **Atomic Writes**: Uses temp file + rename for safety
-- **Validation**: Comprehensive validation of all config fields
-- **Migration**: Auto-migrates old configs to add new sections
-
-**Main Sections:**
-- `defaults`: Per-module default variable values
-- `preferences`: User preferences (editor, output_dir, etc.)
-- `libraries`: Git repository configurations
-
-### Display Manager
-
-`DisplayManager` (`cli/core/display.py`) provides consistent output rendering:
-
-**Key Methods:**
-- `display_message(level, message, context)` - Unified message display
-- `display_success(message, context)` - Success messages
-- `display_error(message, context)` - Error messages  
-- `display_warning(message, context)` - Warning messages
-- `display_info(message, context)` - Info messages
-- `display_templates_table(templates, module, title)` - Template listings
-- `display_template_details(template, id)` - Detailed template view
-
-**Usage:**
-```python
-from cli.core.display import DisplayManager
-
-display = DisplayManager()
-display.display_success("Operation completed")
-display.display_error("Failed to process", context="module_name")
-```
-
-### Icon Manager
-
-`IconManager` provides **Nerd Font icons** for consistent CLI display:
+- `cli/` - Python CLI application source code
+  - `cli/core/` - Core Components of the CLI application
+  - `cli/modules/` - Modules implementing variable specs and technology-specific functions
+  - `cli/__main__.py` - CLI entry point, auto-discovers modules and registers commands
+- `library/` - Template collections organized by module
+  - `library/ansible/` - Ansible playbooks and configurations
+  - `library/compose/` - Docker Compose configurations
+  - `library/docker/` - Docker templates
+  - `library/kubernetes/` - Kubernetes deployments
+  - `library/packer/` - Packer templates
+  - `library/terraform/` - OpenTofu/Terraform templates and examples
 
-**Categories:**
-- **File Types**: `FILE_YAML`, `FILE_JSON`, `FILE_MARKDOWN`, `FILE_JINJA2`, `FILE_DOCKER`, etc.
-- **Status**: `STATUS_SUCCESS` (✓), `STATUS_ERROR` (✗), `STATUS_WARNING` (⚠), `STATUS_INFO` (ℹ)
-- **UI Elements**: `UI_CONFIG`, `UI_LOCK`, `UI_SETTINGS`, `UI_ARROW_RIGHT`
+### Core Components
 
-**Important:** Icons use Nerd Font glyphs (Unicode characters). The terminal must have a Nerd Font installed.
+- `cli/core/collection.py` - Dataclass for VariableCollection (stores variable sections and variables)
+- `cli/core/config.py` - Configuration management (loading, saving, validation)
+- `cli/core/display.py` - Centralized CLI output rendering (**Always use this to display output - never print directly**)
+- `cli/core/exceptions.py` - Custom exceptions for error handling (**Always use this for raising errors**)
+- `cli/core/library.py` - LibraryManager for template discovery from git-synced libraries and static file paths
+- `cli/core/module.py` - Abstract base class for modules (defines standard commands)
+- `cli/core/prompt.py` - Interactive CLI prompts using rich library
+- `cli/core/registry.py` - Central registry for module classes (auto-discovers modules)
+- `cli/core/repo.py` - Repository management for syncing git-based template libraries
+- `cli/core/sections.py` - Dataclass for VariableSection (stores section metadata and variables)
+- `cli/core/template.py` - Template Class for parsing, managing and rendering templates
+- `cli/core/variables.py` - Dataclass for Variable (stores variable metadata and values)
 
-**Usage:**
-```python
-from cli.core.display import IconManager
+### Modules
 
-# Get status icon
-icon = IconManager.get_status_icon("success")  # Returns \uf00c (✓)
+- `cli/modules/compose.py` - Docker Compose-specific functionality
+**(Work in Progress)**
+- `cli/modules/terraform.py` - Terraform-specific functionality
+- `cli/modules/docker.py` - Docker-specific functionality
+- `cli/modules/ansible.py` - Ansible-specific functionality
+- `cli/modules/kubernetes.py` - Kubernetes-specific functionality
+- `cli/modules/packer.py` - Packer-specific functionality
 
-# Get file icon
-icon = IconManager.get_file_icon("config.yaml")  # Returns \uf15c
+### LibraryManager
 
-# Direct access
-folder = IconManager.folder()  # \uf07b
-lock = IconManager.lock()  # \uf084
-```
+- Loads libraries from config file
+- Stores Git Libraries under: `~/.config/boilerplates/libraries/{name}/`
+- Uses sparse-checkout to clone only template directories for git-based libraries (avoiding unnecessary files)
 
-**Best Practices:**
-- ❌ **Don't use emojis** (✓, ✗, ⚠) directly in output
-- ✅ **Do use IconManager** for all icons and symbols
-- ✅ **Do use DisplayManager** for consistent formatting
-- Example: `display.display_success(f"Added {name}")` not `console.print(f"✓ Added {name}")`
+### ConfigManager
 
-## Architecture Notes
+- User Config stored in `~/.config/boilerplates/config.yaml`
 
-### Key Components
+### DisplayManager and IconManager
 
-Modular architecture with dynamic module discovery:
+External code should NEVER directly call `IconManager` or `console.print`, instead always use `DisplayManager` methods.
 
-- **`cli/__main__.py`**: Entry point. Auto-discovers modules and registers commands.
-- **`cli/core/registry.py`**: Central module class store.
-- **`cli/core/module.py`**: Abstract `Module` base class for standardized commands (list, search, show, generate).
-- **`cli/core/library.py`**: `LibraryManager` finds templates from git-synced libraries with priority system.
-- **`cli/core/repo.py`**: Repository management for syncing git-based template libraries.
-- **`cli/core/config.py`**: `ConfigManager` handles configuration, defaults, and library definitions.
-- **`cli/core/template.py`**: Parses templates, merges YAML frontmatter with Jinja2 content.
-- **`cli/core/variables.py`**: Variable data structures (`Variable`, `VariableSection`, `VariableCollection`).
-- **`cli/core/prompt.py`**: Interactive CLI prompts via `rich` library.
-- **`cli/core/display.py`**: Consistent output rendering with `DisplayManager` and `IconManager`.
+- `DisplayManager` provides a **centralized interface** for ALL CLI output rendering (Use `display_***` methods from `DisplayManager` for ALL output)
+- `IconManager` provides **Nerd Font icons** internally for DisplayManager, don't use Emojis or direct console access
 
-### Template Format
+## Templates
 
 Templates are directory-based. Each template is a directory containing all the necessary files and subdirectories for the boilerplate.
 
-#### Main Template File
-
-Requires `template.yaml` or `template.yml` with metadata and variables in YAML frontmatter:
+Requires `template.yaml` or `template.yml` with metadata and variables:
 
 ```yaml
 ---
@@ -260,30 +123,19 @@ spec:
         default: latest
 ```
 
-#### Template Files
+### Template Files
 
 - **Jinja2 Templates (`.j2`)**: Rendered by Jinja2, `.j2` extension removed in output. Support `{% include %}` and `{% import %}`.
 - **Static Files**: Non-`.j2` files copied as-is.
 - **Sanitization**: Auto-sanitized (single blank lines, no leading blanks, trimmed whitespace, single trailing newline).
 
-#### Example Directory Structure
-
-```
-library/compose/my-nginx-template/
-├── template.yaml
-├── compose.yaml.j2
-├── config/
-│   └── nginx.conf.j2
-└── static/
-    └── README.md
-```
-
-#### Variables
+### Variables
 
 **Precedence** (lowest to highest):
 1. Module `spec` (defaults for all templates of that kind)
 2. Template `spec` (overrides module defaults)
-3. CLI `--var` (highest priority)
+3. User `config.yaml` (overrides template and module defaults)
+4. CLI `--var` (highest priority)
 
 **Key Features:**
 - **Required Sections**: Mark with `required: true` (general is implicit). Users must provide all values.
@@ -295,56 +147,37 @@ library/compose/my-nginx-template/
 ```yaml
 spec:
   traefik:
-    title: "Traefik"
-    toggle: "traefik_enabled"
+    title: Traefik
+    required: false
+    toggle: traefik_enabled
     vars:
       traefik_enabled:
-        type: "bool"
+        type: bool
         default: false
       traefik_host:
-        type: "hostname"
+        type: hostname
   
   traefik_tls:
-    title: "Traefik TLS/SSL"
-    needs: "traefik"  # Only shown if traefik is enabled
-    toggle: "traefik_tls_enabled"
+    title: Traefik TLS/SSL
+    needs: traefik
+    toggle: traefik_tls_enabled
     vars:
       traefik_tls_enabled:
-        type: "bool"
+        type: bool
         default: true
       traefik_tls_certresolver:
-        type: "str"
+        type: str
+        sensitive: false
+        default: myresolver
 ```
 
-## Best Practices
+## Prompt
 
-### Template Structure
-- Include `template.yaml`/`template.yml` with descriptive IDs (lowercase-with-hyphens)
-- Use subdirectories for Jinja2 templates (e.g., `config/`)
-- Prefer `config` module for app-specific configs vs complex directories
+Uses `rich` library for interactive prompts. Supports:
+- Text input
+- Password input (masked)
+- Selection from list (single/multiple)
+- Confirmation (yes/no)
+- Default values
 
-### Variables
-- **Priority**: Prefer module spec → override when needed → add new only if unique
-- Use descriptive underscore names, always specify `type`
-- **Defaults**: Define sensible `default` values in `template.yaml` for all non-required variables (improves non-interactive generation)
-- **Credentials**: Mark with `sensitive: true` (hides input), `autogenerated: true` (auto-generates secure values when empty)
-
-### Jinja2
-- Keep logic simple, add descriptive comments
-
-### Docker Compose
-
-**Naming Conventions:**
-- Service: `service_name`, `container_name`, `container_timezone`, `restart_policy`
-- App: Prefix with app name (e.g., `authentik_secret_key`)
-- Database: `database_*` (type, enabled, external, host, port, name, user, password)
-- Network: `network_*` (enabled, name, external)
-- Traefik: `traefik_*` (enabled, host, tls_enabled, tls_entrypoint, tls_certresolver)
-- Ports: `ports_*` (enabled, http, https, ssh)
-- Email: `email_*` (enabled, host, port, username, password, from)
-
-**Patterns:**
-- Use scoped `.env.{service}.j2` files for better security/organization
-- Always: `depends_on`, named volumes, health checks (DB), `restart: {{ restart_policy | default('unless-stopped') }}`
-- Conditionals: `{% if not database_external %}` for service creation
-- Common toggles: `database_enabled`, `email_enabled`, `traefik_enabled`, `ports_enabled`, `network_enabled`
+To skip the prompt use the `--no-interactive` flag, which will use defaults or empty values.

+ 150 - 1
cli/core/display.py

@@ -145,7 +145,21 @@ class IconManager:
 
 
 class DisplayManager:
-    """Handles all rich rendering for the CLI."""
+    """Handles all rich rendering for the CLI.
+    
+    This class is responsible for ALL display output in the CLI, including:
+    - Status messages (success, error, warning, info)
+    - Tables (templates, summaries, results)
+    - Trees (file structures, configurations)
+    - Confirmation dialogs and prompts
+    - Headers and sections
+    
+    Design Principles:
+    - All display logic should go through DisplayManager methods
+    - IconManager is ONLY used internally by DisplayManager
+    - External code should never directly call IconManager or console.print
+    - Consistent formatting across all display types
+    """
 
     def display_templates_table(
         self, templates: list, module_name: str, title: str
@@ -517,3 +531,138 @@ class DisplayManager:
             logger.warning(f"Failed to render next_steps as template: {e}")
             # Fallback to plain text if rendering fails
             console.print(next_steps)
+    
+    def display_status_table(self, title: str, rows: list[tuple[str, str, bool]], 
+                            columns: tuple[str, str] = ("Item", "Status")) -> None:
+        """Display a status table with success/error indicators.
+        
+        Args:
+            title: Table title
+            rows: List of tuples (name, message, success_bool)
+            columns: Column headers (name_header, status_header)
+        """
+        table = Table(title=title, show_header=True)
+        table.add_column(columns[0], style="cyan", no_wrap=True)
+        table.add_column(columns[1])
+        
+        for name, message, success in rows:
+            status_style = "green" if success else "red"
+            status_icon = IconManager.get_status_icon("success" if success else "error")
+            table.add_row(name, f"[{status_style}]{status_icon} {message}[/{status_style}]")
+        
+        console.print(table)
+    
+    def display_summary_table(self, title: str, items: dict[str, str]) -> None:
+        """Display a simple two-column summary table.
+        
+        Args:
+            title: Table title
+            items: Dictionary of key-value pairs to display
+        """
+        table = Table(title=title, show_header=False, box=None, padding=(0, 2))
+        table.add_column(style="bold")
+        table.add_column()
+        
+        for key, value in items.items():
+            table.add_row(key, value)
+        
+        console.print(table)
+    
+    def display_file_operation_table(self, files: list[tuple[str, int, str]]) -> None:
+        """Display a table of file operations with sizes and statuses.
+        
+        Args:
+            files: List of tuples (file_path, size_bytes, status)
+        """
+        table = Table(show_header=True, header_style="bold cyan", box=None, padding=(0, 1))
+        table.add_column("File", style="white", no_wrap=False)
+        table.add_column("Size", justify="right", style="dim")
+        table.add_column("Status", style="yellow")
+        
+        for file_path, size_bytes, status in files:
+            # Format size
+            if size_bytes < 1024:
+                size_str = f"{size_bytes}B"
+            elif size_bytes < 1024 * 1024:
+                size_str = f"{size_bytes / 1024:.1f}KB"
+            else:
+                size_str = f"{size_bytes / (1024 * 1024):.1f}MB"
+            
+            table.add_row(str(file_path), size_str, status)
+        
+        console.print(table)
+    
+    def display_heading(self, text: str, icon_type: str | None = None, style: str = "bold") -> None:
+        """Display a heading with optional icon.
+        
+        Args:
+            text: Heading text
+            icon_type: Type of icon to display (e.g., 'folder', 'file', 'config')
+            style: Rich style to apply
+        """
+        if icon_type:
+            icon = self._get_icon_by_type(icon_type)
+            console.print(f"[{style}]{icon} {text}[/{style}]")
+        else:
+            console.print(f"[{style}]{text}[/{style}]")
+    
+    def display_warning_with_confirmation(self, message: str, details: list[str] | None = None, 
+                                         default: bool = False) -> bool:
+        """Display a warning message with optional details and get confirmation.
+        
+        Args:
+            message: Warning message to display
+            details: Optional list of detail lines to show
+            default: Default value for confirmation
+            
+        Returns:
+            True if user confirms, False otherwise
+        """
+        icon = IconManager.get_status_icon('warning')
+        console.print(f"\n[yellow]{icon} {message}[/yellow]")
+        
+        if details:
+            for detail in details:
+                console.print(f"[yellow]  {detail}[/yellow]")
+        
+        from rich.prompt import Confirm
+        return Confirm.ask("Continue?", default=default)
+    
+    def display_skipped(self, message: str, reason: str | None = None) -> None:
+        """Display a skipped/disabled message.
+        
+        Args:
+            message: The main message to display
+            reason: Optional reason why it was skipped
+        """
+        icon = IconManager.get_status_icon('skipped')
+        if reason:
+            console.print(f"\n[dim]{icon} {message} (skipped - {reason})[/dim]")
+        else:
+            console.print(f"\n[dim]{icon} {message} (skipped)[/dim]")
+    
+    def get_lock_icon(self) -> str:
+        """Get the lock icon for sensitive variables.
+        
+        Returns:
+            Lock icon unicode character
+        """
+        return IconManager.lock()
+    
+    def _get_icon_by_type(self, icon_type: str) -> str:
+        """Get icon by semantic type name.
+        
+        Args:
+            icon_type: Type of icon (e.g., 'folder', 'file', 'config', 'lock')
+            
+        Returns:
+            Icon unicode character
+        """
+        icon_map = {
+            'folder': IconManager.folder(),
+            'file': IconManager.FILE_DEFAULT,
+            'config': IconManager.config(),
+            'lock': IconManager.lock(),
+            'arrow': IconManager.arrow_right(),
+        }
+        return icon_map.get(icon_type, '')

+ 62 - 68
cli/core/module.py

@@ -11,7 +11,7 @@ from rich.panel import Panel
 from rich.prompt import Confirm
 from typer import Argument, Context, Option, Typer, Exit
 
-from .display import DisplayManager, IconManager
+from .display import DisplayManager
 from .library import LibraryManager
 from .prompt import PromptHandler
 from .template import Template
@@ -136,7 +136,7 @@ class Module(ABC):
       )
     else:
       logger.info(f"No templates found matching '{query}' for module '{self.name}'")
-      console.print(f"[yellow]No templates found matching '{query}' for module '{self.name}'[/yellow]")
+      self.display.display_warning(f"No templates found matching '{query}'", context=f"module '{self.name}'")
 
     return filtered_templates
 
@@ -262,12 +262,16 @@ class Module(ABC):
     # Warn if directory is not empty
     if dir_not_empty:
       if interactive:
-        console.print(f"\n[yellow]{IconManager.get_status_icon('warning')} Warning: Directory '{output_dir}' is not empty.[/yellow]")
+        details = []
         if existing_files:
-          console.print(f"[yellow]  {len(existing_files)} file(s) will be overwritten.[/yellow]")
+          details.append(f"{len(existing_files)} file(s) will be overwritten.")
         
-        if not Confirm.ask(f"Continue and potentially overwrite files in '{output_dir}'?", default=False):
-          console.print("[yellow]Generation cancelled.[/yellow]")
+        if not self.display.display_warning_with_confirmation(
+          f"Directory '{output_dir}' is not empty.",
+          details if details else None,
+          default=False
+        ):
+          self.display.display_info("Generation cancelled")
           return None
       else:
         # Non-interactive mode: show warning but continue
@@ -305,7 +309,7 @@ class Module(ABC):
     # Final confirmation (only if we didn't already ask about overwriting)
     if not dir_not_empty and not dry_run:
       if not Confirm.ask("Generate these files?", default=True):
-        console.print("[yellow]Generation cancelled.[/yellow]")
+        self.display.display_info("Generation cancelled")
         return False
     
     return True
@@ -323,31 +327,30 @@ class Module(ABC):
         show_files: Whether to display file contents
     """
     import os
-    from rich.table import Table
     
     console.print()
     console.print("[bold cyan]Dry Run Mode - Simulating File Generation[/bold cyan]")
     console.print()
     
     # Simulate directory creation
-    console.print(f"[bold]{IconManager.folder()} Directory Operations:[/bold]")
+    self.display.display_heading("Directory Operations", icon_type="folder")
     
     # Check if output directory exists
     if output_dir.exists():
-      console.print(f"  [green]{IconManager.get_status_icon('success')}[/green] Output directory exists: [cyan]{output_dir}[/cyan]")
+      self.display.display_success(f"Output directory exists: [cyan]{output_dir}[/cyan]")
       # Check if we have write permissions
       if os.access(output_dir, os.W_OK):
-        console.print(f"  [green]{IconManager.get_status_icon('success')}[/green] Write permission verified")
+        self.display.display_success("Write permission verified")
       else:
-        console.print(f"  [yellow]{IconManager.get_status_icon('warning')}[/yellow] Write permission may be denied")
+        self.display.display_warning("Write permission may be denied")
     else:
-      console.print(f"  [dim]{IconManager.arrow_right()}[/dim] Would create output directory: [cyan]{output_dir}[/cyan]")
+      console.print(f"  [dim][/dim] Would create output directory: [cyan]{output_dir}[/cyan]")
       # Check if parent directory exists and is writable
       parent = output_dir.parent
       if parent.exists() and os.access(parent, os.W_OK):
-        console.print(f"  [green]{IconManager.get_status_icon('success')}[/green] Parent directory writable")
+        self.display.display_success("Parent directory writable")
       else:
-        console.print(f"  [yellow]{IconManager.get_status_icon('warning')}[/yellow] Parent directory may not be writable")
+        self.display.display_warning("Parent directory may not be writable")
     
     # Collect unique subdirectories that would be created
     subdirs = set()
@@ -357,23 +360,19 @@ class Module(ABC):
         subdirs.add(Path(*parts[:i]))
     
     if subdirs:
-      console.print(f"  [dim]{IconManager.arrow_right()}[/dim] Would create {len(subdirs)} subdirectory(ies)")
+      console.print(f"  [dim][/dim] Would create {len(subdirs)} subdirectory(ies)")
       for subdir in sorted(subdirs):
-        console.print(f"    [dim]{IconManager.folder()}[/dim] {subdir}/")
+        console.print(f"    [dim]📁[/dim] {subdir}/")
     
     console.print()
     
     # Display file operations in a table
-    console.print(f"[bold]{IconManager.get_file_icon('file.txt')} File Operations:[/bold]")
-    
-    table = Table(show_header=True, header_style="bold cyan", box=None, padding=(0, 1))
-    table.add_column("File", style="white", no_wrap=False)
-    table.add_column("Size", justify="right", style="dim")
-    table.add_column("Status", style="yellow")
+    self.display.display_heading("File Operations", icon_type="file")
     
     total_size = 0
     new_files = 0
     overwrite_files = 0
+    file_operations = []
     
     for file_path, content in sorted(rendered_files.items()):
       full_path = output_dir / file_path
@@ -388,32 +387,26 @@ class Module(ABC):
         status = "Create"
         new_files += 1
       
-      # Format size
-      if file_size < 1024:
-        size_str = f"{file_size}B"
-      elif file_size < 1024 * 1024:
-        size_str = f"{file_size / 1024:.1f}KB"
-      else:
-        size_str = f"{file_size / (1024 * 1024):.1f}MB"
-      
-      table.add_row(str(file_path), size_str, status)
+      file_operations.append((file_path, file_size, status))
     
-    console.print(table)
+    self.display.display_file_operation_table(file_operations)
     console.print()
     
     # Summary statistics
-    console.print(f"[bold]{IconManager.get_status_icon('info')} Summary:[/bold]")
-    console.print(f"  Total files: {len(rendered_files)}")
-    console.print(f"  New files: {new_files}")
-    console.print(f"  Files to overwrite: {overwrite_files}")
-    
     if total_size < 1024:
       size_str = f"{total_size}B"
     elif total_size < 1024 * 1024:
       size_str = f"{total_size / 1024:.1f}KB"
     else:
       size_str = f"{total_size / (1024 * 1024):.1f}MB"
-    console.print(f"  Total size: {size_str}")
+    
+    summary_items = {
+      "Total files:": str(len(rendered_files)),
+      "New files:": str(new_files),
+      "Files to overwrite:": str(overwrite_files),
+      "Total size:": size_str
+    }
+    self.display.display_summary_table("Summary", summary_items)
     console.print()
     
     # Show file contents if requested
@@ -427,7 +420,7 @@ class Module(ABC):
         print()  # Add blank line after content
       console.print()
     
-    console.print(f"[yellow]{IconManager.get_status_icon('success')} Dry run complete - no files were written[/yellow]")
+    self.display.display_success("Dry run complete - no files were written")
     console.print(f"[dim]Files would have been generated in '{output_dir}'[/dim]")
     logger.info(f"Dry run completed for template '{id}' - {len(rendered_files)} files, {total_size} bytes")
 
@@ -445,9 +438,9 @@ class Module(ABC):
       full_path.parent.mkdir(parents=True, exist_ok=True)
       with open(full_path, 'w', encoding='utf-8') as f:
         f.write(content)
-      console.print(f"[green]Generated file: {file_path}[/green]")
+      console.print(f"[green]Generated file: {file_path}[/green]")  # Keep simple per-file output
     
-    console.print(f"\n[green]{IconManager.get_status_icon('success')} Template generated successfully in '{output_dir}'[/green]")
+    self.display.display_success(f"Template generated successfully in '{output_dir}'")
     logger.info(f"Template written to directory: {output_dir}")
 
   def generate(
@@ -619,7 +612,7 @@ class Module(ABC):
     
     # Set the default value
     config.set_default_value(self.name, actual_var_name, actual_value)
-    console.print(f"[green]{IconManager.get_status_icon('success')} Set default:[/green] [cyan]{actual_var_name}[/cyan] = [yellow]{actual_value}[/yellow]")
+    self.display.display_success(f"Set default: [cyan]{actual_var_name}[/cyan] = [yellow]{actual_value}[/yellow]")
     console.print(f"\n[dim]This will be used as the default value when generating templates with this module.[/dim]")
 
   def config_remove(
@@ -643,9 +636,9 @@ class Module(ABC):
     if var_name in defaults:
       del defaults[var_name]
       config.set_defaults(self.name, defaults)
-      console.print(f"[green]{IconManager.get_status_icon('success')} Removed default for '{var_name}'[/green]")
+      self.display.display_success(f"Removed default for '{var_name}'")
     else:
-      console.print(f"[red]No default found for variable '{var_name}'[/red]")
+      self.display.display_error(f"No default found for variable '{var_name}'")
 
   def config_clear(
     self,
@@ -674,24 +667,27 @@ class Module(ABC):
       if var_name in defaults:
         del defaults[var_name]
         config.set_defaults(self.name, defaults)
-        console.print(f"[green]{IconManager.get_status_icon('success')} Cleared default for '{var_name}'[/green]")
+        self.display.display_success(f"Cleared default for '{var_name}'")
       else:
-        console.print(f"[red]No default found for variable '{var_name}'[/red]")
+        self.display.display_error(f"No default found for variable '{var_name}'")
     else:
       # Clear all defaults
       if not force:
-        console.print(f"[bold yellow]{IconManager.get_status_icon('warning')} Warning:[/bold yellow] This will clear ALL defaults for module '[cyan]{self.name}[/cyan]'")
-        console.print()
-        # Show what will be cleared
+        detail_lines = [f"This will clear ALL defaults for module '{self.name}':", ""]
         for var_name, var_value in defaults.items():
-          console.print(f"  [green]{var_name}[/green] = [yellow]{var_value}[/yellow]")
+          detail_lines.append(f"  [green]{var_name}[/green] = [yellow]{var_value}[/yellow]")
+        
+        self.display.display_warning("Warning: This will clear ALL defaults")
+        console.print()
+        for line in detail_lines:
+          console.print(line)
         console.print()
         if not Confirm.ask(f"[bold red]Are you sure?[/bold red]", default=False):
           console.print("[green]Operation cancelled.[/green]")
           return
       
       config.clear_defaults(self.name)
-      console.print(f"[green]{IconManager.get_status_icon('success')} Cleared all defaults for module '{self.name}'[/green]")
+      self.display.display_success(f"Cleared all defaults for module '{self.name}'")
 
   def config_list(self) -> None:
     """Display the defaults for this specific module in YAML format.
@@ -770,7 +766,7 @@ class Module(ABC):
           _ = template.used_variables
           # Trigger variable definition validation by accessing variables
           _ = template.variables
-          console.print(f"[green]{IconManager.get_status_icon('success')} Jinja2 validation passed[/green]")
+          self.display.display_success("Jinja2 validation passed")
           
           # Semantic validation
           if semantic:
@@ -792,9 +788,9 @@ class Module(ABC):
                   has_semantic_errors = True
             
             if not has_semantic_errors:
-              console.print(f"\n[green]{IconManager.get_status_icon('success')} Semantic validation passed[/green]")
+              self.display.display_success("Semantic validation passed")
             else:
-              console.print(f"\n[red]{IconManager.get_status_icon('error')} Semantic validation found errors[/red]")
+              self.display.display_error("Semantic validation found errors")
               raise Exit(code=1)
           
           if verbose:
@@ -802,7 +798,7 @@ class Module(ABC):
             console.print(f"[dim]Found {len(template.used_variables)} variables[/dim]")
             console.print(f"[dim]Generated {len(rendered_files)} files[/dim]")
         except ValueError as e:
-          console.print(f"[red]{IconManager.get_status_icon('error')} Validation failed for '{template_id}':[/red]")
+          self.display.display_error(f"Validation failed for '{template_id}':")
           console.print(f"\n{e}")
           raise Exit(code=1)
           
@@ -828,27 +824,25 @@ class Module(ABC):
           _ = template.variables
           valid_count += 1
           if verbose:
-            console.print(f"[green]{IconManager.get_status_icon('success')}[/green] {template_id}")
+            self.display.display_success(template_id)
         except ValueError as e:
           invalid_count += 1
           errors.append((template_id, str(e)))
           if verbose:
-            console.print(f"[red]{IconManager.get_status_icon('error')}[/red] {template_id}")
+            self.display.display_error(template_id)
         except Exception as e:
           invalid_count += 1
           errors.append((template_id, f"Load error: {e}"))
           if verbose:
-            console.print(f"[yellow]{IconManager.get_status_icon('warning')}[/yellow] {template_id}")
+            self.display.display_warning(template_id)
       
       # Summary
-      console.print(f"\n[bold]Validation Summary:[/bold]")
-      summary_table = Table(show_header=False, box=None, padding=(0, 2))
-      summary_table.add_column(style="bold")
-      summary_table.add_column()
-      summary_table.add_row("Total templates:", str(total))
-      summary_table.add_row("[green]Valid:[/green]", str(valid_count))
-      summary_table.add_row("[red]Invalid:[/red]", str(invalid_count))
-      console.print(summary_table)
+      summary_items = {
+        "Total templates:": str(total),
+        "[green]Valid:[/green]": str(valid_count),
+        "[red]Invalid:[/red]": str(invalid_count)
+      }
+      self.display.display_summary_table("Validation Summary", summary_items)
       
       # Show errors if any
       if errors:
@@ -858,7 +852,7 @@ class Module(ABC):
           console.print(f"[dim]{error_msg}[/dim]")
         raise Exit(code=1)
       else:
-        console.print(f"\n[green]{IconManager.get_status_icon('success')} All templates are valid![/green]")
+        self.display.display_success("All templates are valid!")
 
   @classmethod
   def register_cli(cls, app: Typer) -> None:

+ 3 - 5
cli/core/prompt.py

@@ -6,7 +6,7 @@ from rich.console import Console
 from rich.prompt import Prompt, Confirm, IntPrompt
 from rich.table import Table
 
-from .display import DisplayManager, IconManager
+from .display import DisplayManager
 from .variable import Variable
 from .collection import VariableCollection
 
@@ -53,9 +53,7 @@ class PromptHandler:
           else:
             unsatisfied_titles.append(dep_key)
         dep_names = ", ".join(unsatisfied_titles) if unsatisfied_titles else "unknown"
-        self.console.print(
-          f"\n[dim]{IconManager.get_status_icon('skipped')} {section.title} (skipped - requires {dep_names} to be enabled)[/dim]"
-        )
+        self.display.display_skipped(section.title, f"requires {dep_names} to be enabled")
         logger.debug(f"Skipping section '{section_key}' - dependencies not satisfied: {dep_names}")
         continue
 
@@ -123,7 +121,7 @@ class PromptHandler:
     if variable.sensitive or variable.autogenerated:
       # Format: "Prompt text 🔒 (default)"
       # The lock icon goes between the text and the default value in parentheses
-      prompt_text = f"{prompt_text} {IconManager.lock()}"
+      prompt_text = f"{prompt_text} {self.display.get_lock_icon()}"
 
     # Check if this specific variable is required (has no default and not autogenerated)
     var_is_required = variable.is_required()

+ 7 - 12
cli/core/repo.py

@@ -13,7 +13,7 @@ from rich.table import Table
 from typer import Argument, Option, Typer
 
 from ..core.config import ConfigManager
-from ..core.display import DisplayManager, IconManager
+from ..core.display import DisplayManager
 from ..core.exceptions import ConfigError
 
 logger = logging.getLogger(__name__)
@@ -191,7 +191,7 @@ def update(
     libraries = config.get_libraries()
     
     if not libraries:
-        console.print("[yellow]No libraries configured.[/yellow]")
+        display.display_warning("No libraries configured")
         console.print("Libraries are auto-configured on first run with a default library.")
         return
     
@@ -244,16 +244,11 @@ def update(
     
     # Display summary table
     if not verbose:
-        table = Table(title="Library Update Summary", show_header=True)
-        table.add_column("Library", style="cyan", no_wrap=True)
-        table.add_column("Status")
-        
-        for name, message, success in results:
-            status_style = "green" if success else "red"
-            status_icon = IconManager.get_status_icon("success" if success else "error")
-            table.add_row(name, f"[{status_style}]{status_icon} {message}[/{status_style}]")
-        
-        console.print(table)
+        display.display_status_table(
+            "Library Update Summary",
+            results,
+            columns=("Library", "Status")
+        )
     
     # Summary
     total = len(results)