Parcourir la source

refactored code

xcad il y a 7 mois
Parent
commit
4dbadc711a
79 fichiers modifiés avec 1592 ajouts et 2308 suppressions
  1. 1 1
      .editorconfig
  2. 4 3
      .github/copilot-instructions.md
  3. 4 3
      WARP.md
  4. 61 6
      cli/__main__.py
  5. 0 4
      cli/core/__init__.py
  6. 0 92
      cli/core/app.py
  7. 0 313
      cli/core/command.py
  8. 75 71
      cli/core/config.py
  9. 0 53
      cli/core/helpers.py
  10. 79 0
      cli/core/library.py
  11. 0 19
      cli/core/logging.py
  12. 0 44
      cli/core/models.py
  13. 178 0
      cli/core/module.py
  14. 374 579
      cli/core/prompt.py
  15. 64 0
      cli/core/registry.py
  16. 0 83
      cli/core/render.py
  17. 0 0
      cli/core/repo.py
  18. 141 66
      cli/core/template.py
  19. 0 153
      cli/core/values.py
  20. 241 216
      cli/core/variables.py
  21. 31 33
      cli/modules/__init__.py
  22. 17 0
      cli/modules/ansible.py
  23. 0 8
      cli/modules/ansible/__init__.py
  24. 0 19
      cli/modules/ansible/commands.py
  25. 52 0
      cli/modules/compose.py
  26. 0 8
      cli/modules/compose/__init__.py
  27. 0 241
      cli/modules/compose/commands.py
  28. 0 47
      cli/modules/compose/variables.py
  29. 16 0
      cli/modules/docker.py
  30. 0 8
      cli/modules/docker/__init__.py
  31. 0 19
      cli/modules/docker/commands.py
  32. 16 0
      cli/modules/github_actions.py
  33. 0 8
      cli/modules/github_actions/__init__.py
  34. 0 23
      cli/modules/github_actions/commands.py
  35. 16 0
      cli/modules/gitlab_ci.py
  36. 0 8
      cli/modules/gitlab_ci/__init__.py
  37. 0 23
      cli/modules/gitlab_ci/commands.py
  38. 16 0
      cli/modules/kestra.py
  39. 0 8
      cli/modules/kestra/__init__.py
  40. 0 23
      cli/modules/kestra/commands.py
  41. 18 0
      cli/modules/kubernetes.py
  42. 0 8
      cli/modules/kubernetes/__init__.py
  43. 0 23
      cli/modules/kubernetes/commands.py
  44. 16 0
      cli/modules/packer.py
  45. 0 8
      cli/modules/packer/__init__.py
  46. 0 23
      cli/modules/packer/commands.py
  47. 16 0
      cli/modules/terraform.py
  48. 0 8
      cli/modules/terraform/__init__.py
  49. 0 23
      cli/modules/terraform/commands.py
  50. 16 0
      cli/modules/vagrant.py
  51. 0 8
      cli/modules/vagrant/__init__.py
  52. 0 23
      cli/modules/vagrant/commands.py
  53. 63 0
      library/compose/n8n/compose.yaml
  54. 52 0
      library/compose/test-complex/compose.yaml
  55. 23 0
      library/compose/tests/compose.yaml
  56. 0 0
      library/packer/proxmox/README.md
  57. 0 0
      library/packer/proxmox/credentials.pkr.hcl
  58. 0 0
      library/packer/proxmox/ubuntu-server-focal-docker/files/99-pve.cfg
  59. 0 0
      library/packer/proxmox/ubuntu-server-focal-docker/http/meta-data
  60. 0 0
      library/packer/proxmox/ubuntu-server-focal-docker/http/user-data
  61. 0 0
      library/packer/proxmox/ubuntu-server-focal-docker/ubuntu-server-focal-docker.pkr.hcl
  62. 0 0
      library/packer/proxmox/ubuntu-server-focal/files/99-pve.cfg
  63. 0 0
      library/packer/proxmox/ubuntu-server-focal/http/meta-data
  64. 0 0
      library/packer/proxmox/ubuntu-server-focal/http/user-data
  65. 0 0
      library/packer/proxmox/ubuntu-server-focal/ubuntu-server-focal.pkr.hcl
  66. 0 0
      library/packer/proxmox/ubuntu-server-jammy-docker/files/99-pve.cfg
  67. 0 0
      library/packer/proxmox/ubuntu-server-jammy-docker/http/meta-data
  68. 0 0
      library/packer/proxmox/ubuntu-server-jammy-docker/http/user-data
  69. 0 0
      library/packer/proxmox/ubuntu-server-jammy-docker/ubuntu-server-jammy-docker.pkr.hcl
  70. 0 0
      library/packer/proxmox/ubuntu-server-jammy/files/99-pve.cfg
  71. 0 0
      library/packer/proxmox/ubuntu-server-jammy/http/meta-data
  72. 0 0
      library/packer/proxmox/ubuntu-server-jammy/http/user-data
  73. 0 0
      library/packer/proxmox/ubuntu-server-jammy/ubuntu-server-jammy.pkr.hcl
  74. 0 0
      library/packer/proxmox/ubuntu-server-noble/files/99-pve.cfg
  75. 0 0
      library/packer/proxmox/ubuntu-server-noble/http/meta-data
  76. 0 0
      library/packer/proxmox/ubuntu-server-noble/http/user-data
  77. 0 0
      library/packer/proxmox/ubuntu-server-noble/ubuntu-server-noble.pkr.hcl
  78. 1 1
      pyproject.toml
  79. 1 1
      setup.cfg

+ 1 - 1
.editorconfig

@@ -43,7 +43,7 @@ trim_trailing_whitespace = false
 indent_size = 2
 indent_size = 2
 
 
 [*.py]
 [*.py]
-indent_size = 4
+indent_size = 2
 
 
 [*.tf]
 [*.tf]
 indent_size = unset
 indent_size = unset

+ 4 - 3
.github/copilot-instructions.md

@@ -59,9 +59,9 @@ boilerplate --log-level DEBUG [command]
 
 
 ### Adding New Modules
 ### Adding New Modules
 
 
-1. Create new module directory in `cli/modules/[module_name]/`
-2. Implement module class inheriting from `BaseModule` in `cli/core/command.py`
-3. Add module to imports in `cli/modules/__init__.py`
+1. Create new module file: `cli/modules/[module_name].py`
+2. Implement module class inheriting from `BaseModule` in the file
+3. Add module to imports in `cli/__main__.py`
 4. Create corresponding template directory in `library/[module_name]/`
 4. Create corresponding template directory in `library/[module_name]/`
 
 
 ## Architecture Notes
 ## Architecture Notes
@@ -103,6 +103,7 @@ tags:
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
+- **Indentation**: ALWAYS use 2 spaces for indentation!
 
 
 ## Configuration
 ## Configuration
 
 

+ 4 - 3
WARP.md

@@ -59,9 +59,9 @@ boilerplate --log-level DEBUG [command]
 
 
 ### Adding New Modules
 ### Adding New Modules
 
 
-1. Create new module directory in `cli/modules/[module_name]/`
-2. Implement module class inheriting from `BaseModule` in `cli/core/command.py`
-3. Add module to imports in `cli/modules/__init__.py`
+1. Create new module file: `cli/modules/[module_name].py`
+2. Implement module class inheriting from `BaseModule` in the file
+3. Add module to imports in `cli/__main__.py`
 4. Create corresponding template directory in `library/[module_name]/`
 4. Create corresponding template directory in `library/[module_name]/`
 
 
 ## Architecture Notes
 ## Architecture Notes
@@ -103,6 +103,7 @@ tags:
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
+- **Spaces in Python**: Prefer using 2 Spaces for indentation
 
 
 ## Configuration
 ## Configuration
 
 

+ 61 - 6
cli/__main__.py

@@ -3,15 +3,70 @@
 Main entry point for the Boilerplates CLI application.
 Main entry point for the Boilerplates CLI application.
 This file serves as the primary executable when running the CLI.
 This file serves as the primary executable when running the CLI.
 """
 """
+import importlib
+import logging
+import pkgutil
+import sys
+from pathlib import Path
+from typer import Typer, Option, Context
+import cli.modules
+from cli.core.registry import registry
 
 
-from cli.core.app import create_app
+app = Typer(no_args_is_help=True)
 
 
+# Set up logging
+logging.basicConfig(
+    level=logging.CRITICAL,
+    format='[%(levelname)s] %(message)s',
+    stream=sys.stdout
+)
+logger = logging.getLogger('boilerplates')
 
 
-def main() -> None:
-    """Main entry point for the CLI application."""
-    app = create_app()
-    app()
+@app.callback()
+def main(
+    ctx: Context,
+    debug: bool = Option(False, "--debug", help="Enable debug logging")
+):
+  """Main CLI application for managing boilerplates."""
+  # Enable debug logging if requested
+  if debug:
+    logging.getLogger('boilerplates').setLevel(logging.DEBUG)
+    logger.debug("Debug logging enabled")
+  
+  logger.debug("Starting boilerplates CLI application")
 
 
+def init_app():
+  """Initialize the application by discovering and registering modules."""
+  try:
+    # Auto-discover and import all modules
+    modules_path = Path(cli.modules.__file__).parent
+    logger.debug(f"Discovering modules in: {modules_path}")
+    
+    for finder, name, ispkg in pkgutil.iter_modules([str(modules_path)]):
+      if not ispkg and not name.startswith('_') and name != 'base':
+        try:
+          logger.debug(f"Importing module: {name}")
+          importlib.import_module(f"cli.modules.{name}")
+        except ImportError as e:
+          logger.warning(f"Could not import {name}: {e}")
+    
+    # Register modules with app
+    logger.debug(f"Registering {len(registry.create_instances())} modules")
+    for module in registry.create_instances():
+      try:
+        logger.debug(f"Registering module: {module.__class__.__name__}")
+        module.register(app)
+      except Exception as e:
+        logger.error(f"Error registering {module.__class__.__name__}: {e}")
+    
+  except Exception as e:
+    logger.error(f"Application initialization error: {e}")
+    exit(1)
+
+def run():
+  """Run the CLI application."""
+  init_app()
+  app()
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-    main()
+  run()

+ 0 - 4
cli/core/__init__.py

@@ -1,4 +0,0 @@
-"""
-Core module for the Boilerplates CLI.
-Contains shared utilities, configuration, and base classes.
-"""

+ 0 - 92
cli/core/app.py

@@ -1,92 +0,0 @@
-"""
-Main application factory and CLI entry point.
-Creates and configures the main Typer application with all modules.
-"""
-
-import logging
-import sys
-from pathlib import Path
-from typing import Optional
-
-import typer
-from rich.console import Console
-from rich.traceback import install
-
-from cli import __version__
-from ..modules import get_all_modules
-
-
-from .logging import setup_logging
-
-
-def version_callback(value: bool):
-    """Callback for version option."""
-    if value:
-        console = Console()
-        console.print(f"Boilerplates CLI v{__version__}", style="bold blue")
-        raise typer.Exit()
-
-
-def create_app() -> typer.Typer:
-    """
-    Create and configure the main CLI application.
-    
-    Returns:
-        Configured Typer application with all modules registered.
-    """
-    # Install rich traceback handler for better error display
-    install(show_locals=True)
-    
-    # Create main app
-    app = typer.Typer(
-        name="boilerplates",
-        help="🚀 Sophisticated CLI tool for managing infrastructure boilerplates",
-        epilog="Made with ❤️  by Christian Lempa",
-        rich_markup_mode="rich",
-        no_args_is_help=True,
-    )
-    
-    @app.callback()
-    def main(
-        ctx: typer.Context,
-        version: Optional[bool] = typer.Option(
-            None, 
-            "--version", 
-            "-v",
-            callback=version_callback,
-            is_eager=True,
-            help="Show version and exit"
-        ),
-        log_level: str = typer.Option(
-            "WARNING",
-            "--log-level",
-            "-l",
-            help="Set logging level",
-            case_sensitive=False,
-        ),
-    ):
-        """
-        🚀 Boilerplates CLI - Manage your infrastructure templates with ease!
-        """
-        
-        # Configure logging
-        setup_logging(log_level=log_level.upper())
-        
-        # Store context for subcommands
-        ctx.ensure_object(dict)
-    
-    # Register all module commands
-    modules = get_all_modules()
-    for module in modules:
-        try:
-            module_app = module.get_app()
-            app.add_typer(
-                module_app,
-                name=module.name,
-                # Don't override help - let the module define it
-            )
-            logging.getLogger("boilerplates.app").info(f"Registered module: {module.name}")
-        except Exception as e:
-            logging.getLogger("boilerplates.app").error(f"Failed to register module {module.name}: {e}")
-    
-    return app

+ 0 - 313
cli/core/command.py

@@ -1,313 +0,0 @@
-"""
-Base classes and utilities for CLI modules and commands.
-Provides common functionality and patterns for all modules.
-"""
-
-import logging
-from abc import ABC, abstractmethod
-from pathlib import Path
-from typing import Optional, Set, Dict, Any, List, Tuple
-
-from rich.console import Console
-import typer
-
-from .config import ConfigManager
-from .helpers import find_boilerplates
-from . import template, values, render
-
-
-
-class BaseModule(ABC):
-    """Abstract base class for all CLI modules with shared commands."""
-    
-    def __init__(self, name: str, icon: str = "", description: str = ""):
-        self.name = name
-        self.icon = icon
-        self.description = description
-        self.console = Console()
-        self.logger = logging.getLogger(f"boilerplates.module.{name}")
-    
-    @property
-    def template_paths(self) -> List[str]:
-        """Return list of valid template file paths/patterns for this module.
-        Override this in modules that support template generation."""
-        return []
-        
-    @property
-    def library_path(self) -> Optional[Path]:
-        """Return the path to the template library for this module.
-        Override this in modules that support template generation."""
-        return None
-        
-    @property
-    def variable_handler_class(self) -> Any:
-        """Return the variable handler class for this module."""
-        return None
-        
-    def get_valid_variables(self) -> Set[str]:
-        """Get the set of valid variable names for this module."""
-        if self.variable_handler_class:
-            handler = self.variable_handler_class()
-            return set(handler._declared.keys())
-        return set()
-        
-    def process_template_content(self, content: str) -> str:
-        """Process template content before rendering. Override if needed."""
-        return content
-        
-    def get_template_syntax(self) -> str:
-        """Return the syntax highlighting to use for this template type."""
-        return "yaml"
-    
-    def get_app(self) -> typer.Typer:
-        """
-        Create and return the Typer app with shared commands.
-        Subclasses can override this to add module-specific commands.
-        """
-        app = typer.Typer(
-            name=self.name,
-            help=f"{self.icon} {self.description}",
-            rich_markup_mode="rich"
-        )
-        
-        # Add shared config commands
-        self._add_config_commands(app)
-        
-        # Add module-specific commands
-        self._add_module_commands(app)
-        
-        return app
-    
-    def _add_config_commands(self, app: typer.Typer) -> None:
-        """
-        Add shared configuration commands to the app.
-        These commands are available for all modules.
-        """
-        config_app = typer.Typer(name="config", help="Manage module configuration")
-        app.add_typer(config_app, name="config")
-        
-        @config_app.command("set", help="Set a configuration value")
-        def set_config(
-            key: str = typer.Argument(..., help="Configuration key"),
-            value: str = typer.Argument(..., help="Configuration value")
-        ):
-            """Set a configuration value for this module."""
-            # Validate that the key is a valid variable for this module
-            valid_vars = self.get_valid_variables()
-            if valid_vars and key not in valid_vars:
-                self.console.print(f"[red]✗[/red] Invalid config key '{key}'. Valid keys are: {', '.join(sorted(valid_vars))}")
-                raise typer.Exit(code=1)
-            
-            config_manager = ConfigManager(self.name)
-            try:
-                # Try to parse as JSON for complex values
-                import json
-                try:
-                    parsed_value = json.loads(value)
-                except json.JSONDecodeError:
-                    parsed_value = value
-                
-                config_manager.set(key, parsed_value)
-                self.console.print(f"[green]✓[/green] Set {self.name} config '{key}' = {parsed_value}")
-            except Exception as e:
-                self.console.print(f"[red]✗[/red] Failed to set config: {e}")
-        
-        @config_app.command("get", help="Get a configuration value")
-        def get_config(
-            key: str = typer.Argument(..., help="Configuration key"),
-            default: Optional[str] = typer.Option(None, "--default", "-d", help="Default value if key not found")
-        ):
-            """Get a configuration value for this module."""
-            config_manager = ConfigManager(self.name)
-            value = config_manager.get(key, default)
-            if value is None:
-                self.console.print(f"[yellow]⚠[/yellow] Config key '{key}' not found")
-                return
-            
-            import json
-            if isinstance(value, (dict, list)):
-                self.console.print(json.dumps(value, indent=2))
-            else:
-                self.console.print(f"{key}: {value}")
-        
-        @config_app.command("list", help="List all configuration values")
-        def list_config():
-            """List all configuration values for this module."""
-            config_manager = ConfigManager(self.name)
-            config = config_manager.list_all()
-            if not config:
-                self.console.print(f"[yellow]No configuration found for {self.name}[/yellow]")
-                return
-            
-            from rich.table import Table
-            table = Table(title=f"⚙️  {self.name.title()} Configuration", title_style="bold blue")
-            table.add_column("Key", style="cyan", no_wrap=True)
-            table.add_column("Value", style="green")
-            
-            import json
-            for key, value in config.items():
-                if isinstance(value, (dict, list)):
-                    value_str = json.dumps(value, indent=2)
-                else:
-                    value_str = str(value)
-                table.add_row(key, value_str)
-            
-            self.console.print(table)
-        
-        @config_app.command("delete", help="Delete a configuration value")
-        def delete_config(key: str = typer.Argument(..., help="Configuration key")):
-            """Delete a configuration value for this module."""
-            config_manager = ConfigManager(self.name)
-            if config_manager.delete(key):
-                self.console.print(f"[green]✓[/green] Deleted config key '{key}'")
-            else:
-                self.console.print(f"[yellow]⚠[/yellow] Config key '{key}' not found")
-        
-        @config_app.command("variables", help="List valid configuration variables for this module")
-        def list_variables():
-            """List all valid configuration variables for this module."""
-            valid_vars = self.get_valid_variables()
-            if not valid_vars:
-                self.console.print(f"[yellow]No variables defined for {self.name} module yet.[/yellow]")
-                return
-            
-            from rich.table import Table
-            table = Table(title=f"🔧 Valid {self.name.title()} Variables", title_style="bold blue")
-            table.add_column("Variable Name", style="cyan", no_wrap=True)
-            table.add_column("Set", style="magenta")
-            table.add_column("Type", style="green")
-            table.add_column("Description", style="dim")
-            
-            # Get detailed variable information
-            if hasattr(self, '_get_variable_details'):
-                var_details = self._get_variable_details()
-                for var_name in sorted(valid_vars):
-                    if var_name in var_details:
-                        detail = var_details[var_name]
-                        table.add_row(
-                            var_name,
-                            detail.get('set', 'unknown'),
-                            detail.get('type', 'str'),
-                            detail.get('display_name', '')
-                        )
-                    else:
-                        table.add_row(var_name, 'unknown', 'str', '')
-            else:
-                for var_name in sorted(valid_vars):
-                    table.add_row(var_name, 'unknown', 'str', '')
-            
-            self.console.print(table)
-    
-    def _add_module_commands(self, app: typer.Typer) -> None:
-        """Add module-specific commands to the app."""
-        # Only add generate command if module supports templates
-        if self.library_path is not None and self.template_paths:
-            self._add_generate_command(app)
-        self._add_custom_commands(app)
-    
-    def _add_custom_commands(self, app: typer.Typer) -> None:
-        """Override this method in subclasses to add module-specific commands."""
-        pass
-    
-    def _add_generate_command(self, app: typer.Typer) -> None:
-        """Add the generate command to the app."""
-        
-        @app.command("generate", help="Generate from a template and write to --out")
-        def generate(
-            name: str,
-            out: Optional[Path] = typer.Option(None, "--out", "-o",
-                help="Output path to write rendered template (prints to stdout when omitted)"),
-            values_file: Optional[Path] = typer.Option(None, "--values-file", "-f",
-                help="Load values from YAML/JSON file"),
-            values: Optional[List[str]] = typer.Option(None, "--values",
-                help="Set values (format: key=value)")
-        ):
-            """Generate output from a template with optional value overrides."""
-            # Find and validate template
-            bps = find_boilerplates(self.library_path, self.template_paths)
-            bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
-            if not bp:
-                self.console.print(f"[red]Template '{name}' not found.[/red]")
-                raise typer.Exit(code=1)
-            
-            # Get variable handler if module provides one
-            var_handler = None
-            if self.variable_handler_class:
-                var_handler = self.variable_handler_class()
-            
-            # Clean and process template content
-            content = self.process_template_content(bp.content)
-            cleaned_content = template.clean_template_content(content)
-            
-            # Find variables if handler exists
-            used_vars = set()
-            if var_handler:
-                _, used_vars = var_handler.determine_variable_sets(cleaned_content)
-            
-            if not used_vars:
-                rendered = content
-            else:
-                # Validate template syntax
-                is_valid, error = template.validate_template(cleaned_content, bp.file_path)
-                if not is_valid:
-                    self.console.print(f"[red]{error}[/red]")
-                    raise typer.Exit(code=2)
-                
-                # Extract defaults and metadata if handler exists
-                template_defaults = {}
-                if var_handler:
-                    template_defaults = var_handler.extract_template_defaults(cleaned_content)
-                    try:
-                        meta_overrides = var_handler.extract_variable_meta_overrides(content)
-                        for var_name, overrides in meta_overrides.items():
-                            if var_name in var_handler._declared and isinstance(overrides, dict):
-                                existing = var_handler._declared[var_name][1]
-                                existing.update(overrides)
-                    except Exception:
-                        pass
-                
-                # Get subscript keys and load values from all sources
-                used_subscripts = set()
-                if var_handler:
-                    used_subscripts = var_handler.find_used_subscript_keys(content)
-                
-                # Load and merge values from all sources
-                try:
-                    merged_values = values.load_and_merge_values(
-                        values_file=values_file,
-                        cli_values=values,
-                        config_values=ConfigManager(self.name).list_all(),
-                        defaults=template_defaults
-                    )
-                except Exception as e:
-                    self.console.print(f"[red]{str(e)}[/red]")
-                    raise typer.Exit(code=1)
-                
-                # Collect final values and render template
-                values_dict = {}
-                if var_handler:
-                    values_dict = var_handler.collect_values(
-                        used_vars,
-                        merged_values,
-                        used_subscripts
-                    )
-                else:
-                    values_dict = merged_values
-                
-                success, rendered, error = template.render_template(
-                    cleaned_content,
-                    values_dict
-                )
-                
-                if not success:
-                    self.console.print(f"[red]{error}[/red]")
-                    raise typer.Exit(code=2)
-            
-            # Output the rendered content
-            output_handler = render.RenderOutput(self.console)
-            output_handler.output_rendered_content(
-                rendered,
-                out,
-                self.get_template_syntax(),
-                bp.name
-            )

