Bladeren bron

refactored code

xcad 5 maanden geleden
bovenliggende
commit
4dbadc711a
79 gewijzigde bestanden met toevoegingen van 1592 en 2308 verwijderingen
  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
 
 [*.py]
-indent_size = 4
+indent_size = 2
 
 [*.tf]
 indent_size = unset

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

@@ -59,9 +59,9 @@ boilerplate --log-level DEBUG [command]
 
 ### 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]/`
 
 ## Architecture Notes
@@ -103,6 +103,7 @@ tags:
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
+- **Indentation**: ALWAYS use 2 spaces for indentation!
 
 ## Configuration
 

+ 4 - 3
WARP.md

@@ -59,9 +59,9 @@ boilerplate --log-level DEBUG [command]
 
 ### 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]/`
 
 ## Architecture Notes
@@ -103,6 +103,7 @@ tags:
 - **Docker Compose**: Default to `compose.yaml` filename (not `docker-compose.yml`)
 - **Logging Standards**: No emojis, avoid multi-lines, use proper log levels
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
+- **Spaces in Python**: Prefer using 2 Spaces for indentation
 
 ## Configuration
 

+ 61 - 6
cli/__main__.py

@@ -3,15 +3,70 @@
 Main entry point for the Boilerplates CLI application.
 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__":
-    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 .logging import setup_logging
+from pathlib import Path
 
 
 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:
-    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:
-            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 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:
-        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:
-        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:
-        content: Template content to render
-        values: Dictionary of values to use in rendering
+        variable_values: Dictionary of variable names to their values
         
     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:
-        # 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:
-        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.
-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]
-boilerplate = "cli.__main__:main"
+boilerplate = "cli.__main__:run"

+ 1 - 1
setup.cfg

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