+ 75 - 71
cli/core/config.py

@@ -1,75 +1,79 @@
-"""
-Configuration management for the Boilerplates CLI.
-Handles module-specific configuration stored in config.json files.
-"""
-
-import json
-import os
-from pathlib import Path
 from typing import Any, Dict, Optional
 from typing import Any, Dict, Optional
-
-from .logging import setup_logging
+from pathlib import Path
 
 
 
 
 class ConfigManager:
 class ConfigManager:
-    """Manages configuration for CLI modules."""
-
-    def __init__(self, module_name: str):
-        self.module_name = module_name
-        self.config_dir = Path.home() / ".boilerplates"
-        self.config_file = self.config_dir / f"{module_name}.json"
-        self.logger = setup_logging()
-
-    def _ensure_config_dir(self) -> None:
-        """Ensure the configuration directory exists."""
-        self.config_dir.mkdir(parents=True, exist_ok=True)
-
-    def _load_config(self) -> Dict[str, Any]:
-        """Load configuration from file."""
-        if not self.config_file.exists():
-            return {}
-
-        try:
-            with open(self.config_file, 'r', encoding='utf-8') as f:
-                return json.load(f)
-        except (json.JSONDecodeError, IOError) as e:
-            self.logger.warning(f"Failed to load config for {self.module_name}: {e}")
-            return {}
-
-    def _save_config(self, config: Dict[str, Any]) -> None:
-        """Save configuration to file."""
-        self._ensure_config_dir()
-        try:
-            with open(self.config_file, 'w', encoding='utf-8') as f:
-                json.dump(config, f, indent=2, ensure_ascii=False)
-        except IOError as e:
-            self.logger.error(f"Failed to save config for {self.module_name}: {e}")
-            raise
-
-    def get(self, key: str, default: Any = None) -> Any:
-        """Get a configuration value."""
-        config = self._load_config()
-        return config.get(key, default)
-
-    def set(self, key: str, value: Any) -> None:
-        """Set a configuration value."""
-        config = self._load_config()
-        config[key] = value
-        self._save_config(config)
-
-    def delete(self, key: str) -> bool:
-        """Delete a configuration value."""
-        config = self._load_config()
-        if key in config:
-            del config[key]
-            self._save_config(config)
-            return True
-        return False
-
-    def list_all(self) -> Dict[str, Any]:
-        """List all configuration values."""
-        return self._load_config()
-
-    def get_config_path(self) -> Path:
-        """Get the path to the configuration file."""
-        return self.config_file
+  """Placeholder for configuration management.
+  
+  This will handle loading and saving user configuration including:
+  - Variable default values (highest priority)
+  - Module settings
+  - User preferences
+  
+  TODO: Implement actual configuration persistence and loading
+  """
+  
+  def __init__(self, config_dir: Optional[Path] = None):
+    """Initialize the configuration manager.
+    
+    Args:
+        config_dir: Directory to store configuration files. 
+                   Defaults to ~/.boilerplates/
+    """
+    if config_dir is None:
+      config_dir = Path.home() / ".boilerplates"
+    
+    self.config_dir = config_dir
+    self.config_dir.mkdir(parents=True, exist_ok=True)
+  
+  def get_variable_defaults(self, module_name: str) -> Dict[str, Any]:
+    """Get user-configured default values for variables in a module.
+    
+    Args:
+        module_name: Name of the module (e.g., 'compose', 'terraform')
+        
+    Returns:
+        Dictionary mapping variable names to their user-configured default values
+        
+    TODO: Implement actual config file loading
+    """
+    # Placeholder implementation - returns empty dict
+    return {}
+  
+  def save_variable_defaults(self, module_name: str, variable_defaults: Dict[str, Any]) -> None:
+    """Save user-configured default values for variables in a module.
+    
+    Args:
+        module_name: Name of the module (e.g., 'compose', 'terraform')
+        variable_defaults: Dictionary mapping variable names to their default values
+        
+    TODO: Implement actual config file saving
+    """
+    # Placeholder implementation - does nothing
+    pass
+  
+  def get_module_config(self, module_name: str) -> Dict[str, Any]:
+    """Get module-specific configuration.
+    
+    Args:
+        module_name: Name of the module
+        
+    Returns:
+        Dictionary with module configuration
+        
+    TODO: Implement actual config loading
+    """
+    # Placeholder implementation - returns empty dict
+    return {}
+  
+  def save_module_config(self, module_name: str, config: Dict[str, Any]) -> None:
+    """Save module-specific configuration.
+    
+    Args:
+        module_name: Name of the module
+        config: Dictionary with module configuration
+        
+    TODO: Implement actual config saving
+    """
+    # Placeholder implementation - does nothing
+    pass

+ 0 - 53
cli/core/helpers.py

@@ -1,53 +0,0 @@
-"""
-Helper functions for common boilerplate operations.
-Provides reusable utilities for scanning directories and formatting output.
-"""
-
-from pathlib import Path
-from typing import List
-import frontmatter
-
-from .models import Boilerplate
-
-
-def find_boilerplates(library_path: Path, file_names: List[str]) -> List[Boilerplate]:
-    """
-    Find all boilerplate files in the library directory and extract metadata.
-    
-    Args:
-        library_path: Path to the library directory to scan
-        file_names: List of file names to search for (e.g., ['compose.yaml', 'docker-compose.yaml'])
-    
-    Returns:
-        List of Boilerplate objects sorted by name
-    """
-    boilerplates = []
-    
-    # Recursively scan all directories
-    for boilerplate_file in library_path.rglob("*"):
-        if boilerplate_file.is_file() and boilerplate_file.name in file_names:
-            try:
-                # Parse frontmatter
-                with open(boilerplate_file, 'r', encoding='utf-8') as f:
-                    post = frontmatter.load(f)
-                    boilerplate = Boilerplate(boilerplate_file, post.metadata, post.content)
-                    
-                    # If no name in frontmatter, use a meaningful name based on path
-                    if boilerplate.name == boilerplate_file.stem:
-                        # For nested paths like factory/runner-pool, use "Factory Runner Pool"
-                        relative_path = boilerplate_file.relative_to(library_path)
-                        path_parts = relative_path.parent.parts
-                        boilerplate.name = " ".join(part.replace("_", " ").replace("-", " ").title() for part in path_parts)
-                    
-                    boilerplates.append(boilerplate)
-                    
-            except Exception as e:
-                # If frontmatter parsing fails, create basic info
-                boilerplate = Boilerplate(boilerplate_file, {}, "")
-                relative_path = boilerplate_file.relative_to(library_path)
-                path_parts = relative_path.parent.parts
-                boilerplate.name = " ".join(part.replace("_", " ").replace("-", " ").title() for part in path_parts)
-                boilerplates.append(boilerplate)
-    
-    return sorted(boilerplates, key=lambda x: x.name)
-

+ 79 - 0
cli/core/library.py

@@ -0,0 +1,79 @@
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+  from .template import Template
+
+
+class Library:
+  """Represents a single library with a specific path."""
+  
+  def __init__(self, name: str, path: Path):
+    self.name = name
+    self.path = path
+
+  def find_by_id(self, module_name: str, files: list[str], template_id: str) -> "Template | None":
+    """Find a template by its ID in this library."""
+    for template in self.find(module_name, files, sorted=False):
+      if template.id == template_id:
+        return template
+    return None
+
+  def find(self, module_name: str, files: list[str], sorted: bool = False) -> list["Template"]:
+    """Find templates in this library for a specific module."""
+    from .template import Template  # Import here to avoid circular import
+    
+    templates = []
+    module_path = self.path / module_name
+    
+    if not module_path.exists():
+      return templates
+    
+    # Find all files matching the specified filenames
+    for filename in files:
+      for file_path in module_path.rglob(filename):
+        if file_path.is_file():
+          # Create Template object using the new class method
+          template = Template.from_file(file_path)
+          templates.append(template)
+
+    if sorted:
+      templates.sort(key=lambda t: t.id)
+
+    return templates
+
+
+class LibraryManager:
+  """Manager for multiple libraries."""
+  
+  def __init__(self):
+    self.libraries = []
+    # Initialize with the default library
+    script_dir = Path(__file__).parent.parent.parent  # Go up from cli/core/ to project root
+    default_library = Library("default", script_dir / "library")
+    self.libraries.append(default_library)
+  
+  def add_library(self, library: Library):
+    """Add a library to the collection."""
+    self.libraries.append(library)
+
+  def find(self, module_name: str, files: list[str], sorted: bool = False) -> list["Template"]:
+    """Find templates across all libraries for a specific module."""
+    all_templates = []
+    
+    for library in self.libraries:
+      templates = library.find(module_name, files, sorted=sorted)
+      all_templates.extend(templates)
+
+    if sorted:
+      all_templates.sort(key=lambda t: t.id)
+
+    return all_templates
+
+  def find_by_id(self, module_name: str, files: list[str], template_id: str) -> "Template | None":
+    """Find a template by its ID across all libraries."""
+    for library in self.libraries:
+      template = library.find_by_id(module_name, files, template_id)
+      if template:
+        return template
+    return None

+ 0 - 19
cli/core/logging.py

@@ -1,19 +0,0 @@
-"""
-Logging utilities for the Boilerplates CLI.
-"""
-
-import logging
-import sys
-
-
-def setup_logging(log_level: str = "WARNING") -> logging.Logger:
-    """Setup basic logging configuration."""
-    # Configure root logger
-    logging.basicConfig(
-        level=getattr(logging, log_level.upper()),
-        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
-        datefmt='%Y-%m-%d %H:%M:%S',
-        handlers=[logging.StreamHandler(sys.stderr)]
-    )
-
-    return logging.getLogger("boilerplates")

+ 0 - 44
cli/core/models.py

@@ -1,44 +0,0 @@
-"""
-Data models and structures for the CLI application.
-Contains classes that represent boilerplate information and other data structures.
-"""
-
-from pathlib import Path
-from typing import Any, Dict, List
-
-
-class Boilerplate:
-    """Data class for boilerplate information extracted from frontmatter."""
-    
-    def __init__(self, file_path: Path, frontmatter_data: Dict[str, Any], content: str):
-        self.file_path = file_path
-        self.content = content
-        
-        # Extract frontmatter fields with defaults
-        self.name = frontmatter_data.get('name', file_path.stem)
-        self.description = frontmatter_data.get('description', 'No description available')
-        self.author = frontmatter_data.get('author', '')
-        self.date = frontmatter_data.get('date', '')
-        self.version = frontmatter_data.get('version', '')
-        self.module = frontmatter_data.get('module', '')
-        self.tags = frontmatter_data.get('tags', [])
-        self.files = frontmatter_data.get('files', [])
-        
-        # Additional computed properties
-        self.relative_path = file_path.name
-        self.size = file_path.stat().st_size if file_path.exists() else 0
-    
-    def to_dict(self) -> Dict[str, Any]:
-        """Convert to dictionary for display."""
-        return {
-            'name': self.name,
-            'description': self.description,
-            'author': self.author,
-            'date': self.date,
-            'version': self.version,
-            'module': self.module,
-            'tags': self.tags,
-            'files': self.files,
-            'path': str(self.relative_path),
-            'size': f"{self.size:,} bytes"
-        }

+ 178 - 0
cli/core/module.py

@@ -0,0 +1,178 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Any, Dict, Optional, Tuple, List
+import logging
+from typer import Typer, Option, Argument
+
+from .library import LibraryManager
+from .prompt import PromptHandler
+from .template import Template
+from .variables import VariableGroup, VariableManager
+from .config import ConfigManager
+
+logger = logging.getLogger('boilerplates')
+
+
+class Module(ABC):
+  """
+  Base Module for all CLI Commands.
+  
+  This class now uses VariableManager for centralized variable management,
+  providing better organization and more advanced variable operations.
+  """
+
+  def __init__(self, name: str, description: str, files: list[str], vars: list[VariableGroup] = None):
+    self.name = name
+    self.description = description
+    self.files = files
+    
+    # Initialize ConfigManager and VariableManager with it
+    self.config_manager = ConfigManager()
+    self.variable_manager = VariableManager(vars if vars is not None else [], self.config_manager)
+
+    self.app = Typer()
+    self.libraries = LibraryManager()  # Initialize library manager
+    
+    # Validate that required attributes are set
+    if not self.name:
+      raise ValueError("Module name must be set")
+    if not self.description:
+      raise ValueError("Module description must be set")
+    if not isinstance(self.files, list) or len(self.files) == 0:
+      raise ValueError("Module files must be a non-empty list")
+    if not all(isinstance(var, VariableGroup) for var in (vars if vars is not None else [])):
+      raise ValueError("Module vars must be a list of VariableGroup instances")
+  
+  @property
+  def vars(self) -> List[VariableGroup]:
+    """Backward compatibility property for accessing variable groups."""
+    return self.variable_manager.variable_groups
+  
+  def get_variable_summary(self) -> Dict[str, Any]:
+    """Get a summary of all variables managed by this module."""
+    return self.variable_manager.get_summary()
+  
+  def add_variable_group(self, group: VariableGroup) -> None:
+    """Add a new variable group to this module."""
+    self.variable_manager.add_group(group)
+  
+  def has_variable(self, name: str) -> bool:
+    """Check if this module has a variable with the given name."""
+    return self.variable_manager.has_variable(name)
+
+  def list(self):
+    """List all templates in the module."""
+    logger.debug(f"Listing templates for module: {self.name}")
+    templates = self.libraries.find(self.name, self.files, sorted=True)
+    logger.debug(f"Found {len(templates)} templates")
+    
+    for template in templates:
+      print(f"{template.id} ({template.name}, {template.directory})")
+    return templates
+
+  def show(self, id: str = Argument(..., metavar="template", help="The template to show details for")):
+    """Show details about a template"""
+    logger.debug(f"Showing details for template: {id} in module: {self.name}")
+    
+    template = self.libraries.find_by_id(self.name, self.files, id)
+    if template:
+      logger.debug(f"Template found: {template.name}")
+      print(f"ID: {template.id}")
+      print(f"Name: {template.name}")
+      print(f"Directory: {template.directory}")
+      print(f"Content:\n{template.content}")
+    else:
+      logger.error(f"Template with ID '{id}' not found")
+      print(f"Template with ID '{id}' not found.")
+
+  def generate(self, id: str = Argument(..., metavar="template", help="The template to generate from"), out: Optional[Path] = Option(None, "--out", "-o", help="Output file to save the generated template")):
+    """Generate a new template with complex variable prompting logic"""
+    logger.info(f"Generating template '{id}' from module '{self.name}'")
+    
+    # Step 1: Find template by ID
+    logger.debug(f"Step 1: Finding template by ID: {id}")
+    template = self.libraries.find_by_id(self.name, self.files, id)
+    if not template:
+      logger.error(f"Template '{id}' not found")
+      print(f"Template '{id}' not found.")
+      return
+    
+    logger.debug(f"Template found: {template.name} with {len(template.vars)} variables")
+    
+    # Step 2: Validate if the variables in the template are valid ones
+    logger.debug(f"Step 2: Validating template variables: {template.vars}")
+    success, missing = self.variable_manager.validate_template_variables(template.vars)
+    if not success:
+      logger.error(f"Template '{id}' has invalid variables: {missing}")
+      print(f"Template '{id}' has invalid variables: {missing}")
+      return
+    
+    logger.debug("All template variables are valid")
+    
+    # Step 3: Disable variables not found in template
+    logger.debug(f"Step 3: Disabling variables not used by template")
+    self.variable_manager.disable_variables_not_in_template(template.vars)
+    logger.debug("Unused variables disabled")
+
+    # Step 4: Resolve variable defaults with priority (module -> template -> user config)
+    logger.debug(f"Step 4: Resolving variable defaults with priority")
+    resolved_defaults = self.variable_manager.resolve_variable_defaults(
+      self.name, 
+      template.vars, 
+      template.var_defaults
+    )
+    logger.debug(f"Resolved defaults: {resolved_defaults}")
+    
+    # Step 5: Match template vars with vars of the module (only enabled ones)
+    logger.debug(f"Step 5: Filtering variables for template")
+    filtered_vars = self.variable_manager.filter_variables_for_template(template.vars)
+    logger.debug(f"Filtered variables: {list(filtered_vars.keys())}")
+    
+    # Step 6: Execute complex group-based prompting logic
+    logger.debug(f"Step 6: Starting complex prompting logic")
+    try:
+      prompt = PromptHandler(filtered_vars, resolved_defaults)
+      final_variable_values = prompt()
+      logger.debug(f"Prompting completed with values: {final_variable_values}")
+    except KeyboardInterrupt:
+      logger.info("Template generation cancelled by user")
+      print("\n[red]Template generation cancelled.[/red]")
+      return
+    except Exception as e:
+      logger.error(f"Error during prompting: {e}")
+      print(f"Error during variable prompting: {e}")
+      return
+    
+    # Step 7: Generate template with final variable values
+    logger.debug(f"Step 7: Generating template with final values")
+    try:
+      generated_content = template.render(final_variable_values)
+      logger.debug("Template rendered successfully")
+    except Exception as e:
+      logger.error(f"Error rendering template: {e}")
+      print(f"Error rendering template: {e}")
+      return
+    
+    # Step 8: Output the generated content
+    logger.debug(f"Step 8: Outputting generated content")
+    if out:
+      try:
+        out.parent.mkdir(parents=True, exist_ok=True)
+        with open(out, 'w', encoding='utf-8') as f:
+          f.write(generated_content)
+        logger.info(f"Template generated and saved to {out}")
+        print(f"✅ Template generated and saved to {out}")
+      except Exception as e:
+        logger.error(f"Error saving to file {out}: {e}")
+        print(f"Error saving to file {out}: {e}")
+    else:
+      print("\n" + "="*60)
+      print("📄 Generated Template Content:")
+      print("="*60)
+      print(generated_content)
+  
+  def register(self, app: Typer):
+    self.app.command()(self.list)
+    self.app.command()(self.show)
+    self.app.command()(self.generate)
+    app.add_typer(self.app, name=self.name, help=self.description, no_args_is_help=True)

+ 374 - 579
cli/core/prompt.py

@@ -1,583 +1,378 @@
-from typing import Any, Dict, Optional, List, Set, Tuple
-from rich.prompt import Prompt, IntPrompt, Confirm
-import typer
-import sys
+from typing import Dict, Any, List, Optional, Union
+import logging
+from rich.console import Console
+from rich.prompt import Prompt, Confirm, IntPrompt, FloatPrompt
+from rich.table import Table
+from rich.panel import Panel
+from rich.text import Text
+from rich.markdown import Markdown
+from rich import box
+import re
+
+logger = logging.getLogger('boilerplates')
 
 
 class PromptHandler:
 class PromptHandler:
-    def __init__(self, declared_variables: Dict[str, Tuple[str, Dict[str, Any]]], variable_sets: Dict[str, Dict[str, Any]]):
-        self._declared = declared_variables
-        self.variable_sets = variable_sets
-
-    @staticmethod
-    def ask_bool(prompt_text: str, default: bool = False, description: Optional[str] = None) -> bool:
-        """Ask a yes/no question, render default in cyan when in a TTY, and
-        fall back to typer.confirm when not attached to a TTY.
-        """
-        if description and description.strip():
-            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
-            
-        if not (sys.stdin.isatty() and sys.stdout.isatty()):
-            return typer.confirm(prompt_text, default=default)
-
-        if default:
-            indicator = "[cyan]Y[/cyan]/n"
-        else:
-            indicator = "y/[cyan]N[/cyan]"
-
-        prompt_full = f"{prompt_text} [{indicator}]"
-        resp = Prompt.ask(prompt_full, default="", show_default=False)
-        if resp is None or str(resp).strip() == "":
-            return bool(default)
-        r = str(resp).strip().lower()
-        return r[0] in ("y", "1", "t")
-
-    @staticmethod
-    def ask_int(prompt_text: str, default: Optional[int] = None, description: Optional[str] = None) -> int:
-        if description and description.strip():
-            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
-        return IntPrompt.ask(prompt_text, default=default, show_default=True)
-
-    @staticmethod
-    def ask_str(prompt_text: str, default: Optional[str] = None, show_default: bool = True, description: Optional[str] = None) -> str:
-        if description and description.strip():
-            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
-        return Prompt.ask(prompt_text, default=default, show_default=show_default)
-
-    def collect_values(self, used_vars: Set[str], template_defaults: Dict[str, Any] = None, used_subscripts: Dict[str, Set[str]] = None) -> Dict[str, Any]:
-        """Interactively prompt for values for the variables that appear in the template.
-
-        For variables that were declared in `variable_sets` we use their metadata.
-        For unknown variables, we fall back to a generic prompt.
-        """
-        if template_defaults is None:
-            template_defaults = {}
-        values: Dict[str, Any] = {}
-
-        # Group used vars by their set.
-        # Iterate through declared variable_sets so the prompt order
-        # matches the order variables were defined in each set.
-        set_used_vars: Dict[str, List[str]] = {}
-        if isinstance(self.variable_sets, dict):
-            for set_name, set_def in self.variable_sets.items():
-                vars_map = set_def.get("variables") if isinstance(set_def, dict) and "variables" in set_def else set_def
-                if not isinstance(vars_map, dict):
-                    continue
-                for var_name in vars_map.keys():
-                    if var_name in used_vars and var_name in self._declared:
-                        if set_name not in set_used_vars:
-                            set_used_vars[set_name] = []
-                        set_used_vars[set_name].append(var_name)
-            
-            # If the set name is used as a variable, include the set for prompting
-                if set_name in used_vars and set_name not in set_used_vars:
-                    set_used_vars[set_name] = []
-
-        # Process each set in order: 'always' sets first, then others
-        if set_used_vars:
-            # Sort sets by priority: 'always' sets first, then by name
-            def sort_sets(item):
-                set_name, vars_list = item
-                set_def = self.variable_sets.get(set_name, {})
-                is_always = bool(set_def.get('always', False))
-                return (0 if is_always else 1, set_name)
-            
-            sorted_sets = sorted(set_used_vars.items(), key=sort_sets)
-            
-            for set_name, vars_in_set in sorted_sets:
-                # Retrieve per-set definition to pick up the custom prompt if provided
-                set_def = self.variable_sets.get(set_name, {})
-                set_prompt = set_def.get("prompt") if isinstance(set_def, dict) else None
-                typer.secho(f"\n{set_name.title()} Settings", fg=typer.colors.BLUE, bold=True)
-
-                def _print_defaults_for_set(vars_list):
-                    # Collect variables that have an effective default to print.
-                    printable = []
-                    for v in vars_list:
-                        meta_info = self._declared[v][1]
-                        display_name = meta_info.get("display_name", v.replace("_", " ").title())
-                        default = self._get_effective_default(v, template_defaults, values)
-                        # Skip variables that have no effective default (they must be provided by the user)
-                        if default is not None:
-                            printable.append((v, display_name, default))
-
-                    # If there are no defaults to show, don't print a header or blank line.
-                    if not printable:
-                        return
-
-                    # Print a blank line and a consistent header for defaults so it matches
-                    # the 'Required ... Variables' section formatting.
-                    typer.secho("\nDefault %s Variables" % set_name.title(), fg=typer.colors.GREEN, bold=True)
-
-                    for v, display_name, default in printable:
-                        # If variable is accessed with subscripts, show '(multiple)'
-                        if used_subscripts and v in used_subscripts and used_subscripts[v]:
-                            typer.secho(f"{display_name}: ", fg=typer.colors.BRIGHT_BLACK, nl=False)
-                            typer.secho("(multiple)", fg=typer.colors.CYAN)
-                        else:
-                            typer.secho(f"{display_name}: ", fg=typer.colors.BRIGHT_BLACK, nl=False)
-                            typer.secho(f"{default}", fg=typer.colors.CYAN)
-
-                # Decide whether this set is enabled and whether it should be
-                # customized. Support three modes in the set definition:
-                # - 'always': True => the set is enabled and we skip the enable
-                #    question (but may still ask to customize values)
-                # - 'prompt_enable': str => ask this question first to enable the
-                #    set (stores values[set_name] boolean)
-                # - 'prompt' (existing): when provided, ask whether to customize
-                #    the values. We ask 'prompt_enable' first when present, then
-                #    'prompt' to decide whether to customize.
-
-                set_always = bool(set_def.get('always', False))
-                set_prompt_enable = set_def.get('prompt_enable')
-                set_customize_prompt = set_prompt or f"Do you want to change the {set_name.title()} settings?"
-
-                if set_always:
-                    enable_set = True
-                elif set_prompt_enable:
-                    enable_set = self.ask_bool(set_prompt_enable, default=False)
-                else:
-                    # No explicit enable prompt: fall back to asking the customize prompt
-                    # and treat that as enabling when answered Yes.
-                    enable_set = None
-
-                # If we have a definitive enable decision, store it into values
-                if enable_set is not None:
-                    values[set_name] = enable_set
-                    # If a declared variable exists with the same name, don't prompt it
-                    if set_name in vars_in_set:
-                        vars_in_set = [v for v in vars_in_set if v != set_name]
-
-                # If we didn't ask prompt_enable, ask the customize prompt directly
-                if enable_set is None:
-                    # Check for undefined variables first, before asking if they want to enable
-                    undefined_vars_in_set = []
-                    for var in vars_in_set:
-                        effective_default = self._get_effective_default(var, template_defaults, values)
-                        if effective_default is None:
-                            undefined_vars_in_set.append(var)
-                    
-                    # If there are undefined variables, we must enable this set
-                    if undefined_vars_in_set:
-                        typer.secho(f"\n{set_name.title()} Settings", fg=typer.colors.BLUE, bold=True)
-                        typer.secho(f"Required {set_name.title()} Variables", fg=typer.colors.YELLOW, bold=True)
-                        for var in undefined_vars_in_set:
-                            meta_info = self._declared[var][1]
-                            display_name = meta_info.get("display_name", var.replace("_", " ").title())
-                            vtype = meta_info.get("type", "str")
-                            prompt = meta_info.get("prompt", f"Enter {display_name}")
-                            description = meta_info.get("description")
-                            
-                            # Handle subscripted variables
-                            subs = used_subscripts.get(var, set()) if used_subscripts else set()
-                            if subs:
-                                result_map = {}
-                                for k in subs:
-                                    # Required sub-key: enforce non-empty
-                                    kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
-                                    if not (sys.stdin.isatty() and sys.stdout.isatty()):
-                                        # Non-interactive: empty value is an error
-                                        if kval is None or str(kval).strip() == "":
-                                            typer.secho(f"[red]Required value for {display_name}['{k}'] cannot be blank in non-interactive mode.[/red]")
-                                            raise typer.Exit(code=1)
-                                    else:
-                                        # Interactive: re-prompt until non-empty
-                                        while kval is None or str(kval).strip() == "":
-                                            typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
-                                            kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
-                                    result_map[k] = self._guess_and_cast(kval)
-                                values[var] = result_map
-                                continue
-
-                            if vtype == "bool":
-                                val = self.ask_bool(prompt, default=False, description=description)
-                            elif vtype == "int":
-                                val = self.ask_int(prompt, default=None, description=description)
-                            else:
-                                val = self.ask_str(prompt, default=None, show_default=False, description=description)
-                            
-                            values[var] = self._cast_value_from_input(val, vtype)
-                        
-                        # Since we prompted for required variables, enable the set
-                        values[set_name] = True
-                        if set_name in vars_in_set:
-                            vars_in_set = [v for v in vars_in_set if v != set_name]
-                        
-                        # Print defaults and ask if they want to change others
-                        _print_defaults_for_set(vars_in_set)
-                        change_set = self.ask_bool(set_customize_prompt, default=False)
-                        if not change_set:
-                            # Use defaults for remaining variables
-                            for var in vars_in_set:
-                                if var not in values:  # Don't override variables we already prompted for
-                                    meta_info = self._declared[var][1]
-                                    default = self._get_effective_default(var, template_defaults, values)
-                                    values[var] = default
-                            continue
-                    else:
-                        # No undefined variables, ask the customize prompt as normal
-                        change_set = self.ask_bool(set_customize_prompt, default=False)
-                        values[set_name] = change_set
-                        if set_name in vars_in_set:
-                            vars_in_set = [v for v in vars_in_set if v != set_name]
-                        if not change_set:
-                            # Use defaults for this set
-                            for var in vars_in_set:
-                                if var not in values:  # Don't override variables that might have been set
-                                    meta_info = self._declared[var][1]
-                                    default = self._get_effective_default(var, template_defaults, values)
-                                    values[var] = default
-                            continue
-
-                # If we had an enable_set (True/False) and it is False, skip customizing
-                if enable_set is not None and not enable_set:
-                    for var in vars_in_set:
-                        if var not in values:  # Don't override variables that might have been set
-                            meta_info = self._declared[var][1]
-                            default = self._get_effective_default(var, template_defaults, values)
-                            values[var] = default
-                    continue
-
-                # At this point the set is enabled. Check for undefined variables first.
-                undefined_vars_in_set = []
-                for var in vars_in_set:
-                    effective_default = self._get_effective_default(var, template_defaults, values)
-                    if effective_default is None:
-                        undefined_vars_in_set.append(var)
-                
-                # Prompt for undefined variables in this set
-                if undefined_vars_in_set:
-                    typer.secho(f"\nRequired {set_name.title()} Variables", fg=typer.colors.YELLOW, bold=True)
-                    for var in undefined_vars_in_set:
-                        meta_info = self._declared[var][1]
-                        display_name = meta_info.get("display_name", var.replace("_", " ").title())
-                        vtype = meta_info.get("type", "str")
-                        prompt = meta_info.get("prompt", f"Enter {display_name}")
-                        description = meta_info.get("description")
-                        
-                        # Handle subscripted variables
-                        subs = used_subscripts.get(var, set()) if used_subscripts else set()
-                        if subs:
-                            result_map = {}
-                            for k in subs:
-                                # Required sub-key: enforce non-empty
-                                kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
-                                if not (sys.stdin.isatty() and sys.stdout.isatty()):
-                                    if kval is None or str(kval).strip() == "":
-                                        typer.secho(f"[red]Required value for {display_name}['{k}'] cannot be blank in non-interactive mode.[/red]")
-                                        raise typer.Exit(code=1)
-                                else:
-                                    while kval is None or str(kval).strip() == "":
-                                        typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
-                                        kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
-                                result_map[k] = self._guess_and_cast(kval)
-                            values[var] = result_map
-                            continue
-
-                        if vtype == "bool":
-                            val = self.ask_bool(prompt, default=False, description=description)
-                        elif vtype == "int":
-                            val = self.ask_int(prompt, default=None, description=description)
-                        else:
-                            val = self.ask_str(prompt, default=None, show_default=False, description=description)
-                            # Enforce non-empty for required scalar variables
-                            if not (sys.stdin.isatty() and sys.stdout.isatty()):
-                                if val is None or str(val).strip() == "":
-                                    typer.secho(f"[red]Required value for {display_name} cannot be blank in non-interactive mode.[/red]")
-                                    raise typer.Exit(code=1)
-                            else:
-                                while val is None or str(val).strip() == "":
-                                    typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
-                                    val = self.ask_str(prompt, default=None, show_default=False, description=description)
-
-                        values[var] = self._cast_value_from_input(val, vtype)
-
-                # Print defaults now (only after enabling and prompting for required vars)
-                # so the user sees current values before customizing.
-                _print_defaults_for_set(vars_in_set)
-
-                # If we have asked prompt_enable earlier (and the set is enabled),
-                # now ask whether to customize. For 'always' sets we still ask the
-                # customize prompt.
-                if set_prompt_enable or set_always:
-                    change_set = self.ask_bool(set_customize_prompt, default=False)
-                    if not change_set:
-                        for var in vars_in_set:
-                            if var not in values:  # Don't override variables that might have been set
-                                meta_info = self._declared[var][1]
-                                default = self._get_effective_default(var, template_defaults, values)
-                                values[var] = default
-                        continue
-
-                # Prompt for each variable in the set
-                for var in vars_in_set:
-                    # Skip variables that have already been prompted for
-                    if var in values:
-                        continue
-                        
-                    meta_info = self._declared[var][1]
-                    display_name = meta_info.get("display_name", var.replace("_", " ").title())
-                    vtype = meta_info.get("type", "str")
-                    prompt = meta_info.get("prompt", f"Enter {display_name}")
-                    description = meta_info.get("description")
-                    default = self._get_effective_default(var, template_defaults, values)
-
-                    # Build prompt text and rely on show_default to display the default value
-                    prompt_text = f"{prompt}"
-
-                    # If variable is accessed with subscripts in the template, always prompt for each key and store as dict
-                    subs = used_subscripts.get(var, set()) if used_subscripts else set()
-                    if subs:
-                        # Print all default values for subscripted keys before prompting
-                        for k in subs:
-                            key_default = None
-                            if isinstance(default, dict):
-                                key_default = default.get(k)
-                            elif default is not None:
-                                key_default = default
-                            typer.secho(f"{display_name}['{k}']: ", fg=typer.colors.BRIGHT_BLACK, nl=False)
-                            typer.secho(f"{key_default}", fg=typer.colors.CYAN)
-                        result_map = {}
-                        for k in subs:
-                            kval = Prompt.ask(f"Value for {display_name}['{k}']:", default=str(default.get(k)) if isinstance(default, dict) and default.get(k) is not None else None, show_default=True)
-                            result_map[k] = self._guess_and_cast(kval)
-                        values[var] = result_map
-                        continue
-
-                    if vtype == "bool":
-                        # Normalize default to bool
-                        bool_default = False
-                        if isinstance(default, bool):
-                            bool_default = default
-                        elif isinstance(default, str):
-                            bool_default = default.lower() in ("true", "1", "yes")
-                        elif isinstance(default, int):
-                            bool_default = default != 0
-                        val = self.ask_bool(prompt_text, default=bool_default, description=description)
-                    elif vtype == "int":
-                        # Use IntPrompt to validate and parse integers; show default if present
-                        int_default = None
-                        if isinstance(default, int):
-                            int_default = default
-                        elif isinstance(default, str) and default.isdigit():
-                            int_default = int(default)
-                        val = self.ask_int(prompt_text, default=int_default, description=description)
-                    else:
-                        # Use Prompt for string input and show default
-                        str_default = str(default) if default is not None else None
-                        val = self.ask_str(prompt_text, default=str_default, show_default=True, description=description)
-
-                    # Handle collection types: arrays and maps
-                    if vtype in ("array", "list"):
-                        values[var] = self.prompt_array(var, meta_info, default)
-                        continue
-
-                    if vtype in ("map", "dict"):
-                        # If the template indexes this variable with specific keys, prompt per-key
-                        subs = used_subscripts.get(var, set()) if used_subscripts else set()
-                        if subs:
-                            # Prompt for each accessed key; allow single scalar default to apply to all
-                            result_map = {}
-                            # If default is a scalar, ask whether to expand it to accessed keys
-                            if not isinstance(default, dict) and default is not None:
-                                use_single = self.ask_bool(f"Use single value {default} for all {display_name} keys?", default=True)
-                                if use_single:
-                                    for k in subs:
-                                        result_map[k] = default
-                                    values[var] = result_map
-                                    continue
-                            # Otherwise prompt per key or use metadata keys when present
-                            keys_meta = meta_info.get("keys")
-                            for k in subs:
-                                if isinstance(keys_meta, dict) and k in keys_meta:
-                                    # reuse metadata prompt
-                                    kmeta = keys_meta[k]
-                                    result_map[k] = self.prompt_scalar(k, kmeta, kmeta.get("default"))
-                                else:
-                                    # generic prompt
-                                    kval = self.ask_str(f"Value for {display_name}['{k}']:")
-                                    result_map[k] = self._guess_and_cast(kval)
-                            values[var] = result_map
-                            continue
-
-                        # Fallback to full map prompting
-                        values[var] = self.prompt_map(var, meta_info, default)
-                        continue
-
-                    # store scalar/canonicalized value
-                    values[var] = self._cast_value_from_input(val, vtype)
-
-        # Handle variables that belong to sets but weren't processed
-        all_set_vars = set()
-        if isinstance(self.variable_sets, dict):
-            for set_name, set_def in self.variable_sets.items():
-                vars_map = set_def.get("variables") if isinstance(set_def, dict) and "variables" in set_def else set_def
-                if isinstance(vars_map, dict):
-                    all_set_vars.update(vars_map.keys())
+  """Advanced prompt handler with Rich UI for complex variable group logic."""
+
+  def __init__(self, variable_groups: Dict[str, Any], resolved_defaults: Dict[str, Any] = None):
+    """Initialize the prompt handler.
+    
+    Args:
+        variable_groups: Dictionary of variable groups from VariableManager
+        resolved_defaults: Pre-resolved default values with priority handling
+    """
+    self.variable_groups = variable_groups
+    self.resolved_defaults = resolved_defaults or {}
+    self.console = Console()
+    self.final_values = {}
+    
+  def __call__(self) -> Dict[str, Any]:
+    """Execute the complex prompting logic and return final variable values."""
+    logger.debug(f"Starting advanced prompt handler with {len(self.variable_groups)} variable groups")
+    
+    self._show_welcome_message()
+    
+    # Process each variable group with the complex logic
+    for group_name, group_data in self.variable_groups.items():
+      self._process_variable_group(group_name, group_data)
+    
+    self._show_summary()
+    return self.final_values
+  
+  def _show_welcome_message(self):
+    """Display a welcome message for the template generation."""
+    welcome_text = Text("🚀 Template Generation", style="bold blue")
+    subtitle = Text("Configure variables for your template", style="dim")
+    
+    panel = Panel(
+      f"{welcome_text}\n{subtitle}",
+      box=box.ROUNDED,
+      padding=(1, 2)
+    )
+    self.console.print(panel)
+    self.console.print()
+  
+  def _process_variable_group(self, group_name: str, group_data: Dict[str, Any]):
+    """Process a single variable group with complex prompting logic.
+    
+    Logic flow:
+    1. Check if group has variables with no default values → always prompt
+    2. If group is not enabled → ask user if they want to enable it
+    3. If group is enabled → prompt for variables without values
+    4. Ask if user wants to change existing variable values
+    """
+    logger.debug(f"Processing variable group: {group_name}")
+    
+    variables = group_data.get('vars', {})
+    if not variables:
+      logger.debug(f"Group {group_name} has no variables, skipping")
+      return
+      
+    # Show group header
+    self._show_group_header(group_name, group_data.get('description', ''))
+    
+    # Step 1: Check for variables with no default values (always prompt)
+    vars_without_defaults = self._get_variables_without_defaults(variables)
+    
+    # Step 2: Determine if group should be enabled
+    group_enabled = self._determine_group_enabled_status(group_name, variables, vars_without_defaults)
+    
+    if not group_enabled:
+      logger.debug(f"Group {group_name} disabled by user")
+      return
+      
+    # Step 3: Prompt for required variables (those without defaults)
+    if vars_without_defaults:
+      self.console.print(f"[bold red]Required variables for {group_name}:[/bold red]")
+      for var_name in vars_without_defaults:
+        var_data = variables[var_name]
+        value = self._prompt_for_variable(var_name, var_data, required=True)
+        self.final_values[var_name] = value
+    
+    # Step 4: Handle variables with defaults - ask if user wants to change them
+    vars_with_defaults = self._get_variables_with_defaults(variables)
+    
+    if vars_with_defaults:
+      self._handle_variables_with_defaults(group_name, vars_with_defaults, variables)
+    
+    self.console.print()  # Add spacing between groups
+  
+  def _show_group_header(self, group_name: str, description: str):
+    """Display a header for the variable group."""
+    header = f"[bold cyan]📦 {group_name.title()} Variables[/bold cyan]"
+    if description:
+      header += f"\n[dim]{description}[/dim]"
+    
+    self.console.print(Panel(header, box=box.SIMPLE, padding=(0, 1)))
+  
+  def _get_variables_without_defaults(self, variables: Dict[str, Any]) -> List[str]:
+    """Get list of variable names that have no default values."""
+    return [
+      var_name for var_name, var_data in variables.items()
+      if var_name not in self.resolved_defaults or self.resolved_defaults[var_name] is None
+    ]
+  
+  def _get_variables_with_defaults(self, variables: Dict[str, Any]) -> List[str]:
+    """Get list of variable names that have default values."""
+    return [
+      var_name for var_name, var_data in variables.items()
+      if var_name in self.resolved_defaults and self.resolved_defaults[var_name] is not None
+    ]
+  
+  def _determine_group_enabled_status(self, group_name: str, variables: Dict[str, Any], vars_without_defaults: List[str]) -> bool:
+    """Determine if a variable group should be enabled based on complex logic."""
+    
+    # If there are required variables (no defaults), group must be enabled
+    if vars_without_defaults:
+      logger.debug(f"Group {group_name} has required variables, enabling automatically")
+      return True
+    
+    # Check if group is enabled by default values or should ask user
+    vars_with_defaults = self._get_variables_with_defaults(variables)
+    if not vars_with_defaults:
+      logger.debug(f"Group {group_name} has no variables with defaults, skipping")
+      return False
+    
+    # Show preview of what this group would configure
+    self._show_group_preview(group_name, vars_with_defaults)
+    
+    # Ask user if they want to enable this optional group
+    try:
+      return Confirm.ask(
+        f"[yellow]Do you want to configure {group_name} variables?[/yellow]",
+        default=False
+      )
+    except (EOFError, KeyboardInterrupt):
+      logger.debug(f"User interrupted prompt for group {group_name}, defaulting to disabled")
+      return False
+  
+  def _show_group_preview(self, group_name: str, vars_with_defaults: List[str]):
+    """Show a preview of variables that would be configured in this group."""
+    if not vars_with_defaults:
+      return
+      
+    table = Table(title=f"Variables in {group_name}", box=box.SIMPLE)
+    table.add_column("Variable", style="cyan")
+    table.add_column("Default Value", style="green")
+    
+    for var_name in vars_with_defaults:
+      default_value = self.resolved_defaults.get(var_name, "None")
+      table.add_row(var_name, str(default_value))
+    
+    self.console.print(table)
+  
+  def _handle_variables_with_defaults(self, group_name: str, vars_with_defaults: List[str], variables: Dict[str, Any]):
+    """Handle variables that have default values."""
+    
+    # First, set all default values
+    for var_name in vars_with_defaults:
+      default_value = self.resolved_defaults.get(var_name)
+      self.final_values[var_name] = default_value
+    
+    # Ask if user wants to customize any of these values
+    try:
+      want_to_customize = Confirm.ask(f"[yellow]Do you want to customize any {group_name} variables?[/yellow]", default=False)
+    except (EOFError, KeyboardInterrupt):
+      logger.debug(f"User interrupted customization prompt for group {group_name}, using defaults")
+      return
+    
+    if want_to_customize:
+      for var_name in vars_with_defaults:
+        var_data = variables[var_name]
+        current_value = self.final_values[var_name]
         
         
-        # Handle unknown variables (variables that don't belong to any set)
-        # If a variable was already set (for example by the set-level prompt mapping
-        # into `values[set_name]`), don't prompt for it again.
-        for var in used_vars:
-            if var not in self._declared and var not in values and var not in all_set_vars:
-                prompt_text = f"Value for '{var}':"
-                val = Prompt.ask(prompt_text, default="", show_default=False)
-                values[var] = self._guess_and_cast(val)
-            elif var in self._declared and var not in values:
-                # This is a declared variable that wasn't processed in its set
-                # This shouldn't happen with proper logic, but let's handle it
-                meta_info = self._declared[var][1]
-                default = self._get_effective_default(var, template_defaults, values)
-                if default is not None:
-                    values[var] = default
-                else:
-                    # No default available, prompt generically
-                    display_name = meta_info.get("display_name", var.replace("_", " ").title())
-                    vtype = meta_info.get("type", "str")
-                    prompt = meta_info.get("prompt", f"Enter {display_name}")
-                    description = meta_info.get("description")
-                    
-                    if vtype == "bool":
-                        val = self.ask_bool(prompt, default=False, description=description)
-                    elif vtype == "int":
-                        val = self.ask_int(prompt, default=None, description=description)
-                    else:
-                        val = self.ask_str(prompt, default=None, show_default=False, description=description)
-                    
-                    values[var] = self._cast_value_from_input(val, vtype)
-
-        return values
-
-    def _get_effective_default(self, var_name: str, template_defaults: Dict[str, Any], current_values: Dict[str, Any]):
-        # Prefer template-provided default, else declared metadata default
-        meta_info = self._declared.get(var_name, ({}, {}))[1] if var_name in self._declared else {}
-        candidate = None
-        if template_defaults and var_name in template_defaults:
-            candidate = template_defaults[var_name]
+        self.console.print(f"\n[dim]Current value for [bold]{var_name}[/bold]: {current_value}[/dim]")
+        
+        try:
+          change_variable = Confirm.ask(f"Change [bold]{var_name}[/bold]?", default=False)
+        except (EOFError, KeyboardInterrupt):
+          logger.debug(f"User interrupted change prompt for variable {var_name}, keeping current value")
+          continue
+          
+        if change_variable:
+          new_value = self._prompt_for_variable(var_name, var_data, required=False, current_value=current_value)
+          self.final_values[var_name] = new_value
+  
+  def _prompt_for_variable(self, var_name: str, var_data: Dict[str, Any], required: bool = False, current_value: Any = None) -> Any:
+    """Prompt user for a single variable with type validation."""
+    
+    var_type = var_data.get('type', 'string')
+    description = var_data.get('description', '')
+    options = var_data.get('options', [])
+    
+    # Build prompt message
+    prompt_parts = [f"[bold]{var_name}[/bold]"]
+    if description:
+      prompt_parts.append(f"({description})")
+    
+    prompt_message = " ".join(prompt_parts)
+    
+    # Add type information if not string
+    if var_type != 'string':
+      prompt_message += f" [dim]({var_type})[/dim]"
+    
+    # Handle different variable types
+    try:
+      if var_type == 'boolean':
+        return self._prompt_boolean(prompt_message, current_value)
+      elif var_type == 'integer':
+        return self._prompt_integer(prompt_message, current_value)
+      elif var_type == 'float':
+        return self._prompt_float(prompt_message, current_value)
+      elif var_type == 'choice' and options:
+        return self._prompt_choice(prompt_message, options, current_value)
+      elif var_type == 'list':
+        return self._prompt_list(prompt_message, current_value)
+      else:  # string or unknown type
+        return self._prompt_string(prompt_message, current_value, required)
+        
+    except KeyboardInterrupt:
+      self.console.print("\n[red]Operation cancelled by user[/red]")
+      raise
+    except Exception as e:
+      logger.error(f"Error prompting for variable {var_name}: {e}")
+      self.console.print(f"[red]Error getting input for {var_name}. Using default string prompt.[/red]")
+      return self._prompt_string(prompt_message, current_value, required)
+  
+  def _prompt_string(self, prompt_message: str, current_value: Any = None, required: bool = False) -> str:
+    """Prompt for string input with validation."""
+    default_val = str(current_value) if current_value is not None else None
+    
+    while True:
+      try:
+        value = Prompt.ask(prompt_message, default=default_val)
+        
+        if required and not value.strip():
+          self.console.print("[red]This field is required and cannot be empty[/red]")
+          continue
+          
+        return value.strip()
+      except (EOFError, KeyboardInterrupt):
+        if required:
+          self.console.print(f"\n[red]This field is required. Using empty string.[/red]")
+          return ""
         else:
         else:
-            candidate = meta_info.get("default") if isinstance(meta_info, dict) else None
-
-        # If candidate names another variable and that variable has already
-        # been provided by the user, use that value.
-        if isinstance(candidate, str) and candidate in current_values:
-            return current_values[candidate]
-
-        # Otherwise, try to resolve identifier references to declared defaults
-        if isinstance(candidate, str) and candidate in self._declared:
-            decl_def = self._declared[candidate][1].get("default")
-            if decl_def is not None:
-                return decl_def
-
-        return candidate
-
-    def prompt_scalar(self, var_name: str, meta_info: Dict[str, Any], default_val: Any) -> Any:
-        display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
-        vtype = meta_info.get("type", "str")
-        prompt = meta_info.get("prompt", f"Enter {display_name}")
-        description = meta_info.get("description")
-        if vtype == "bool":
-            bool_default = False
-            if isinstance(default_val, bool):
-                bool_default = default_val
-            elif isinstance(default_val, str):
-                bool_default = default_val.lower() in ("true", "1", "yes")
-            elif isinstance(default_val, int):
-                bool_default = default_val != 0
-            return self.ask_bool(prompt, default=bool_default, description=description)
-        if vtype == "int":
-            int_default = None
-            if isinstance(default_val, int):
-                int_default = default_val
-            elif isinstance(default_val, str) and default_val.isdigit():
-                int_default = int(default_val)
-            return self.ask_int(prompt, default=int_default, description=description)
-        str_default = str(default_val) if default_val is not None else None
-        return self.ask_str(prompt, default=str_default, show_default=True, description=description)
-
-    def prompt_array(self, var_name: str, meta_info: Dict[str, Any], default_val: Any) -> Any:
-        display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
-        item_type = meta_info.get("item_type", "str")
-        item_prompt = meta_info.get("item_prompt", f"Enter {display_name} item")
-        default_list = default_val if isinstance(default_val, list) else []
-        default_count = len(default_list) if default_list else 0
-        count = self.ask_int(f"How many entries for {display_name}?", default=default_count or 1)
-        arr = []
-        for i in range(count):
-            item_default = default_list[i] if i < len(default_list) else None
-            item_prompt_text = f"{item_prompt} [{i}]"
-            if item_type == "int":
-                int_d = item_default if isinstance(item_default, int) else (int(item_default) if isinstance(item_default, str) and str(item_default).isdigit() else None)
-                item_val = self.ask_int(item_prompt_text, default=int_d)
-            elif item_type == "bool":
-                item_bool_d = self._cast_str_to_bool(item_default)
-                item_val = self.ask_bool(item_prompt_text, default=item_bool_d)
-            else:
-                item_str_d = str(item_default) if item_default is not None else None
-                item_val = self.ask_str(item_prompt_text, default=item_str_d, show_default=True)
-            arr.append(self._cast_value_from_input(item_val, item_type))
-        return arr
-
-    def prompt_map(self, var_name: str, meta_info: Dict[str, Any], default_val: Any) -> Any:
-        display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
-        keys_meta = meta_info.get("keys")
-        result_map = {}
-        if isinstance(keys_meta, dict):
-            for key_name, kmeta in keys_meta.items():
-                kdisplay = kmeta.get("display_name", f"{display_name}['{key_name}']")
-                ktype = kmeta.get("type", "str")
-                kdefault = kmeta.get("default") if "default" in kmeta else (default_val.get(key_name) if isinstance(default_val, dict) and key_name in default_val else None)
-                kprompt = kmeta.get("prompt", f"Enter value for {kdisplay}")
-                if ktype == "int":
-                    kd = kdefault if isinstance(kdefault, int) else (int(kdefault) if isinstance(kdefault, str) and str(kdefault).isdigit() else None)
-                    kval = self.ask_int(kprompt, default=kd)
-                elif ktype == "bool":
-                    kval = self.ask_bool(kprompt, default=self._cast_str_to_bool(kdefault))
-                else:
-                    kval = self.ask_str(kprompt, default=str(kdefault) if kdefault is not None else None, show_default=True)
-                result_map[key_name] = self._cast_value_from_input(kval, ktype)
-            return result_map
-        if isinstance(default_val, dict) and len(default_val) > 0:
-            for key_name, kdefault in default_val.items():
-                kprompt = f"Enter value for {display_name}['{key_name}']"
-                kval = self.ask_str(kprompt, default=str(kdefault) if kdefault is not None else None, show_default=True)
-                result_map[key_name] = self._guess_and_cast(kval)
-            return result_map
-        count = self.ask_int(f"How many named entries for {display_name}?", default=1)
-        for i in range(count):
-            key_name = self.ask_str(f"Key name [{i}]", default=None, show_default=False)
-            kval = self.ask_str(f"Value for {display_name}['{key_name}']:", default=None, show_default=False)
-            result_map[key_name] = self._guess_and_cast(kval)
-        return result_map
-
-    @staticmethod
-    def _cast_str_to_bool(s):
-        if isinstance(s, bool):
-            return s
-        if isinstance(s, int):
-            return s != 0
-        if isinstance(s, str):
-            return s.lower() in ("true", "1", "yes")
-        return False
-
-    @staticmethod
-    def _cast_value_from_input(raw, vtype):
-        if vtype == "int":
-            try:
-                return int(raw)
-            except Exception:
-                return raw
-        if vtype == "bool":
-            bool_val = PromptHandler._cast_str_to_bool(raw)
-            return "true" if bool_val else "false"
-        return raw
-
-    @staticmethod
-    def _guess_and_cast(raw):
-        s = raw if not isinstance(raw, str) else raw.strip()
-        if s == "":
-            return raw
-        if isinstance(s, str) and s.isdigit():
-            return PromptHandler._cast_value_from_input(s, "int")
-        if isinstance(s, str) and s.lower() in ("true", "false", "yes", "no", "1", "0", "t", "f"):
-            return PromptHandler._cast_value_from_input(s, "bool")
-        return PromptHandler._cast_value_from_input(s, "str")
-
+          return default_val or ""
+  
+  def _prompt_boolean(self, prompt_message: str, current_value: Any = None) -> bool:
+    """Prompt for boolean input."""
+    default_val = bool(current_value) if current_value is not None else None
+    try:
+      return Confirm.ask(prompt_message, default=default_val)
+    except (EOFError, KeyboardInterrupt):
+      return default_val if default_val is not None else False
+  
+  def _prompt_integer(self, prompt_message: str, current_value: Any = None) -> int:
+    """Prompt for integer input with validation."""
+    default_val = int(current_value) if current_value is not None else None
+    
+    while True:
+      try:
+        return IntPrompt.ask(prompt_message, default=default_val)
+      except ValueError:
+        self.console.print("[red]Please enter a valid integer[/red]")
+      except (EOFError, KeyboardInterrupt):
+        return default_val if default_val is not None else 0
+  
+  def _prompt_float(self, prompt_message: str, current_value: Any = None) -> float:
+    """Prompt for float input with validation."""
+    default_val = float(current_value) if current_value is not None else None
+    
+    while True:
+      try:
+        return FloatPrompt.ask(prompt_message, default=default_val)
+      except ValueError:
+        self.console.print("[red]Please enter a valid number[/red]")
+      except (EOFError, KeyboardInterrupt):
+        return default_val if default_val is not None else 0.0
+  
+  def _prompt_choice(self, prompt_message: str, options: List[Any], current_value: Any = None) -> Any:
+    """Prompt for choice from a list of options."""
+    
+    # Show available options
+    self.console.print(f"\n[dim]Available options:[/dim]")
+    for i, option in enumerate(options, 1):
+      marker = "→" if option == current_value else " "
+      self.console.print(f"  {marker} {i}. {option}")
+    
+    while True:
+      try:
+        choice = Prompt.ask(f"{prompt_message} (1-{len(options)})")
+        
+        try:
+          choice_idx = int(choice) - 1
+          if 0 <= choice_idx < len(options):
+            return options[choice_idx]
+          else:
+            self.console.print(f"[red]Please enter a number between 1 and {len(options)}[/red]")
+        except ValueError:
+          # Try to match by string value
+          matching_options = [opt for opt in options if str(opt).lower() == choice.lower()]
+          if matching_options:
+            return matching_options[0]
+          self.console.print(f"[red]Please enter a valid option number (1-{len(options)}) or exact option name[/red]")
+      except (EOFError, KeyboardInterrupt):
+        return current_value if current_value is not None else options[0] if options else None
+  
+  def _prompt_list(self, prompt_message: str, current_value: Any = None) -> List[str]:
+    """Prompt for list input (comma-separated values)."""
+    
+    current_str = ""
+    if current_value and isinstance(current_value, list):
+      current_str = ", ".join(str(item) for item in current_value)
+    elif current_value:
+      current_str = str(current_value)
+    
+    self.console.print(f"[dim]Enter values separated by commas[/dim]")
+    
+    try:
+      value = Prompt.ask(prompt_message, default=current_str)
+      
+      if not value.strip():
+        return []
+      
+      # Split by comma and clean up
+      items = [item.strip() for item in value.split(',') if item.strip()]
+      return items
+    except (EOFError, KeyboardInterrupt):
+      if current_value and isinstance(current_value, list):
+        return current_value
+      elif current_value:
+        return [str(current_value)]
+      else:
+        return []
+  
+  def _show_summary(self):
+    """Display a summary of all configured variables."""
+    if not self.final_values:
+      self.console.print("[yellow]No variables were configured.[/yellow]")
+      return
+    
+    self.console.print("\n" + "="*50)
+    self.console.print("[bold green]📋 Configuration Summary[/bold green]")
+    self.console.print("="*50)
+    
+    table = Table(box=box.SIMPLE_HEAVY)
+    table.add_column("Variable", style="cyan", min_width=20)
+    table.add_column("Value", style="green")
+    table.add_column("Type", style="dim")
+    
+    for var_name, value in self.final_values.items():
+      var_type = type(value).__name__
+      # Format value for display
+      if isinstance(value, list):
+        display_value = ", ".join(str(item) for item in value)
+      else:
+        display_value = str(value)
+      
+      table.add_row(var_name, display_value, var_type)
+    
+    self.console.print(table)
+    self.console.print()
+    
+    try:
+      if not Confirm.ask("[bold]Proceed with template generation?[/bold]", default=True):
+        raise KeyboardInterrupt("Template generation cancelled by user")
+    except (EOFError, KeyboardInterrupt):
+      # If user cancels, still proceed with defaults
+      self.console.print("[yellow]Using current configuration to proceed.[/yellow]")

+ 64 - 0
cli/core/registry.py

@@ -0,0 +1,64 @@
+"""Module registry system with decorator-based registration."""
+from typing import Type, Dict, List
+from functools import wraps
+
+
+class Registry:
+  """Registry using decorators for explicit module registration."""
+  
+  def __init__(self):
+    self._modules: Dict[str, Type] = {}
+    self._configs: Dict[str, Dict] = {}
+
+  def register_module(self, name: str = None, description: str = None, files: List[str] = None, enabled: bool = True, **kwargs):
+    """Decorator to register a module class with automatic configuration."""
+    def decorator(cls: Type):
+      module_name = name or cls.__name__.replace("Module", "").lower()
+      config = {
+        'name': module_name,
+        'description': description or f"Manage {module_name} configurations",
+        'files': files or [],
+        'enabled': enabled,
+        **kwargs
+      }
+      
+      original_init = cls.__init__
+      
+      @wraps(original_init)
+      def enhanced_init(self, *args, **init_kwargs):
+        if not hasattr(self, '_configured'):
+          for attr, value in config.items():
+            if not hasattr(self, attr) or not getattr(self, attr):
+              setattr(self, attr, value)
+          self._configured = True
+        original_init(self, *args, **init_kwargs)
+      
+      cls.__init__ = enhanced_init
+      cls._module_name = module_name
+      cls._module_config = config
+      
+      if enabled:
+        self._modules[module_name] = cls
+        self._configs[module_name] = config
+      
+      return cls
+    return decorator
+  
+  def get_module_configs(self) -> Dict[str, Dict]:
+    """Get all module configurations."""
+    return self._configs.copy()
+  
+  def create_instances(self) -> List:
+    """Create instances of all registered modules, sorted alphabetically."""
+    instances = []
+    for name, cls in sorted(self._modules.items()):
+      try:
+        instances.append(cls())
+      except Exception as e:
+        print(f"Warning: Could not instantiate {cls.__name__}: {e}")
+    return instances
+
+
+# Global registry instance
+registry = Registry()
+register_module = registry.register_module

+ 0 - 83
cli/core/render.py

@@ -1,83 +0,0 @@
-"""
-Core rendering functionality for handling template output and display.
-Provides consistent rendering and output handling across different module types.
-"""
-import logging
-from pathlib import Path
-from typing import Optional, Union
-
-from rich.console import Console
-from rich.syntax import Syntax
-
-logger = logging.getLogger(__name__)
-
-class RenderOutput:
-    """Handles the output of rendered templates."""
-    
-    def __init__(self, console: Optional[Console] = None):
-        self.console = console or Console()
-        
-    def write_to_file(self, content: str, output_path: Path) -> None:
-        """
-        Write rendered content to a file.
-        
-        Args:
-            content: Content to write
-            output_path: Path to write the content to
-            
-        Raises:
-            Exception: If writing fails
-        """
-        try:
-            # Ensure parent directory exists
-            output_parent = output_path.parent
-            if not output_parent.exists():
-                output_parent.mkdir(parents=True, exist_ok=True)
-                
-            output_path.write_text(content, encoding="utf-8")
-            self.console.print(f"[green]Rendered content written to {output_path}[/green]")
-        except Exception as e:
-            raise Exception(f"Failed to write output to {output_path}: {e}")
-            
-    def print_to_console(self, content: str, syntax: str = "yaml",
-                        template_name: Optional[str] = None) -> None:
-        """
-        Print rendered content to the console with syntax highlighting.
-        
-        Args:
-            content: Content to print
-            syntax: Syntax highlighting to use (default: yaml)
-            template_name: Optional template name to show in header
-        """
-        if template_name:
-            self.console.print(f"\n\nGenerated Content for [bold cyan]{template_name}[/bold cyan]\n")
-            
-        syntax_output = Syntax(
-            content,
-            syntax,
-            theme="monokai",
-            line_numbers=False,
-            word_wrap=True
-        )
-        self.console.print(syntax_output)
-        
-    def output_rendered_content(self, content: str, output_target: Optional[Union[str, Path]],
-                              syntax: str = "yaml", template_name: Optional[str] = None) -> None:
-        """
-        Output rendered content either to a file or console.
-        
-        Args:
-            content: Content to output
-            output_target: Path to output file or None for console output
-            syntax: Syntax highlighting to use for console output
-            template_name: Optional template name for console output header
-            
-        Raises:
-            Exception: If writing to file fails
-        """
-        if output_target:
-            if isinstance(output_target, str):
-                output_target = Path(output_target)
-            self.write_to_file(content, output_target)
-        else:
-            self.print_to_console(content, syntax, template_name)

+ 0 - 0
cli/core/repo.py


+ 141 - 66
cli/core/template.py

@@ -1,83 +1,158 @@
-"""
-Core template utilities for processing and rendering boilerplate templates.
-Provides shared functionality for template cleaning, validation, and rendering
-across different module types (compose, ansible, etc.).
-"""
-import re
-import logging
 from pathlib import Path
 from pathlib import Path
-from typing import Optional, Tuple
+from typing import Any, Dict, Set, Tuple
+from jinja2 import Environment, BaseLoader, meta, nodes
+import frontmatter
 
 
-try:
-    import jinja2
-except ImportError:
-    jinja2 = None
 
 
-logger = logging.getLogger(__name__)
-
-def clean_template_content(content: str) -> str:
-    """
-    Remove template metadata blocks and prepare content for Jinja2 rendering.
+class Template:
+  """Data class for template information extracted from frontmatter."""
+  
+  def __init__(self, file_path: Path, frontmatter_data: Dict[str, Any], content: str):
+    self.file_path = file_path
+    self.content = content
     
     
-    Args:
-        content: Raw template content
-        
-    Returns:
-        Cleaned template content with metadata blocks removed
-    """
-    # Remove variables block as it's not valid Jinja2 syntax
-    return re.sub(r"\{%\s*variables\s*%\}(.+?)\{%\s*endvariables\s*%\}\n?", "", content, flags=re.S)
+    # Extract frontmatter fields with defaults
+    self.name = frontmatter_data.get('name', file_path.parent.name)  # Use directory name as default
+    self.description = frontmatter_data.get('description', 'No description available')
+    self.author = frontmatter_data.get('author', '')
+    self.date = frontmatter_data.get('date', '')
+    self.version = frontmatter_data.get('version', '')
+    self.module = frontmatter_data.get('module', '')
+    self.tags = frontmatter_data.get('tags', [])
+    self.files = frontmatter_data.get('files', [])
+    
+    # Additional computed properties
+    self.id = file_path.parent.name  # Unique identifier (parent directory name)
+    self.directory = file_path.parent.name  # Directory name where the template is located
+    self.relative_path = file_path.name
+    self.size = file_path.stat().st_size if file_path.exists() else 0
+    
+    # Extract variables and defaults from the template content
+    # vars: Set[str] - All Jinja2 variable names found in template (e.g., {'app_name', 'port', 'debug'})
+    # var_defaults: Dict[str, Any] - Default values from | default() filters (e.g., {'app_name': 'my-app', 'port': 8080})
+    self.vars, self.var_defaults = self._parse_template_variables(content)
 
 
-def validate_template(content: str, template_path: Optional[Path] = None) -> Tuple[bool, Optional[str]]:
-    """
-    Validate Jinja2 template syntax before rendering.
+  @classmethod
+  def from_file(cls, file_path: Path) -> "Template":
+    """Create a Template instance from a file path."""
+    try:
+      frontmatter_data, content = cls._parse_frontmatter(file_path)
+      return cls(file_path=file_path, frontmatter_data=frontmatter_data, content=content)
+    except Exception:
+      # If frontmatter parsing fails, create a basic Template object
+      return cls(
+        file_path=file_path,
+        frontmatter_data={'name': file_path.parent.name},
+        content=""
+      )
+  
+  @staticmethod
+  def _parse_frontmatter(file_path: Path) -> Tuple[Dict[str, Any], str]:
+    """Parse frontmatter and content from a file."""
+    with open(file_path, 'r', encoding='utf-8') as f:
+      post = frontmatter.load(f)
+    return post.metadata, post.content
+  
+  def _parse_template_variables(self, template_content: str) -> Tuple[Set[str], Dict[str, Any]]:
+    """Parse Jinja2 template to extract variables and their default values.
+    
+    Analyzes template content to find:
+    1. All undeclared variables (using AST analysis)
+    2. Default values from | default() filters (using AST traversal)
+    
+    Examples:
+        {{ app_name | default('my-app') }} → vars={'app_name'}, defaults={'app_name': 'my-app'}
+        {{ port | default(8080) }} → vars={'port'}, defaults={'port': 8080}
+        {{ unused_var }} → vars={'unused_var'}, defaults={}
     
     
-    Args:
-        content: Template content to validate
-        template_path: Optional path to template file for error messages
-        
     Returns:
     Returns:
-        Tuple of (is_valid, error_message)
+        Tuple of (all_variable_names, variable_defaults)
     """
     """
-    if not jinja2:
-        return False, "Jinja2 is required to render templates. Install it and retry."
-        
     try:
     try:
-        env = jinja2.Environment(loader=jinja2.BaseLoader())
-        env.parse(content)
-        return True, None
-    except jinja2.exceptions.TemplateSyntaxError as e:
-        path_info = f" in '{template_path}'" if template_path else ""
-        return False, f"Template syntax error{path_info}: {e.message} (line {e.lineno})"
-    except Exception as e:
-        path_info = f" '{template_path}'" if template_path else ""
-        return False, f"Failed to parse template{path_info}: {e}"
+      # Use consistent Jinja2 environment configuration
+      env = Environment(
+        loader=BaseLoader(),
+        trim_blocks=True,           # Remove first newline after block tags
+        lstrip_blocks=True,         # Strip leading whitespace from block tags  
+        keep_trailing_newline=False # Remove trailing newlines
+      )
+      ast = env.parse(template_content)
+      
+      # Extract all undeclared variables
+      all_variables = meta.find_undeclared_variables(ast)
+      
+      # Extract default values from | default() filters
+      defaults = {
+        node.node.name: node.args[0].value
+        for node in ast.find_all(nodes.Filter)
+        if node.name == 'default' 
+        and isinstance(node.node, nodes.Name) 
+        and node.args 
+        and isinstance(node.args[0], nodes.Const)
+      }
+      
+      return all_variables, defaults
+    except Exception:
+      return set(), {}
 
 
-def render_template(content: str, values: dict) -> Tuple[bool, str, Optional[str]]:
-    """
-    Render a template with the provided values.
+  @staticmethod
+  def _parse_frontmatter(file_path: Path) -> Tuple[Dict[str, Any], str]:
+    """Parse frontmatter and content from a file."""
+    with open(file_path, 'r', encoding='utf-8') as f:
+      post = frontmatter.load(f)
+    return post.metadata, post.content
+
+  def to_dict(self) -> Dict[str, Any]:
+    """Convert to dictionary for display."""
+    return {
+      'id': self.id,
+      'name': self.name,
+      'description': self.description,
+      'author': self.author,
+      'date': self.date,
+      'version': self.version,
+      'module': self.module,
+      'tags': self.tags,
+      'files': self.files,
+      'directory': self.directory,
+      'path': str(self.relative_path),
+      'size': f"{self.size:,} bytes",
+      'vars': list(self.vars),
+      'var_defaults': self.var_defaults
+    }
+
+  def render(self, variable_values: Dict[str, Any]) -> str:
+    """Render the template with the provided variable values.
     
     
     Args:
     Args:
-        content: Template content to render
-        values: Dictionary of values to use in rendering
+        variable_values: Dictionary of variable names to their values
         
         
     Returns:
     Returns:
-        Tuple of (success, rendered_content or empty string, error_message or None)
+        Rendered template content as string
     """
     """
-    if not jinja2:
-        return False, "", "Jinja2 is required to render templates. Install it and retry."
-        
+    import logging
+    import re
+    
+    logger = logging.getLogger('boilerplates')
+    
     try:
     try:
-        # Enable whitespace control for cleaner output
-        env = jinja2.Environment(
-            loader=jinja2.BaseLoader(),
-            trim_blocks=True,
-            lstrip_blocks=True
-        )
-        template = env.from_string(content)
-        rendered = template.render(**values)
-        return True, rendered, None
-    except jinja2.exceptions.TemplateError as e:
-        return False, "", f"Template rendering error: {e}"
+      # Configure Jinja2 environment to handle whitespace and blank lines
+      env = Environment(
+        loader=BaseLoader(),
+        trim_blocks=True,           # Remove first newline after block tags
+        lstrip_blocks=True,         # Strip leading whitespace from block tags
+        keep_trailing_newline=False # Remove trailing newlines
+      )
+      jinja_template = env.from_string(self.content)
+      rendered_content = jinja_template.render(**variable_values)
+      
+      # Additional post-processing to remove multiple consecutive blank lines
+      # Replace multiple consecutive newlines with single newlines
+      rendered_content = re.sub(r'\n\s*\n\s*\n+', '\n\n', rendered_content)
+      # Remove leading/trailing whitespace
+      rendered_content = rendered_content.strip()
+      
+      return rendered_content
     except Exception as e:
     except Exception as e:
-        return False, "", f"Unexpected error while rendering: {e}"
+      logger.error(f"Jinja2 template rendering failed: {e}")
+      raise ValueError(f"Failed to render template: {e}")

+ 0 - 153
cli/core/values.py

@@ -1,153 +0,0 @@
-"""
-Core values loading functionality for handling template values from various sources.
-Provides consistent value loading from files and command line arguments.
-"""
-import json
-import logging
-from pathlib import Path
-from typing import Dict, List, Any, Optional
-
-try:
-    import yaml
-except ImportError:
-    yaml = None
-
-logger = logging.getLogger(__name__)
-
-class ValuesLoader:
-    """Handles loading and merging of template values from various sources."""
-
-    @staticmethod
-    def load_from_file(file_path: Path) -> Dict[str, Any]:
-        """
-        Load values from a YAML or JSON file.
-        
-        Args:
-            file_path: Path to the values file
-            
-        Returns:
-            Dictionary of loaded values
-            
-        Raises:
-            ValueError: If file format is unsupported or file doesn't exist
-            Exception: If file loading fails
-        """
-        if not file_path.exists():
-            raise ValueError(f"Values file '{file_path}' not found.")
-            
-        try:
-            with open(file_path, 'r', encoding='utf-8') as f:
-                if file_path.suffix.lower() in ['.yaml', '.yml']:
-                    if not yaml:
-                        raise ImportError("PyYAML is required to load YAML files. Install it and retry.")
-                    return yaml.safe_load(f) or {}
-                elif file_path.suffix.lower() == '.json':
-                    return json.load(f)
-                else:
-                    raise ValueError(
-                        f"Unsupported file format '{file_path.suffix}'. Use .yaml, .yml, or .json"
-                    )
-        except Exception as e:
-            raise Exception(f"Failed to load values from {file_path}: {e}")
-
-    @staticmethod
-    def parse_cli_values(values: List[str]) -> Dict[str, Any]:
-        """
-        Parse values provided via command line arguments.
-        
-        Args:
-            values: List of key=value strings
-            
-        Returns:
-            Dictionary of parsed values
-            
-        Raises:
-            ValueError: If value format is invalid
-        """
-        result = {}
-        
-        for value_pair in values:
-            if '=' not in value_pair:
-                raise ValueError(
-                    f"Invalid value format '{value_pair}'. Use key=value format."
-                )
-                
-            key, val = value_pair.split('=', 1)
-            
-            # Try to parse as JSON for complex values
-            try:
-                result[key] = json.loads(val)
-            except json.JSONDecodeError:
-                # If not valid JSON, use as string
-                result[key] = val
-                
-        return result
-
-    @staticmethod
-    def merge_values(*sources: Dict[str, Any]) -> Dict[str, Any]:
-        """
-        Merge multiple value sources with later sources taking precedence.
-        
-        Args:
-            *sources: Dictionaries of values to merge
-            
-        Returns:
-            Merged values dictionary
-        """
-        result = {}
-        
-        for source in sources:
-            result.update(source)
-            
-        return result
-
-def load_and_merge_values(
-    values_file: Optional[Path] = None,
-    cli_values: Optional[List[str]] = None,
-    config_values: Optional[Dict[str, Any]] = None,
-    defaults: Optional[Dict[str, Any]] = None
-) -> Dict[str, Any]:
-    """
-    Load and merge values from all available sources in order of precedence:
-    defaults <- config <- file <- CLI
-    
-    Args:
-        values_file: Optional path to values file
-        cli_values: Optional list of CLI key=value pairs
-        config_values: Optional values from configuration
-        defaults: Optional default values
-        
-    Returns:
-        Dictionary of merged values
-        
-    Raises:
-        Exception: If value loading fails
-    """
-    sources = []
-    
-    # Start with defaults if provided
-    if defaults:
-        sources.append(defaults)
-        
-    # Add config values if provided
-    if config_values:
-        sources.append(config_values)
-        
-    # Load from file if specified
-    if values_file:
-        try:
-            file_values = ValuesLoader.load_from_file(values_file)
-            sources.append(file_values)
-        except Exception as e:
-            raise Exception(f"Failed to load values file: {e}")
-            
-    # Parse CLI values if provided
-    if cli_values:
-        try:
-            parsed_cli_values = ValuesLoader.parse_cli_values(cli_values)
-            sources.append(parsed_cli_values)
-        except ValueError as e:
-            raise Exception(f"Failed to parse CLI values: {e}")
-            
-    # Merge all sources
-    return ValuesLoader.merge_values(*sources)

+ 241 - 216
cli/core/variables.py

@@ -1,217 +1,242 @@
-"""
-Core variables support for interactive collection and detection.
-
-Provides a BaseVariables class that can detect which variable sets are used
-in a Jinja2 template and interactively collect values from the user.
-"""
-from typing import Dict, List, Tuple, Set, Any
-import jinja2
-from jinja2 import meta
-import typer
-from .prompt import PromptHandler
-
-
-class BaseVariables:
-    """Base implementation for variable sets and interactive prompting.
-
-     Subclasses should set `variable_sets` to one of two shapes:
-
-     1) Legacy shape (mapping of set-name -> { var_name: { ... } })
-         { "general": { "foo": { ... }, ... } }
-
-     2) New shape (mapping of set-name -> { "prompt": str, "variables": { var_name: { ... } } })
-         { "general": { "prompt": "...", "variables": { "foo": { ... } } } }
+from typing import Any, Dict, List, Tuple
+from .config import ConfigManager
+
+
+class Variable:
+  """Data class for variable information."""
+  
+  def __init__(self, name: str, description: str = "", value: Any = None, var_type: str = "string", options: List[Any] = None, enabled: bool = True):
+    self.name = name
+    self.description = description
+    self.value = value
+    self.type = var_type  # e.g., string, integer, boolean, choice
+    self.options = options if options is not None else []  # For choice type
+    self.enabled = enabled  # Whether this variable is enabled (default: True)
+
+
+class VariableGroup():
+  """Data class for variable groups."""
+  
+  def __init__(self, name: str, description: str = "", vars: List[Variable] = None, enabled: bool = True):
+    self.name = name
+    self.description = description
+    self.vars = vars if vars is not None else []
+    self.enabled = enabled  # Whether this variable group is enabled
+    self.prompt_to_set = ""  # Custom prompt message
+    self.prompt_to_enable = ""  # Custom prompt message when asking to enable this group
+  
+  def is_enabled(self) -> bool:
+    """Check if this variable group is enabled."""
+    return self.enabled
+  
+  def enable(self) -> None:
+    """Enable this variable group."""
+    self.enabled = True
+  
+  def disable(self) -> None:
+    """Disable this variable group."""
+    self.enabled = False
+  
+  def get_enabled_variables(self) -> List[Variable]:
+    """Get all enabled variables in this group."""
+    return [var for var in self.vars if var.enabled]
+  
+  def disable_variables_not_in_template(self, template_vars: List[str]) -> None:
+    """Disable all variables that are not found in the template variables.
+    
+    Args:
+        template_vars: List of variable names used in the template
     """
     """
-
-    variable_sets: Dict[str, Dict[str, Any]] = {}
-
-    def __init__(self) -> None:
-        # Flattened list of all declared variable names -> (set_name, meta)
-        self._declared: Dict[str, Tuple[str, Dict[str, Any]]] = {}
-        # Support both legacy and new shapes. If the set value contains a
-        # 'variables' key, use that mapping; otherwise assume the mapping is
-        # directly the vars map (legacy).
-        if not hasattr(self, "variable_sets"):
-            self.variable_sets = {}
-        # Ensure we can iterate over variable_sets
-        if not isinstance(self.variable_sets, dict):
-            self.variable_sets = {}
-        for set_name, set_def in self.variable_sets.items():
-                vars_map = set_def.get("variables") if isinstance(set_def, dict) and "variables" in set_def else set_def
-                if not isinstance(vars_map, dict):
-                    continue
-                for var_name, meta_info in vars_map.items():
-                    self._declared[var_name] = (set_name, meta_info)
-
-    def find_used_variables(self, template_content: str) -> Set[str]:
-        """Parse the Jinja2 template and return the set of variable names used."""
-        env = jinja2.Environment()
-        try:
-            ast = env.parse(template_content)
-            used = meta.find_undeclared_variables(ast)
-            return set(used)
-        except Exception:
-            # If parsing fails, fallback to an empty set (safe behavior)
-            return set()
-
-    def find_used_subscript_keys(self, template_content: str) -> Dict[str, Set[str]]:
-        """Return mapping of variable name -> set of string keys accessed via subscripting
-
-        Example: for template using service_port['http'] and service_port['https']
-        this returns { 'service_port': {'http', 'https'} }.
-        """
-        try:
-            env = jinja2.Environment()
-            ast = env.parse(template_content)
-            # Walk AST and collect Subscript nodes
-            from jinja2 import nodes
-
-            subs: Dict[str, Set[str]] = {}
-
-            for node in ast.find_all(nodes.Getitem):
-                # Getitem node structure: node.node (value), node.arg (index)
-                try:
-                    if isinstance(node.node, nodes.Name):
-                        var_name = node.node.name
-                        # index can be Const (string) or Name/other; handle Const
-                        idx = node.arg
-                        if isinstance(idx, nodes.Const) and isinstance(idx.value, str):
-                            subs.setdefault(var_name, set()).add(idx.value)
-                except Exception:
-                    continue
-
-            return subs
-        except Exception:
-            return {}
-
-    def extract_template_defaults(self, template_content: str) -> Dict[str, Any]:
-        """Extract default values from Jinja2 expressions like {{ var | default(value) }}."""
-        import re
-
-        def _parse_literal(s: str):
-            s = s.strip()
-            if s.startswith("'") and s.endswith("'"):
-                return s[1:-1]
-            if s.startswith('"') and s.endswith('"'):
-                return s[1:-1]
-            if s.isdigit():
-                return int(s)
-            return s
-
-        defaults: Dict[str, Any] = {}
-
-
-        # Match {{ var['key'] | default(value) }} and {{ var | default(value) }}
-        pattern_subscript = r'\{\{\s*(\w+)\s*\[\s*["\']([^"\']+)["\']\s*\]\s*\|\s*default\(([^)]+)\)\s*\}\}'
-        for var, key, default_str in re.findall(pattern_subscript, template_content):
-            if var not in defaults or not isinstance(defaults[var], dict):
-                defaults[var] = {}
-            defaults[var][key] = _parse_literal(default_str)
-
-        pattern_scalar = r'\{\{\s*(\w+)\s*\|\s*default\(([^)]+)\)\s*\}\}'
-        for var, default_str in re.findall(pattern_scalar, template_content):
-            # Only set scalar default if not already set as a dict
-            if var not in defaults:
-                defaults[var] = _parse_literal(default_str)
-
-        # Handle simple {% set name = other | default('val') %} patterns
-        set_pattern = r"\{%\s*set\s+(\w+)\s*=\s*([^%]+?)\s*%}"
-        for set_var, expr in re.findall(set_pattern, template_content):
-            m = re.match(r"(\w+)\s*\|\s*default\(([^)]+)\)", expr.strip())
-            if m:
-                src_var, src_default = m.groups()
-                if src_var in defaults:
-                    defaults[set_var] = defaults[src_var]
-                else:
-                    defaults[set_var] = _parse_literal(src_default)
-
-        # Resolve transitive references: if a default is an identifier that
-        # points to another default, follow it; if it points to a declared
-        # variable with a metadata default, use that.
-        def _resolve_ref(value, seen: Set[str]):
-            if not isinstance(value, str):
-                return value
-            if value in seen:
-                return value
-            seen.add(value)
-            if value in defaults:
-                return _resolve_ref(defaults[value], seen)
-            if value in self._declared:
-                declared_def = self._declared[value][1].get("default")
-                if declared_def is not None:
-                    return declared_def
-            return value
-
-        for k in list(defaults.keys()):
-            defaults[k] = _resolve_ref(defaults[k], set([k]))
-
-        return defaults
-
-    def extract_variable_meta_overrides(self, template_content: str) -> Dict[str, Dict[str, Any]]:
-        """Extract variable metadata overrides from a Jinja2 block.
-
-        Supports a block like:
-
-        {% variables %}
-        container_hostname:
-          description: "..."
-        {% endvariables %}
-
-        The contents are parsed as YAML and returned as a dict mapping
-        variable name -> metadata overrides.
-        """
-        import re
-        try:
-            m = re.search(r"\{%\s*variables\s*%\}(.+?)\{%\s*endvariables\s*%\}", template_content, flags=re.S)
-            if not m:
-                return {}
-            yaml_block = m.group(1).strip()
-            try:
-                import yaml
-            except Exception:
-                return {}
-            try:
-                data = yaml.safe_load(yaml_block) or {}
-                if isinstance(data, dict):
-                    # Ensure values are dicts
-                    cleaned: Dict[str, Dict[str, Any]] = {}
-                    for k, v in data.items():
-                        if v is None:
-                            cleaned[k] = {}
-                        elif isinstance(v, dict):
-                            cleaned[k] = v
-                        else:
-                            # If a scalar was provided, interpret as description
-                            cleaned[k] = {"description": v}
-                    return cleaned
-            except Exception:
-                return {}
-        except Exception:
-            return {}
-        return {}
-
-    def determine_variable_sets(self, template_content: str) -> Tuple[List[str], Set[str]]:
-        """
-        Also returns the raw set of used variable names.
-        """
-        used = self.find_used_variables(template_content)
-        matched_sets: List[str] = []
-        variable_sets = getattr(self, "variable_sets", {})
-        if not isinstance(variable_sets, dict):
-            return [], used
-        for set_name, set_def in variable_sets.items():
-            vars_map = set_def.get("variables") if isinstance(set_def, dict) and "variables" in set_def else set_def
-            if not isinstance(vars_map, dict):
-                continue
-            if any(var in used for var in vars_map.keys()):
-                matched_sets.append(set_name)
-        return matched_sets, used
-
-    def collect_values(self, used_vars: Set[str], template_defaults: Dict[str, Any] = None, used_subscripts: Dict[str, Set[str]] = None) -> Dict[str, Any]:
-        """Interactively prompt for values for the variables that appear in the template.
-
-        For variables that were declared in `variable_sets` we use their metadata.
-        For unknown variables, we fall back to a generic prompt.
-        """
-        prompt_handler = PromptHandler(self._declared, getattr(self, "variable_sets", {}))
-        return prompt_handler.collect_values(used_vars, template_defaults, used_subscripts)
+    for var in self.vars:
+      if var.name not in template_vars:
+        var.enabled = False
+  
+  @classmethod
+  def from_dict(cls, name: str, config: Dict[str, Any]) -> "VariableGroup":
+    """Create a VariableGroup from a dictionary configuration."""
+    variables = []
+    vars_config = config.get("vars", {})
+    
+    for var_name, var_config in vars_config.items():
+      var_type = var_config.get("var_type", "string")  # Default to string if not specified
+      enabled = var_config.get("enabled", True)  # Default to enabled if not specified
+      variables.append(Variable(
+        name=var_name,
+        description=var_config.get("description", ""),
+        value=var_config.get("value"),
+        var_type=var_type,
+        enabled=enabled
+      ))
+    
+    return cls(
+      name=name,
+      description=config.get("description", ""),
+      vars=variables,
+      enabled=config.get("enabled", True)  # Default to enabled if not specified
+    )
+
+
+class VariableManager:
+  """Manager class for handling collections of VariableGroups.
+  
+  The VariableManager centralizes variable-related operations for:
+  - Managing VariableGroups
+  - Validating template variables
+  - Filtering variables for specific templates
+  - Resolving variable defaults with priority handling
+  """
+  
+  def __init__(self, variable_groups: List[VariableGroup] = None, config_manager: ConfigManager = None):
+    """Initialize the VariableManager with a list of VariableGroups and ConfigManager."""
+    self.variable_groups = variable_groups if variable_groups is not None else []
+    self.config_manager = config_manager if config_manager is not None else ConfigManager()
+  
+  def add_group(self, group: VariableGroup) -> None:
+    """Add a VariableGroup to the manager."""
+    if not isinstance(group, VariableGroup):
+      raise ValueError("group must be a VariableGroup instance")
+    self.variable_groups.append(group)
+  
+  def disable_variables_not_in_template(self, template_vars: List[str]) -> None:
+    """Disable all variables in all groups that are not found in the template variables.
+    
+    Args:
+        template_vars: List of variable names used in the template
+    """
+    for group in self.variable_groups:
+      group.disable_variables_not_in_template(template_vars)
+  
+  def get_all_variable_names(self) -> List[str]:
+    """Get all variable names from all variable groups."""
+    return [var.name for group in self.variable_groups for var in group.vars]
+  
+  def has_variable(self, name: str) -> bool:
+    """Check if a variable exists in any group."""
+    for group in self.variable_groups:
+      for var in group.vars:
+        if var.name == name:
+          return True
+    return False
+  
+  def validate_template_variables(self, template_vars: List[str]) -> Tuple[bool, List[str]]:
+    """Validate if all template variables exist in the variable groups.
+    
+    Args:
+        template_vars: List of variable names used in the template
+        
+    Returns:
+        Tuple of (success: bool, missing_variables: List[str])
+    """
+    all_variables = self.get_all_variable_names()
+    missing_variables = [var for var in template_vars if var not in all_variables]
+    success = len(missing_variables) == 0
+    return success, missing_variables
+  
+  def filter_variables_for_template(self, template_vars: List[str]) -> Dict[str, Any]:
+    """Filter the variable groups to only include variables needed by the template.
+    
+    Args:
+        template_vars: List of variable names used in the template
+        
+    Returns:
+        Dictionary with filtered variable groups and their variables, including group metadata
+    """
+    filtered_vars = {}
+    
+    for group in self.variable_groups:
+      group_has_template_vars = False
+      group_vars = {}
+      
+      for variable in group.vars:
+        if variable.name in template_vars:
+          group_has_template_vars = True
+          group_vars[variable.name] = {
+            'name': variable.name,
+            'description': variable.description,
+            'value': variable.value,
+            'type': variable.type,
+            'options': getattr(variable, 'options', []),
+            'enabled': variable.enabled
+          }
+      
+      # Only include groups that have variables used by the template
+      if group_has_template_vars:
+        filtered_vars[group.name] = {
+          'description': group.description,
+          'enabled': group.enabled,
+          'prompt_to_set': getattr(group, 'prompt_to_set', ''),
+          'prompt_to_enable': getattr(group, 'prompt_to_enable', ''),
+          'vars': group_vars
+        }
+    
+    return filtered_vars
+  
+  def get_module_defaults(self, template_vars: List[str]) -> Dict[str, Any]:
+    """Get default values from module variable definitions for template variables.
+    
+    Args:
+        template_vars: List of variable names used in the template
+        
+    Returns:
+        Dictionary mapping variable names to their default values
+    """
+    defaults = {}
+    
+    for group in self.variable_groups:
+      for variable in group.vars:
+        if variable.name in template_vars and variable.value is not None:
+          defaults[variable.name] = variable.value
+    
+    return defaults
+  
+  def resolve_variable_defaults(self, module_name: str, template_vars: List[str], template_defaults: Dict[str, Any] = None) -> Dict[str, Any]:
+    """Resolve variable default values with hardcoded priority handling.
+    
+    Priority order (hardcoded):
+    1. Module variable defaults (low priority)
+    2. Template's built-in defaults from |default() filters (medium priority)
+    3. User config defaults (high priority)
+    
+    Args:
+        module_name: Name of the module (for config lookup)
+        template_vars: List of variable names used in the template
+        template_defaults: Dictionary of template's built-in default values
+        
+    Returns:
+        Dictionary of variable names to their resolved default values
+    """
+    if template_defaults is None:
+      template_defaults = {}
+    
+    # Priority 1: Start with module variable defaults (low priority)
+    defaults = self.get_module_defaults(template_vars)
+    
+    # Priority 2: Override with template's built-in defaults (medium priority)
+    defaults.update(template_defaults)
+    
+    # Priority 3: Override with user config defaults (high priority)
+    user_config_defaults = self.config_manager.get_variable_defaults(module_name)
+    for var_name in template_vars:
+      if var_name in user_config_defaults:
+        defaults[var_name] = user_config_defaults[var_name]
+    
+    return defaults
+  
+  def get_summary(self) -> Dict[str, Any]:
+    """Get a summary of all variable groups and their contents."""
+    summary = {
+      'total_groups': len(self.variable_groups),
+      'total_variables': len(self.get_all_variable_names()),
+      'groups': []
+    }
+    
+    for group in self.variable_groups:
+      group_info = {
+        'name': group.name,
+        'description': group.description,
+        'variable_count': len(group.vars),
+        'variables': [var.name for var in group.vars]
+      }
+      summary['groups'].append(group_info)
+    
+    return summary

+ 31 - 33
cli/modules/__init__.py

@@ -1,39 +1,37 @@
 """
 """
 Modules package for the Boilerplates CLI.
 Modules package for the Boilerplates CLI.
-Contains all module implementations for different infrastructure types.
-"""
-
-from typing import List
-from .ansible import AnsibleModule
-from .docker import DockerModule
-from .compose import ComposeModule
-from .github_actions import GitHubActionsModule
-from .gitlab_ci import GitLabCIModule
-from .kestra import KestraModule
-from .kubernetes import KubernetesModule
-from .packer import PackerModule
-from .terraform import TerraformModule
-from .vagrant import VagrantModule
 
 
-from ..core.command import BaseModule
+To add a new module:
+1. Create a new Python file: cli/modules/[module_name].py
+2. Create a class inheriting from Module with the import: from ..core.module import Module
+3. Ensure the class properly sets 'files' parameter and implements required methods
+4. Import and register the module in cli/__main__.py
 
 
+Available modules:
+- compose: Manage Docker Compose configurations and services
+- ansible: Manage Ansible playbooks and configurations
+- docker: Manage Docker configurations and files
+- github_actions: Manage GitHub Actions workflows
+- gitlab_ci: Manage GitLab CI/CD pipelines
+- kestra: Manage Kestra workflows and configurations
+- kubernetes: Manage Kubernetes manifests and configurations
+- packer: Manage Packer templates and configurations
+- terraform: Manage Terraform configurations and modules
+- vagrant: Manage Vagrant configurations and files
 
 
-def get_all_modules() -> List[BaseModule]:
-    """
-    Get all available CLI modules.
+Example:
+    # In cli/modules/mymodule.py
+    from ..core.module import Module
     
     
-    Returns:
-        List of initialized module instances.
-    """
-    return [
-        AnsibleModule(),
-        DockerModule(),
-        ComposeModule(),
-        GitHubActionsModule(),
-        GitLabCIModule(),
-        KestraModule(),
-        KubernetesModule(),
-        PackerModule(),
-        TerraformModule(),
-        VagrantModule(),
-    ]
+    class MyModule(Module):
+        def __init__(self):
+            super().__init__(
+                name="mymodule",
+                description="My module description",
+                files=["config.yml", "settings.json"],
+                vars={"key": "value"}  # optional
+            )
+        
+        def register(self, app):
+            return super().register(app)
+"""

+ 17 - 0
cli/modules/ansible.py

@@ -0,0 +1,17 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="ansible",
+  description="Manage Ansible playbooks and configurations",
+  files=["playbook.yml", "playbook.yaml", "main.yml", "main.yaml", "site.yml", "site.yaml"],
+  priority=8
+)
+class AnsibleModule(Module):
+  """Module for managing Ansible playbooks and configurations."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

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

@@ -1,8 +0,0 @@
-"""
-Ansible module for the Boilerplates CLI.
-Provides commands for managing Ansible playbooks and configurations.
-"""
-
-from .commands import AnsibleModule
-
-__all__ = ["AnsibleModule"]

+ 0 - 19
cli/modules/ansible/commands.py

@@ -1,19 +0,0 @@
-"""
-Ansible module commands and functionality.
-Handles Ansible playbook management with shared base commands.
-"""
-
-from typing import Optional
-import typer
-from ...core.command import BaseModule
-
-
-class AnsibleModule(BaseModule):
-    """Module for managing Ansible playbooks and configurations."""
-    
-    def __init__(self):
-        super().__init__(name="ansible", icon="🎭", description="Manage Ansible playbooks and configurations")
-    
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 52 - 0
cli/modules/compose.py

@@ -0,0 +1,52 @@
+from ..core.module import Module
+from ..core.variables import VariableGroup, Variable, VariableManager
+from ..core.registry import register_module
+
+@register_module(
+  name="compose",
+  description="Manage Docker Compose configurations and services",
+  files=["docker-compose.yml", "compose.yml", "docker-compose.yaml", "compose.yaml"]
+)
+class ComposeModule(Module):
+  """Module for managing Compose configurations and services."""
+
+  def __init__(self):
+    # name, description, and files are automatically injected by the decorator!
+    vars = self._init_vars()
+    super().__init__(name=self.name, description=self.description, files=self.files, vars=vars)
+
+  def _init_vars(self):
+    """Initialize default variables for the compose module."""
+    
+    # Define variable sets configuration as a dictionary
+    variable_sets_config = {
+      "general": {
+        "description": "General variables for compose services",
+        "vars": {
+          "service_name": {"description": "Name of the service", "value": None},
+          "container_name": {"description": "Name of the container", "value": None},
+          "docker_image": {"description": "Docker image to use", "value": "nginx:latest"},
+          "restart_policy": {"description": "Restart policy", "value": "unless-stopped"}
+        }
+      },
+      "swarm": {
+        "description": "Variables for Docker Swarm deployment",
+        "vars": {
+          "replica_count": {"description": "Number of replicas in Swarm", "value": 1, "var_type": "integer"}
+        }
+      },
+      "traefik": {
+        "description": "Variables for Traefik labels",
+        "vars": {
+          "traefik_http_port": {"description": "HTTP port for Traefik", "value": 80, "var_type": "integer"},
+          "traefik_https_port": {"description": "HTTPS port for Traefik", "value": 443, "var_type": "integer"},
+          "traefik_entrypoints": {"description": "Entry points for Traefik", "value": ["http", "https"], "var_type": "list"}
+        }
+      }
+    }
+
+    # Convert dictionary configuration to VariableGroup objects using from_dict
+    return [VariableGroup.from_dict(name, config) for name, config in variable_sets_config.items()]
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/compose/__init__.py

@@ -1,8 +0,0 @@
-"""
-Compose module for the Boilerplates CLI.
-Manage Compose configurations and services.
-"""
-
-from .commands import ComposeModule
-
-__all__ = ["ComposeModule"]

+ 0 - 241
cli/modules/compose/commands.py

@@ -1,241 +0,0 @@
-"""
-Compose module commands and functionality.
-Manage Compose configurations and services and template operations.
-"""
-
-import re
-import typer
-from pathlib import Path
-from rich.console import Console
-from rich.table import Table
-from rich.syntax import Syntax
-from typing import List, Optional, Set, Dict, Any
-
-from ...core.command import BaseModule
-from ...core.helpers import find_boilerplates
-from .variables import ComposeVariables
-
-
-class ComposeModule(BaseModule):
-    """Module for managing compose boilerplates."""
-
-    compose_filenames = ["compose.yaml", "docker-compose.yaml", "compose.yml", "docker-compose.yml"]
-    _library_path = Path(__file__).parent.parent.parent.parent / "library" / "compose"
-
-    def __init__(self):
-        super().__init__(name="compose", icon="🐳", description="Manage Compose Templates and Configurations")
-
-    # Core BaseModule integration
-    @property
-    def template_paths(self) -> List[str]:
-        # Prefer compose.yaml as default per project rules
-        return self.compose_filenames
-
-    @property
-    def library_path(self) -> Path:
-        return self._library_path
-
-    @property
-    def variable_handler_class(self):
-        return ComposeVariables
-    
-    def _get_variable_details(self) -> Dict[str, Dict[str, Any]]:
-        """Get detailed information about variables for display."""
-        variables = ComposeVariables()
-        details = {}
-        for var_name, (set_name, var_meta) in variables._declared.items():
-            details[var_name] = {
-                'set': set_name,
-                'type': var_meta.get('type', 'str'),
-                'display_name': var_meta.get('display_name', var_name),
-                'default': var_meta.get('default'),
-                'prompt': var_meta.get('prompt', '')
-            }
-        return details
-
-    def _add_custom_commands(self, app: typer.Typer) -> None:
-        """Add compose-specific commands to the app."""
-
-        @app.command("list", help="List all compose boilerplates")
-        def list():
-            """List all compose boilerplates from library/compose directory."""
-            bps = find_boilerplates(self.library_path, self.compose_filenames)
-            if not bps:
-                self.console.print("[yellow]No compose boilerplates found.[/yellow]")
-                return
-            table = Table(title="🐳 Available Compose Boilerplates", title_style="bold blue")
-            table.add_column("Name", style="cyan", no_wrap=True)
-            table.add_column("Module", style="magenta")
-            table.add_column("Path", style="green")
-            table.add_column("Size", justify="right", style="yellow")
-            table.add_column("Description", style="dim")
-            for bp in bps:
-                if bp.size < 1024:
-                    size_str = f"{bp.size} B"
-                elif bp.size < 1024 * 1024:
-                    size_str = f"{bp.size // 1024} KB"
-                else:
-                    size_str = f"{bp.size // (1024 * 1024)} MB"
-                table.add_row(
-                    bp.name,
-                    bp.module,
-                    str(bp.file_path.relative_to(self.library_path)),
-                    size_str,
-                    bp.description[:50] + "..." if len(bp.description) > 50 else bp.description
-                )
-            self.console.print(table)
-
-        @app.command("show", help="Show details about a compose boilerplate")
-        def show(name: str, raw: bool = typer.Option(False, "--raw", help="Output only the raw boilerplate content")):
-            """Show details about a compose boilerplate by name."""
-            bps = find_boilerplates(self.library_path, self.compose_filenames)
-            # Match by directory name (parent folder of the compose file) instead of frontmatter 'name'
-            bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
-            if not bp:
-                self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
-                return
-            if raw:
-                # Output only the raw boilerplate content
-                print(bp.content)
-                return
-            # Print frontmatter info in a clean, readable format
-            from rich.text import Text
-            from rich.console import Group
-            
-            info = bp.to_dict()
-            
-            # Create a clean header
-            header = Text()
-            header.append("🐳 Boilerplate: ", style="bold")
-            header.append(f"{info['name']}", style="bold blue")
-            header.append(f" ({info['version']})", style="magenta")
-            header.append("\n", style="bold")
-            header.append(f"{info['description']}", style="dim white")
-            
-            # Create metadata section with clean formatting
-            metadata = Text()
-            metadata.append("\nDetails:\n", style="bold cyan")
-            metadata.append("─" * 40 + "\n", style="dim cyan")
-            
-            # Format each field with consistent styling
-            fields = [
-                ("Tags", ", ".join(info['tags']), "cyan"),
-                ("Author", info['author'], "dim white"), 
-                ("Date", info['date'], "dim white"),
-                ("Size", info['size'], "dim white"),
-                ("Path", info['path'], "dim white")
-            ]
-            
-            for label, value, color in fields:
-                metadata.append(f"{label}: ")
-                metadata.append(f"{value}\n", style=color)
-            
-            # Handle files list if present
-            if info['files'] and len(info['files']) > 0:
-                metadata.append("  Files: ")
-                files_str = ", ".join(info['files'][:3])  # Show first 3
-                if len(info['files']) > 3:
-                    files_str += f" ... and {len(info['files']) - 3} more"
-                metadata.append(f"{files_str}\n", style="green")
-            
-            # Display everything as a group
-            display_group = Group(header, metadata)
-            self.console.print(display_group)
-
-
-            # Show the content of the boilerplate file in a cleaner form
-            from rich.panel import Panel
-            from rich.syntax import Syntax
-
-            # Detect if content contains Jinja2 templating
-            has_jinja = bool(re.search(r'\{\{.*\}\}|\{\%.*\%\}|\{\#.*\#\}', bp.content))
-            
-            # Use appropriate lexer based on content
-            # Use yaml+jinja for combined YAML and Jinja2 highlighting when Jinja2 is present
-            lexer = "yaml+jinja" if has_jinja else "yaml"
-            syntax = Syntax(bp.content, lexer, theme="monokai", line_numbers=True, word_wrap=True)
-            panel = Panel(syntax, title=f"{bp.file_path.name}", border_style="blue", padding=(1,2))
-            self.console.print(panel)
-
-        @app.command("search", help="Search compose boilerplates")
-        def search(query: str):
-            pass
-
-        @app.command("generate", help="Generate a compose file from a boilerplate and write to --out")
-        def generate(
-            name: str, 
-            out: Optional[Path] = typer.Option(None, "--out", "-o", help="Output path to write rendered boilerplate (prints to stdout when omitted)"),
-            values_file: Optional[Path] = typer.Option(None, "--values-file", "-f", help="Load values from YAML/JSON file"),
-            cli_values: Optional[List[str]] = typer.Option(None, "--values", help="Set values (format: key=value)")
-        ):
-            """Render a compose boilerplate interactively and write output to --out."""
-            from ...core import template, values as values_mod, render
-            from ...core.config import ConfigManager
-
-            # Find and validate boilerplate
-            bps = find_boilerplates(self.library_path, self.compose_filenames)
-            bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
-            if not bp:
-                self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
-                raise typer.Exit(code=1)
-
-            # Clean template content and find variables
-            cv = ComposeVariables()
-            cleaned_content = template.clean_template_content(bp.content)
-            matched_sets, used_vars = cv.determine_variable_sets(cleaned_content)
-
-            # If no variables used, return original content
-            if not used_vars:
-                rendered = bp.content
-            else:
-                # Validate template syntax
-                is_valid, error = template.validate_template(cleaned_content, bp.file_path)
-                if not is_valid:
-                    self.console.print(f"[red]{error}[/red]")
-                    raise typer.Exit(code=2)
-
-                # Extract defaults and variable metadata
-                template_defaults = cv.extract_template_defaults(cleaned_content)
-                try:
-                    meta_overrides = cv.extract_variable_meta_overrides(bp.content)
-                    # Merge overrides into declared metadata
-                    for var_name, overrides in meta_overrides.items():
-                        if var_name in cv._declared and isinstance(overrides, dict):
-                            existing = cv._declared[var_name][1]
-                            existing.update(overrides)
-                except Exception:
-                    meta_overrides = {}
-
-                # Get subscript keys and load values from all sources
-                used_subscripts = cv.find_used_subscript_keys(bp.content)
-                config_manager = ConfigManager(self.name)
-                try:
-                    merged_values = values_mod.load_and_merge_values(
-                        values_file=values_file,
-                        cli_values=cli_values,
-                        config_values=config_manager.list_all(),
-                        defaults=template_defaults
-                    )
-                except Exception as e:
-                    self.console.print(f"[red]{str(e)}[/red]")
-                    raise typer.Exit(code=1)
-
-                # Collect final values and render template
-                values_dict = cv.collect_values(used_vars, merged_values, used_subscripts)
-                success, rendered, error = template.render_template(
-                    cleaned_content,
-                    values_dict
-                )
-
-                if not success:
-                    self.console.print(f"[red]{error}[/red]")
-                    raise typer.Exit(code=2)
-
-            # Output the rendered content
-            output_handler = render.RenderOutput(self.console)
-            output_handler.output_rendered_content(
-                rendered,
-                out,
-                "yaml",
-                bp.name
-            )

+ 0 - 47
cli/modules/compose/variables.py

@@ -1,47 +0,0 @@
-from typing import Dict, Any
-from ...core.variables import BaseVariables
-
-
-class ComposeVariables(BaseVariables):
-    """Compose-specific variable sets declaration.
-
-    Each entry in `variable_sets` is now a mapping with a `prompt` to ask
-    whether the set should be applied and a `variables` mapping containing
-    the individual variable definitions.
-    """
-
-    def __init__(self) -> None:
-        self.variable_sets: Dict[str, Dict[str, Any]] = {
-        "general": {
-            "always": True,
-            "prompt": "Do you want to change the general settings?",
-            "variables": {
-                    "service_name": {"display_name": "Service name", "default": None, "type": "str", "prompt": "Enter service name"},
-                    "service_port": {"display_name": "Service port", "default": None, "type": "int", "prompt": "Enter service port(s)", "description": "Port number(s) the service will expose (has to be a single port)"},
-                    "container_name": {"display_name": "Container name", "default": None, "type": "str", "prompt": "Enter container name"},
-                    "container_hostname": {"display_name": "Container hostname", "default": None, "type": "str", "prompt": "Enter container hostname", "description": "Hostname that will be set inside the container"},
-                    "docker_network": {"display_name": "Docker network", "default": "bridge", "type": "str", "prompt": "Enter Docker network name"},
-                    "restart_policy": {"display_name": "Restart policy", "default": "unless-stopped", "type": "str", "prompt": "Enter restart policy"},
-            },
-        },
-        "swarm": {
-            "prompt_enable": "Do you want to enable swarm mode?",
-            "prompt": "Do you want to change the Swarm settings?",
-            "variables": {
-                "swarm_replicas": {"display_name": "Number of replicas", "default": 1, "type": "int", "prompt": "Enter number of replicas"},
-            },
-        },
-        "traefik": {
-            "prompt_enable": "Do you want to add Traefik labels?",
-            "prompt": "Do you want to change the Traefik labels?",
-            "variables": {
-                "traefik_enable": {"display_name": "Enable Traefik", "default": True, "type": "bool", "prompt": "Enable Traefik routing for this service?"},
-                "traefik_host": {"display_name": "Routing Rule Host", "default": None, "type": "str", "prompt": "Enter hostname for the routing rule (e.g., example.com))", "description": "Domain name that Traefik will use to route traffic to this service"},
-                "traefik_tls": {"display_name": "Enable TLS", "default": False, "type": "bool", "prompt": "Enable TLS for this router?", "description": "Whether to enable HTTPS/TLS encryption for this route"},
-                "traefik_certresolver": {"display_name": "Certificate resolver", "type": "str", "prompt": "Enter certificate resolver name", "description": "Name of the certificate resolver to use for obtaining SSL certificates"},
-                "traefik_middleware": {"display_name": "Middlewares", "default": None, "type": "str", "prompt": "Enter middlewares (comma-separated, leave empty for none)", "description": "Comma-separated list of Traefik middlewares to apply to this route"},
-                "traefik_entrypoint": {"display_name": "EntryPoint", "default": "web", "type": "str", "prompt": "Enter entrypoint name", "description": "Name of the Traefik entrypoint to use for this router"},
-            },
-        },
-    }
-        super().__init__()

+ 16 - 0
cli/modules/docker.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="docker",
+  description="Manage Docker configurations and files",
+  files=["Dockerfile", "dockerfile", ".dockerignore"]
+)
+class DockerModule(Module):
+  """Module for managing Docker configurations and files."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/docker/__init__.py

@@ -1,8 +0,0 @@
-"""
-Docker module for the Boilerplates CLI.
-Provides commands for managing Docker configurations and containers.
-"""
-
-from .commands import DockerModule
-
-__all__ = ["DockerModule"]

+ 0 - 19
cli/modules/docker/commands.py

@@ -1,19 +0,0 @@
-"""
-Docker module commands and functionality.
-Handles Docker container management with shared base commands.
-"""
-
-from typing import Optional
-import typer
-from ...core.command import BaseModule
-
-
-class DockerModule(BaseModule):
-    """Module for managing Docker configurations and containers."""
-    
-    def __init__(self):
-        super().__init__(name="docker", icon="🐳", description="Manage Docker configurations and containers")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/github_actions.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="github-actions",
+  description="Manage GitHub Actions workflows",
+  files=["action.yml", "action.yaml", "workflow.yml", "workflow.yaml"]
+)
+class GitHubActionsModule(Module):
+  """Module for managing GitHub Actions workflows."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/github_actions/__init__.py

@@ -1,8 +0,0 @@
-"""
-GitHub Actions module for the Boilerplates CLI.
-Manage GitHub Actions workflows and CI/CD.
-"""
-
-from .commands import GitHubActionsModule
-
-__all__ = ["GitHubActionsModule"]

+ 0 - 23
cli/modules/github_actions/commands.py

@@ -1,23 +0,0 @@
-"""
-GitHub Actions module commands and functionality.
-Manage GitHub Actions workflows and CI/CD and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class GitHubActionsModule(BaseModule):
-    """Module for managing github actions configurations."""
-    
-    def __init__(self):
-        super().__init__(name="github_actions", icon="🚀", description="Manage GitHub Actions workflows and CI/CD")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/gitlab_ci.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="gitlab-ci",
+  description="Manage GitLab CI/CD pipelines",
+  files=[".gitlab-ci.yml", ".gitlab-ci.yaml", "gitlab-ci.yml", "gitlab-ci.yaml"]
+)
+class GitLabCIModule(Module):
+  """Module for managing GitLab CI/CD pipelines."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/gitlab_ci/__init__.py

@@ -1,8 +0,0 @@
-"""
-GitLab CI module for the Boilerplates CLI.
-Manage GitLab CI/CD pipelines and configurations.
-"""
-
-from .commands import GitLabCIModule
-
-__all__ = ["GitLabCIModule"]

+ 0 - 23
cli/modules/gitlab_ci/commands.py

@@ -1,23 +0,0 @@
-"""
-GitLab CI module commands and functionality.
-Manage GitLab CI/CD pipelines and configurations and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class GitLabCIModule(BaseModule):
-    """Module for managing gitlab ci configurations."""
-
-    def __init__(self):
-        super().__init__(name="gitlab_ci", icon="🦊", description="Manage GitLab CI/CD pipelines and configurations")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/kestra.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="kestra",
+  description="Manage Kestra workflows and configurations",
+  files=["inputs.yaml", "variables.yaml", "webhook.yaml", "flow.yml", "flow.yaml"]
+)
+class KestraModule(Module):
+  """Module for managing Kestra workflows and configurations."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/kestra/__init__.py

@@ -1,8 +0,0 @@
-"""
-Kestra module for the Boilerplates CLI.
-Manage Kestra workflows and orchestration.
-"""
-
-from .commands import KestraModule
-
-__all__ = ["KestraModule"]

+ 0 - 23
cli/modules/kestra/commands.py

@@ -1,23 +0,0 @@
-"""
-Kestra module commands and functionality.
-Manage Kestra workflows and orchestration and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class KestraModule(BaseModule):
-    """Module for managing Kestra workflows and orchestration."""
-
-    def __init__(self):
-        super().__init__(name="kestra", icon="⚡", description="Manage Kestra workflows and orchestration")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 18 - 0
cli/modules/kubernetes.py

@@ -0,0 +1,18 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="kubernetes",
+  description="Manage Kubernetes manifests and configurations",
+  files=["deployment.yml", "deployment.yaml", "service.yml", "service.yaml", "manifest.yml", "manifest.yaml", "values.yml", "values.yaml"],
+  priority=5,
+  dependencies=["docker"]
+)
+class KubernetesModule(Module):
+  """Module for managing Kubernetes manifests and configurations."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

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

@@ -1,8 +0,0 @@
-"""
-Kubernetes module for the Boilerplates CLI.
-Manage Kubernetes deployments and configurations.
-"""
-
-from .commands import KubernetesModule
-
-__all__ = ["KubernetesModule"]

+ 0 - 23
cli/modules/kubernetes/commands.py

@@ -1,23 +0,0 @@
-"""
-Kubernetes module commands and functionality.
-Manage Kubernetes deployments and configurations and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class KubernetesModule(BaseModule):
-    """Module for managing Kubernetes configurations."""
-
-    def __init__(self):
-        super().__init__(name="kubernetes", icon="☸️", description="Manage Kubernetes deployments and configurations")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/packer.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="packer",
+  description="Manage Packer templates and configurations",
+  files=["template.pkr.hcl", "build.pkr.hcl", "variables.pkr.hcl", "sources.pkr.hcl"]
+)
+class PackerModule(Module):
+  """Module for managing Packer templates and configurations."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

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

@@ -1,8 +0,0 @@
-"""
-Packer module for the Boilerplates CLI.
-Manage Packer templates and image building.
-"""
-
-from .commands import PackerModule
-
-__all__ = ["PackerModule"]

+ 0 - 23
cli/modules/packer/commands.py

@@ -1,23 +0,0 @@
-"""
-Packer module commands and functionality.
-Manage Packer templates and image building and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class PackerModule(BaseModule):
-    """Module for managing Packer configurations."""
-
-    def __init__(self):
-        super().__init__(name="packer", icon="📦", description="Manage Packer templates and image building")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/terraform.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="terraform",
+  description="Manage Terraform configurations and modules",
+  files=["main.tf", "variables.tf", "outputs.tf", "versions.tf", "providers.tf", "terraform.tf"]
+)
+class TerraformModule(Module):
+  """Module for managing Terraform configurations and modules."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

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

@@ -1,8 +0,0 @@
-"""
-Terraform module for the Boilerplates CLI.
-Manage Terraform infrastructure as code.
-"""
-
-from .commands import TerraformModule
-
-__all__ = ["TerraformModule"]

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

@@ -1,23 +0,0 @@
-"""
-Terraform module commands and functionality.
-Manage Terraform infrastructure as code and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class TerraformModule(BaseModule):
-    """Module for managing terraform configurations."""
-    
-    def __init__(self):
-        super().__init__(name="terraform", icon="🏗️", description="Manage Terraform infrastructure as code")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 16 - 0
cli/modules/vagrant.py

@@ -0,0 +1,16 @@
+from ..core.module import Module
+from ..core.registry import register_module
+
+@register_module(
+  name="vagrant",
+  description="Manage Vagrant configurations and files",
+  files=["Vagrantfile", "vagrantfile"]
+)
+class VagrantModule(Module):
+  """Module for managing Vagrant configurations and files."""
+
+  def __init__(self):
+    super().__init__(name=self.name, description=self.description, files=self.files)
+
+  def register(self, app):
+    return super().register(app)

+ 0 - 8
cli/modules/vagrant/__init__.py

@@ -1,8 +0,0 @@
-"""
-Vagrant module for the Boilerplates CLI.
-Manage Vagrant environments and virtual machines.
-"""
-
-from .commands import VagrantModule
-
-__all__ = ["VagrantModule"]

+ 0 - 23
cli/modules/vagrant/commands.py

@@ -1,23 +0,0 @@
-"""
-Vagrant module commands and functionality.
-Manage Vagrant environments and virtual machines and template operations.
-"""
-
-from pathlib import Path
-from typing import List, Optional
-
-import typer
-from rich.table import Table
-
-from ...core.command import BaseModule
-
-
-class VagrantModule(BaseModule):
-    """Module for managing vagrant configurations."""
-    
-    def __init__(self):
-        super().__init__(name="vagrant", icon="📦", description="Manage Vagrant environments and virtual machines")
-
-    def add_module_commands(self, app: typer.Typer) -> None:
-        """Add Module-specific commands to the app."""
-        pass

+ 63 - 0
library/compose/n8n/compose.yaml

@@ -0,0 +1,63 @@
+---
+name: "n8n"
+description: "Workflow automation and integration tool"
+version: "0.0.1"
+date: "2025-09-03"
+author: "Christian Lempa"
+tags:
+  - n8n
+  - automation
+  - workflows
+  - compose
+---
+{% set postgres.always = 'True' %}
+services:
+  {{ service_name | default('n8n') }}:
+    image: n8nio/n8n:1.110.1
+    container_name: {{ container_name | default('n8n') }}
+    environment:
+      - N8N_LOG_LEVEL={{ container_loglevel | default('info') }}
+      - GENERIC_TIMEZONE={{ container_timezone }}
+      - TZ={{ container_timezone }}
+      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
+      - N8N_RUNNERS_ENABLED=true
+      {% if traefik %}
+      {% if traefik_tls %}
+      - N8N_EDITOR_BASE_URL=https://{{ traefik_host }}
+      {% else %}
+      - N8N_EDITOR_BASE_URL=http://{{ traefik_host }}
+      {% endif %}
+      {% endif %}
+      {% if postgres %}
+      - DB_TYPE=postgresdb
+      - DB_POSTGRESDB_HOST={{ postgres_host }}
+      - DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT:-5432}
+      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
+      - DB_POSTGRESDB_USER=${POSTGRES_USER}
+      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
+      {% endif %}
+    volumes:
+      - /etc/localtime:/etc/localtime:ro
+      - data:/home/node/.n8n
+    networks:
+      - {{ docker_network | default('bridge') }}
+    {% if traefik %}
+    labels:
+      - traefik.enable={{ traefik_enable | default('true') }}
+      - traefik.http.routers.{{ container_name }}.rule=Host(`{{ traefik_host }}`)
+      {% if traefik_tls %}
+      - traefik.http.routers.{{ container_name }}.entrypoints=websecure
+      - traefik.http.routers.{{ container_name }}.tls=true
+      - traefik.http.routers.{{ container_name }}.tls.certresolver={{ traefik_certresolver }}
+      - traefik.http.services.{{ container_name }}.loadbalancer.server.port=5678
+      {% endif %}
+    {% endif %}
+    restart: {{ restart_policy }}
+
+volumes:
+  data:
+    driver: local
+
+networks:
+  {{ docker_network | default('bridge') }}:
+    external: true

+ 52 - 0
library/compose/test-complex/compose.yaml

@@ -0,0 +1,52 @@
+---
+name: "Complex Test Template"
+description: "A comprehensive template for testing complex variable prompting logic"
+version: "1.0.0"
+date: "2025-01-01"
+author: "GitHub Copilot"
+tags:
+  - "test"
+  - "complex"
+  - "variables"
+---
+services:
+  {{ service_name }}:
+    image: {{ docker_image | default('nginx:latest') }}
+    container_name: {{ container_name | default(service_name) }}
+    restart: {{ restart_policy | default('unless-stopped') }}
+    
+    {% if traefik_http_port %}
+    ports:
+      - "{{ traefik_http_port }}:80"
+      {% if traefik_https_port %}
+      - "{{ traefik_https_port }}:443"
+      {% endif %}
+    {% endif %}
+    
+    {% if replica_count and replica_count > 1 %}
+    deploy:
+      replicas: {{ replica_count }}
+      update_config:
+        parallelism: 1
+        delay: 10s
+      restart_policy:
+        condition: on-failure
+    {% endif %}
+    
+    {% if traefik_entrypoints %}
+    labels:
+      - "traefik.enable=true"
+      {% for entrypoint in traefik_entrypoints %}
+      - "traefik.http.routers.{{ service_name }}.entrypoints={{ entrypoint }}"
+      {% endfor %}
+    {% endif %}
+    
+    environment:
+      - APP_NAME={{ service_name }}
+      {% if container_name %}
+      - CONTAINER_NAME={{ container_name }}
+      {% endif %}
+
+networks:
+  default:
+    name: {{ service_name }}_network

+ 23 - 0
library/compose/tests/compose.yaml

@@ -0,0 +1,23 @@
+---
+name: "Test Ubuntu Container"
+description: "A simple test compose file to run an Ubuntu container"
+version: "1.0.0"
+date: "2025-09-03"
+author: "Christian Lempa"
+tags:
+  - ubuntu
+  - test
+  - docker
+variables:
+  container_name: "ubuntu-test"
+  image_tag: "latest"
+  restart_policy: "unless-stopped"
+---
+services:
+  {{ service_name }}:
+    image: ubuntu:latest
+    container_name: {{ container_name | default('ubuntu-test') }}
+    restart: {{ restart_policy | default('unless-stopped') }}
+    command: tail -f /dev/null
+    stdin_open: true
+    tty: true

+ 0 - 0
library/proxmox/README.md → library/packer/proxmox/README.md


+ 0 - 0
library/proxmox/credentials.pkr.hcl → library/packer/proxmox/credentials.pkr.hcl


+ 0 - 0
library/proxmox/ubuntu-server-focal-docker/files/99-pve.cfg → library/packer/proxmox/ubuntu-server-focal-docker/files/99-pve.cfg


+ 0 - 0
library/proxmox/ubuntu-server-focal-docker/http/meta-data → library/packer/proxmox/ubuntu-server-focal-docker/http/meta-data


+ 0 - 0
library/proxmox/ubuntu-server-focal-docker/http/user-data → library/packer/proxmox/ubuntu-server-focal-docker/http/user-data


+ 0 - 0
library/proxmox/ubuntu-server-focal-docker/ubuntu-server-focal-docker.pkr.hcl → library/packer/proxmox/ubuntu-server-focal-docker/ubuntu-server-focal-docker.pkr.hcl


+ 0 - 0
library/proxmox/ubuntu-server-focal/files/99-pve.cfg → library/packer/proxmox/ubuntu-server-focal/files/99-pve.cfg


+ 0 - 0
library/proxmox/ubuntu-server-focal/http/meta-data → library/packer/proxmox/ubuntu-server-focal/http/meta-data


+ 0 - 0
library/proxmox/ubuntu-server-focal/http/user-data → library/packer/proxmox/ubuntu-server-focal/http/user-data


+ 0 - 0
library/proxmox/ubuntu-server-focal/ubuntu-server-focal.pkr.hcl → library/packer/proxmox/ubuntu-server-focal/ubuntu-server-focal.pkr.hcl


+ 0 - 0
library/proxmox/ubuntu-server-jammy-docker/files/99-pve.cfg → library/packer/proxmox/ubuntu-server-jammy-docker/files/99-pve.cfg


+ 0 - 0
library/proxmox/ubuntu-server-jammy-docker/http/meta-data → library/packer/proxmox/ubuntu-server-jammy-docker/http/meta-data


+ 0 - 0
library/proxmox/ubuntu-server-jammy-docker/http/user-data → library/packer/proxmox/ubuntu-server-jammy-docker/http/user-data


+ 0 - 0
library/proxmox/ubuntu-server-jammy-docker/ubuntu-server-jammy-docker.pkr.hcl → library/packer/proxmox/ubuntu-server-jammy-docker/ubuntu-server-jammy-docker.pkr.hcl


+ 0 - 0
library/proxmox/ubuntu-server-jammy/files/99-pve.cfg → library/packer/proxmox/ubuntu-server-jammy/files/99-pve.cfg


+ 0 - 0
library/proxmox/ubuntu-server-jammy/http/meta-data → library/packer/proxmox/ubuntu-server-jammy/http/meta-data


+ 0 - 0
library/proxmox/ubuntu-server-jammy/http/user-data → library/packer/proxmox/ubuntu-server-jammy/http/user-data


+ 0 - 0
library/proxmox/ubuntu-server-jammy/ubuntu-server-jammy.pkr.hcl → library/packer/proxmox/ubuntu-server-jammy/ubuntu-server-jammy.pkr.hcl


+ 0 - 0
library/proxmox/ubuntu-server-noble/files/99-pve.cfg → library/packer/proxmox/ubuntu-server-noble/files/99-pve.cfg


+ 0 - 0
library/proxmox/ubuntu-server-noble/http/meta-data → library/packer/proxmox/ubuntu-server-noble/http/meta-data


+ 0 - 0
library/proxmox/ubuntu-server-noble/http/user-data → library/packer/proxmox/ubuntu-server-noble/http/user-data


+ 0 - 0
library/proxmox/ubuntu-server-noble/ubuntu-server-noble.pkr.hcl → library/packer/proxmox/ubuntu-server-noble/ubuntu-server-noble.pkr.hcl


+ 1 - 1
pyproject.toml

@@ -26,4 +26,4 @@ dependencies = [
 ]
 ]
 
 
 [project.scripts]
 [project.scripts]
-boilerplate = "cli.__main__:main"
+boilerplate = "cli.__main__:run"

+ 1 - 1
setup.cfg

@@ -20,4 +20,4 @@ install_requires =
 
 
 [options.entry_points]
 [options.entry_points]
 console_scripts =
 console_scripts =
-    boilerplate = cli.__main__:main
+    boilerplate = cli.__main__:run