xcad 5 mesi fa
parent
commit
88222cb51f
6 ha cambiato i file con 328 aggiunte e 517 eliminazioni
  1. 21 53
      cli/core/module.py
  2. 0 255
      cli/core/prompt.py
  3. 41 7
      cli/core/template.py
  4. 76 98
      cli/core/variables.py
  5. 159 87
      cli/modules/compose.py
  6. 31 17
      library/compose/n8n/compose.yaml

+ 21 - 53
cli/core/module.py

@@ -1,17 +1,15 @@
 from abc import ABC
 from pathlib import Path
-from typing import List, Optional, Dict, Any
+from typing import Optional, Dict, Any
 import logging
 import yaml
 from typer import Typer, Option, Argument
 from rich.console import Console
-from .config import get_config
-from .exceptions import TemplateNotFoundError, TemplateValidationError
+from .exceptions import TemplateNotFoundError
 from .library import LibraryManager
-from .prompt import SimplifiedPromptHandler
 
 logger = logging.getLogger('boilerplates')
-console = Console()  # Single shared console instance
+console = Console()
 
 
 class Module(ABC):
@@ -29,6 +27,11 @@ class Module(ABC):
       )
     
     self.libraries = LibraryManager()
+    
+    # Initialize variables if the subclass defines _init_variables method
+    if hasattr(self, '_init_variables'):
+      self._init_variables()
+    
     self.metadata = self._build_metadata()
   
   def _build_metadata(self) -> Dict[str, Any]:
@@ -110,62 +113,27 @@ class Module(ABC):
     # Validate template (will raise TemplateValidationError if validation fails)
     self._validate_template(template, id)
     
-    # Process variables and render
-    values = self._process_variables(template)
-    
-    try:
-      content = template.render(values)
-    except Exception as e:
-      logger.error(f"Failed to render template: {e}")
-      raise
-    
-    # Output result
-    if out:
-      out.parent.mkdir(parents=True, exist_ok=True)
-      out.write_text(content)
-      console.print(f"[green]✅ Generated to {out}[/green]")
-    else:
-      console.print(f"\n{'='*60}\nGenerated Content:\n{'='*60}")
-      console.print(content)
+    print("TEST")
   
   def _validate_template(self, template, template_id: str) -> None:
     """Validate template and raise error if validation fails."""
-    # Template is now self-validating, no need for registered variables
-    warnings = template.validate()
+    # Update template variables with module variable registry
+    module_variable_registry = getattr(self, 'variables', None)
+    if module_variable_registry:
+      template.update_variables_with_module_metadata(module_variable_registry)
+      
+      # Validate parent-child relationships in the registry
+      registry_errors = module_variable_registry.validate_parent_child_relationships()
+      if registry_errors:
+        logger.error(f"Variable registry validation errors: {registry_errors}")
+    
+    # Validate template with module variables
+    warnings = template.validate(module_variable_registry)
     
     # If there are non-critical warnings, log them but don't fail
     if warnings:
       logger.warning(f"Template '{template_id}' has validation warnings: {warnings}")
   
-  def _process_variables(self, template) -> Dict[str, Any]:
-    """Process template variables with prompting."""
-    # Use template's analyzed variables
-    if not template.variables:
-      return {}
-    
-    # Apply metadata to variables
-    self._apply_metadata_to_variables(template.variables, template.variable_metadata)
-    
-    # Collect defaults from analyzed variables
-    defaults = {}
-    for var_name, var in template.variables.items():
-      if var.default is not None:
-        defaults[var_name] = var.default
-    
-    # Use rich output if enabled
-    if not get_config().use_rich_output:
-      # Simple fallback - just prompt for missing values
-      values = defaults.copy()
-      for var_name, var in template.variables.items():
-        if var_name not in values:
-          values[var_name] = input(f"Enter {var_name}: ")
-      return values
-    
-    # Pass metadata to prompt handler
-    prompt_handler = SimplifiedPromptHandler(template.variables)
-    prompt_handler.category_metadata = self.metadata.get('categories', {})
-    return prompt_handler()
-  
   def _apply_metadata_to_variables(self, variables: Dict[str, Any], template_metadata: Dict[str, Any]):
     """Apply metadata from module and template to variables."""
     # First apply module metadata

+ 0 - 255
cli/core/prompt.py

@@ -1,255 +0,0 @@
-"""Simplified prompt handler for template variables."""
-from typing import Dict, Any, List, Tuple, Optional
-from collections import OrderedDict
-from rich.console import Console
-from rich.prompt import Prompt, Confirm, IntPrompt, FloatPrompt
-import logging
-from .variables import Variable
-
-logger = logging.getLogger('boilerplates')
-console = Console()
-
-
-class SimplifiedPromptHandler:
-  """Prompt handler for template-detected variables."""
-  
-  def __init__(self, variables: Dict[str, Variable]):
-    """Initialize with template variables.
-    
-    Args:
-      variables: Dict of variable name to Variable object
-    """
-    self.variables = variables
-    self.values = {}
-    self.category_metadata = {}  # Will be set by Module if available
-    
-  def __call__(self) -> Dict[str, Any]:
-    """Execute the prompting flow."""
-    # Group variables by prefix (preserves registration order)
-    groups, standalone = self._group_variables()
-    
-    # Process standalone variables first as "General" group
-    if standalone:
-      self._process_variable_set("General", standalone, is_group=False)
-    
-    # Process each group in the order they were registered
-    for group_name, group_vars in groups.items():
-      self._process_variable_set(group_name.title(), group_vars, is_group=True)
-    
-    return self.values
-  
-  def _group_variables(self) -> Tuple[Dict[str, List[str]], List[str]]:
-    """Group variables by their prefix or enabler status.
-    
-    Returns:
-      (groups, standalone) where groups is {prefix: [var_names]}
-    """
-    groups = OrderedDict()
-    standalone = []
-    
-    # First pass: identify all groups
-    for var_name, var in self.variables.items():
-      if var.group:
-        # This variable belongs to a group
-        if var.group not in groups:
-          groups[var.group] = []
-        groups[var.group].append(var_name)
-      else:
-        # Check if this is an enabler for other variables
-        # An enabler is a variable that other variables use as their group
-        is_group_enabler = any(v.group == var_name for v in self.variables.values())
-        if is_group_enabler:
-          if var_name not in groups:
-            groups[var_name] = []
-          # The enabler itself is not added to the group list
-        else:
-          # Truly standalone variable
-          standalone.append(var_name)
-    
-    return groups, standalone
-  
-  def _process_variable_set(self, display_name: str, var_names: List[str], 
-                            is_group: bool = False):
-    """Unified method to process any set of variables.
-    
-    Args:
-        display_name: Name to show in the header
-        var_names: List of variable names to process
-        is_group: Whether this is a group (vs standalone)
-    """
-    # Deduplicate variables
-    var_names = list(dict.fromkeys(var_names))  # Preserves order while removing duplicates
-    
-    # Get icon and description for this category
-    icon = self._get_category_icon(display_name)
-    description = self._get_category_description(display_name)
-    
-    # Check if this group has an enabler
-    group_name = display_name.lower()
-    enabler = None
-    if is_group and group_name in self.variables:
-      enabler_var = self.variables[group_name]
-      if enabler_var.is_enabler:
-        enabler = group_name
-        # Show section header with icon and description
-        header = f"\n{icon}[bold cyan]{display_name}[/bold cyan]"
-        if description:
-          header += f" [dim]- {description}[/dim]"
-        console.print(header)
-        console.print()  # Add newline after header
-        enabled = Confirm.ask(
-          f"Enable {enabler}?", 
-          default=bool(enabler_var.default)
-        )
-        self.values[enabler] = enabled
-        
-        if not enabled:
-          # Skip all group variables
-          return
-    
-    # Split into required and optional
-    required = []
-    optional = []
-    for var_name in var_names:
-      var = self.variables[var_name]
-      if var.is_required:
-        required.append(var_name)
-      else:
-        optional.append(var_name)
-    
-    # Apply defaults
-    for var_name in optional:
-      self.values[var_name] = self.variables[var_name].default
-    
-    # Process required variables
-    if required:
-      if not enabler:  # Show header only if we haven't shown it for enabler
-        header = f"\n{icon}[bold cyan]{display_name}[/bold cyan]"
-        if description:
-          header += f" [dim]- {description}[/dim]"
-        console.print(header)
-        console.print()  # Add newline after header
-      for var_name in required:
-        var = self.variables[var_name]
-        self.values[var_name] = self._prompt_variable(var, required=True)
-    
-    # Process optional variables
-    if optional:
-      # Filter out enabler variables from display
-      display_optional = [v for v in optional if v != enabler]
-      if display_optional:
-        # Show section header if not already shown
-        if not required and not enabler:
-          header = f"\n{icon}[bold cyan]{display_name}[/bold cyan]"
-          if description:
-            header += f" [dim]- {description}[/dim]"
-          console.print(header)
-          console.print()  # Add newline after header
-        
-        # Show current values with label
-        console.print("\n[white]Default values [/white]", end="")
-        self._show_variables_inline(display_optional)
-        
-        if Confirm.ask("Do you want to change any values?", default=False):
-          console.print()  # Add newline after prompt
-          for var_name in optional:
-            # Skip the enabler variable as it was already handled
-            if var_name == enabler:
-              continue
-            var = self.variables[var_name]
-            self.values[var_name] = self._prompt_variable(
-              var, current_value=self.values[var_name]
-            )
-  
-  def _show_variables_inline(self, var_names: List[str]):
-    """Display variables inline without header."""
-    items = []
-    for var_name in var_names:
-      var = self.variables[var_name]
-      value = self.values.get(var_name, var.default)
-      if value is not None:
-        # Format value based on type
-        if isinstance(value, bool):
-          formatted_value = str(value).lower()
-        elif isinstance(value, str) and ' ' in value:
-          formatted_value = f"'{value}'"
-        else:
-          formatted_value = str(value)
-        items.append(f"{var.display_name}: [cyan]({formatted_value})[/cyan]")
-    
-    if items:
-      console.print(f"[dim white]{', '.join(items)}[/dim white]")
-  
-  def _get_category_icon(self, category: str) -> str:
-    """Get icon for a category."""
-    # Only use icons from metadata
-    if self.category_metadata and category.lower() in self.category_metadata:
-      cat_meta = self.category_metadata[category.lower()]
-      if 'icon' in cat_meta:
-        return cat_meta['icon'] + ' '
-    return ''  # No icon if not defined in metadata
-  
-  def _get_category_description(self, category: str) -> str:
-    """Get description for a category."""
-    if self.category_metadata and category.lower() in self.category_metadata:
-      cat_meta = self.category_metadata[category.lower()]
-      if 'description' in cat_meta:
-        return cat_meta['description']
-    return ''  # No description if not defined in metadata
-  
-  def _prompt_variable(
-    self, 
-    var: Variable,
-    required: bool = False,
-    current_value: Any = None
-  ) -> Any:
-    """Prompt for a single variable value."""
-    # Build prompt message with description if available
-    display_text = var.description if var.description else var.display_name
-    
-    # Add hint if available
-    hint_text = ""
-    if var.hint:
-      hint_text = f" [dim]({var.hint})[/dim]"
-    
-    # Build the full prompt
-    if current_value is not None:
-      prompt_msg = f"Enter {display_text}{hint_text} [dim]({current_value})[/dim]"
-    elif required:
-      prompt_msg = f"Enter {display_text}{hint_text} [red](Required)[/red]"
-    else:
-      prompt_msg = f"Enter {display_text}{hint_text}"
-    
-    # Show tip if available
-    if var.tip:
-      console.print(f"[dim cyan]💡 {var.tip}[/dim cyan]")
-    
-    # Handle different types
-    if var.type == 'boolean':
-      default = bool(current_value) if current_value is not None else None
-      return Confirm.ask(prompt_msg, default=default)
-    
-    elif var.type == 'integer':
-      default = int(current_value) if current_value is not None else None
-      while True:
-        try:
-          return IntPrompt.ask(prompt_msg, default=default)
-        except ValueError:
-          console.print("[red]Please enter a valid integer[/red]")
-    
-    elif var.type == 'float':
-      default = float(current_value) if current_value is not None else None
-      while True:
-        try:
-          return FloatPrompt.ask(prompt_msg, default=default)
-        except ValueError:
-          console.print("[red]Please enter a valid number[/red]")
-    
-    else:  # string
-      default = str(current_value) if current_value is not None else None
-      while True:
-        value = Prompt.ask(prompt_msg, default=default) or ""
-        if required and not value.strip():
-          console.print("[red]This field is required[/red]")
-          continue
-        return value.strip()

+ 41 - 7
cli/core/template.py

@@ -6,7 +6,7 @@ import re
 from jinja2 import Environment, BaseLoader, meta, nodes, TemplateSyntaxError
 import frontmatter
 from .exceptions import TemplateValidationError
-from .variables import Variable, analyze_template_variables
+# Module variables will be handled by the module's VariableRegistry
 
 
 @dataclass
@@ -37,7 +37,6 @@ class Template:
   # Template variable analysis results
   vars: Set[str] = field(default_factory=set, init=False)
   var_defaults: Dict[str, Any] = field(default_factory=dict, init=False)
-  variables: Dict[str, Variable] = field(default_factory=dict, init=False)  # Analyzed variables
   
   def __post_init__(self):
     """Initialize computed properties after dataclass initialization."""
@@ -53,10 +52,6 @@ class Template:
     
     # Parse template variables
     self.vars, self.var_defaults = self._parse_template_variables(self.content)
-    # Analyze variables to create TemplateVariable objects
-    self.variables = analyze_template_variables(
-      self.vars, self.var_defaults, self.content
-    )
   
   @staticmethod
   def _create_jinja_env() -> Environment:
@@ -153,9 +148,12 @@ class Template:
       logging.getLogger('boilerplates').debug(f"Error parsing template variables: {e}")
       return set(), {}
 
-  def validate(self) -> List[str]:
+  def validate(self, module_variable_metadata: Dict[str, Dict[str, Any]] = None) -> List[str]:
     """Validate template integrity.
     
+    Args:
+        module_variable_metadata: Module's variable metadata for validation
+    
     Returns:
         List of validation error messages. Empty list if valid.
     
@@ -173,6 +171,11 @@ class Template:
     except Exception as e:
       raise TemplateValidationError(self.id, [f"Template parsing error: {str(e)}"])
     
+    # Validate variable definitions (critical - should raise immediately)
+    undefined_vars = self._validate_variable_definitions(module_variable_metadata or {})
+    if undefined_vars:
+      raise TemplateValidationError(self.id, undefined_vars)
+    
     # All variables are now auto-detected, no need to check for undefined
     # The template parser will have found all variables used
     
@@ -189,6 +192,37 @@ class Template:
     
     return errors
 
+  def update_variables_with_module_metadata(self, module_variable_registry) -> None:
+    """Update template variables with module variable registry.
+    
+    Args:
+        module_variable_registry: Module's VariableRegistry instance
+    """
+    # This method is kept for compatibility but simplified
+    # Variables are now managed directly by the VariableRegistry
+    pass
+
+  def _validate_variable_definitions(self, module_variable_registry) -> List[str]:
+    """Validate that all template variables are properly defined.
+    
+    Args:
+        module_variable_registry: Module's VariableRegistry instance
+    
+    Returns:
+        List of error messages for undefined variables
+    """
+    errors = []
+    
+    # For now, simplified validation - just check template-specific variables
+    # Module variables are validated by the VariableRegistry itself
+    for var_name in self.vars:
+      if var_name.startswith('template.'):
+        # Template-specific variables must be defined in frontmatter
+        if var_name not in self.variable_metadata:
+          errors.append(f"Template variable '{var_name}' must be defined in frontmatter 'variables' section")
+    
+    return errors
+
   def render(self, variable_values: Dict[str, Any]) -> str:
     """Render the template with the provided variable values."""
     logger = logging.getLogger('boilerplates')

+ 76 - 98
cli/core/variables.py

@@ -1,120 +1,98 @@
-from typing import Any, Dict, List, Optional, Set
+from typing import Any, Dict, List, Optional
 from dataclasses import dataclass, field
+from enum import Enum
+
+
+class VariableType(Enum):
+  """Supported variable types."""
+  STR = "str"
+  INT = "int" 
+  BOOL = "bool"
+  ENUM = "enum"
+  FLOAT = "float"
 
 
 @dataclass
 class Variable:
-  """Variable detected from template analysis.
+  """Represents a single variable with metadata."""
   
-  Represents a variable found in a template with all its properties:
-  - Simple variables: service_name, container_name
-  - Dotted variables: traefik.host, network.name, service_port.http
-  - Enabler variables: Variables used in {% if var %} conditions
-  """
   name: str
+  type: VariableType = VariableType.STR
+  description: str = ""
+  display: str = ""  # Display name for UI
   default: Any = None
-  type: str = "string"  # string, integer, float, boolean (inferred from default or usage)
-  
-  # Variable characteristics
-  is_enabler: bool = False  # Used in {% if %} conditions
-  
-  # Grouping info (extracted from dotted notation)
-  group: Optional[str] = None  # e.g., 'traefik' for 'traefik.host'
+  options: List[str] = field(default_factory=list)  # For enum types
+  parent: Optional[str] = None  # Parent variable name (for dotted notation)
   
-  # Metadata for enhanced UX
-  description: Optional[str] = None  # Override for variable description
-  hint: Optional[str] = None  # Helpful hint shown during input
-  tip: Optional[str] = None  # Additional tip or best practice
-  icon: Optional[str] = None  # Icon for this specific variable
-  validation: Optional[str] = None  # Regex pattern for validation
+  def has_parent(self) -> bool:
+    """Check if this variable has a parent."""
+    return self.parent is not None
   
-  @property
-  def display_name(self) -> str:
-    """Get display name for prompts."""
-    if self.group:
-      # Remove group prefix for display
-      return self.name.replace(f"{self.group}.", "").replace(".", " ")
-    return self.name.replace(".", " ")
-  
-  @property 
-  def is_required(self) -> bool:
-    """Check if variable is required (no default value)."""
-    return self.default is None
+  def get_full_name(self) -> str:
+    """Get the full dotted name."""
+    if self.parent:
+      return f"{self.parent}.{self.name}"
+    return self.name
 
 
-def analyze_template_variables(
-  vars_used: Set[str],
-  var_defaults: Dict[str, Any],
-  template_content: str
-) -> Dict[str, Variable]:
-  """Analyze template variables and create TemplateVariable objects.
-  
-  Args:
-    vars_used: Set of all variable names used in template
-    var_defaults: Dict of variable defaults from template
-    template_content: The raw template content for additional analysis
-  
-  Returns:
-    Dict mapping variable name to Variable object
-  """
-  variables = {}
+class VariableRegistry:
+  """Registry for managing module variables."""
   
-  # Detect enabler variables (used in {% if %} conditions)
-  enablers = _detect_enablers(template_content)
+  def __init__(self):
+    self._variables: Dict[str, Variable] = {}  # Full name -> Variable
   
-  for var_name in vars_used:
-    var = Variable(
-      name=var_name,
-      default=var_defaults.get(var_name)
-    )
+  def register_variable(self, variable: Variable) -> Variable:
+    """Register a variable in the registry."""
+    full_name = variable.get_full_name()
     
-    # Detect if it's an enabler
-    var.is_enabler = var_name in enablers
+    # Convert string type to enum if needed
+    if isinstance(variable.type, str):
+      try:
+        variable.type = VariableType(variable.type.lower())
+      except ValueError:
+        variable.type = VariableType.STR
     
-    # Infer type from default value
-    if var.default is not None:
-      if isinstance(var.default, bool):
-        var.type = "boolean"
-      elif isinstance(var.default, int):
-        var.type = "integer"
-      elif isinstance(var.default, float):
-        var.type = "float"
-      else:
-        var.type = "string"
+    # Validate enum options
+    if variable.type == VariableType.ENUM and not variable.options:
+      raise ValueError(f"Variable '{full_name}' of type 'enum' must have options")
     
-    # If it's an enabler without a default, assume boolean
-    if var.is_enabler and var.default is None:
-      var.type = "boolean"
-      var.default = False  # Default enablers to False
-    
-    # Detect group from dotted notation
-    if '.' in var_name:
-      var.group = var_name.split('.')[0]
-    
-    variables[var_name] = var
+    self._variables[full_name] = variable
+    return variable
   
-  return variables
-
-
-def _detect_enablers(template_content: str) -> Set[str]:
-  """Detect variables used as enablers in {% if %} conditions.
+  def get_variable(self, name: str) -> Optional[Variable]:
+    """Get variable by full name."""
+    return self._variables.get(name)
   
-  Args:
-    template_content: The raw template content
+  def get_all_variables(self) -> Dict[str, Variable]:
+    """Get all registered variables."""
+    return self._variables.copy()
   
-  Returns:
-    Set of variable names that are used as enablers
-  """
-  import re
-  enablers = set()
+  def get_parent_variables(self) -> List[Variable]:
+    """Get all variables that have children (enabler variables)."""
+    parent_names = set()
+    for var in self._variables.values():
+      if var.parent:
+        parent_names.add(var.parent)
+    
+    return [self._variables[name] for name in parent_names if name in self._variables]
   
-  # Find variables used in {% if var %} patterns
-  # This catches: {% if var %}, {% if not var %}, {% if var and ... %}
-  if_pattern = re.compile(r'{%\s*if\s+(not\s+)?(\w+)(?:\s|%)', re.MULTILINE)
-  for match in if_pattern.finditer(template_content):
-    var_name = match.group(2)
-    # Skip Jinja2 keywords
-    if var_name not in ['true', 'false', 'none', 'True', 'False', 'None']:
-      enablers.add(var_name)
+  def get_children_of(self, parent_name: str) -> List[Variable]:
+    """Get all child variables of a specific parent."""
+    return [var for var in self._variables.values() if var.parent == parent_name]
   
-  return enablers
+  def validate_parent_child_relationships(self) -> List[str]:
+    """Validate that all parent-child relationships are consistent."""
+    errors = []
+    
+    for var in self._variables.values():
+      if var.parent:
+        # Check if parent exists
+        if var.parent not in self._variables:
+          errors.append(f"Variable '{var.get_full_name()}' references non-existent parent '{var.parent}'")
+        else:
+          parent_var = self._variables[var.parent]
+          # Parent should generally be boolean if it has children
+          if parent_var.type != VariableType.BOOL:
+            errors.append(f"Parent variable '{var.parent}' should be boolean type (has children)")
+    
+    return errors

+ 159 - 87
cli/modules/compose.py

@@ -1,5 +1,6 @@
 from ..core.module import Module
 from ..core.registry import registry
+from ..core.variables import Variable, VariableRegistry, VariableType
 
 class ComposeModule(Module):
   """Docker Compose module."""
@@ -8,93 +9,164 @@ class ComposeModule(Module):
   description = "Manage Docker Compose configurations"
   files = ["docker-compose.yml", "compose.yml", "compose.yaml"]
   
-  # Category metadata
-  categories = {
-    "general": {
-      "icon": "󰖷 ",
-      "description": "General container settings"
-    },
-    "network": {
-      "icon": "󰈀 ",
-      "description": "Network configuration",
-      "tip": "Use external networks for cross-container communication"
-    },
-    "traefik": {
-      "icon": " ",
-      "description": "Reverse proxy and load balancer",
-      "tip": "Automatic SSL certificates with Let's Encrypt"
-    },
-    "swarm": {
-      "icon": " ",
-      "description": "Docker Swarm orchestration"
-    }
-  }
-  
-  # Variable metadata
-  variable_metadata = {
-    "service_name": {
-      "hint": "e.g., webapp, api, database",
-      "validation": "^[a-z][a-z0-9-]*$"
-    },
-    "container_name": {
-      "hint": "Leave empty to use service name",
-      "description": "Custom container name"
-    },
-    "network": {
-      "description": "Enable custom network configuration"
-    },
-    "network.name": {
-      "hint": "e.g., frontend, backend, bridge",
-      "description": "Docker network name"
-    },
-    "network.external": {
-      "hint": "Use 'true' for existing networks",
-      "tip": "External networks must be created before running"
-    },
-    "traefik": {
-      "description": "Enable Traefik reverse proxy",
-      "tip": "Requires Traefik to be running separately"
-    },
-    "traefik.host": {
-      "hint": "e.g., app.example.com, api.mydomain.org",
-      "description": "Domain name for your service",
-      "validation": "^[a-z0-9][a-z0-9.-]*[a-z0-9]$"
-    },
-    "traefik.tls": {
-      "description": "Enable HTTPS/TLS",
-      "tip": "Requires valid domain and DNS configuration"
-    },
-    "traefik.certresolver": {
-      "hint": "e.g., letsencrypt, staging",
-      "description": "Certificate resolver name"
-    },
-    "swarm": {
-      "description": "Enable Docker Swarm mode",
-      "tip": "Requires Docker Swarm to be initialized"
-    },
-    "swarm.replicas": {
-      "hint": "Number of container instances",
-      "validation": "^[1-9][0-9]*$"
-    },
-    "service_port_http": {
-      "hint": "e.g., 8080, 3000, 80",
-      "description": "HTTP port mapping",
-      "validation": "^[1-9][0-9]{0,4}$"
-    },
-    "service_port_https": {
-      "hint": "e.g., 8443, 3443, 443",
-      "description": "HTTPS port mapping",
-      "validation": "^[1-9][0-9]{0,4}$"
-    },
-    "nginx_dashboard": {
-      "description": "Enable Nginx status dashboard"
-    },
-    "nginx_dashboard_port_dashboard": {
-      "hint": "e.g., 8081, 9090",
-      "description": "Dashboard port",
-      "validation": "^[1-9][0-9]{0,4}$"
-    }
-  }
+  def _init_variables(self):
+    """Initialize module-specific variables."""
+    self.variables = VariableRegistry()
+    
+    # Register root variables
+    self.variables.register_variable(Variable(
+      name="service_name",
+      type=VariableType.STR,
+      description="Service name",
+      display="Service Name"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="container_name",
+      type=VariableType.STR,
+      description="Custom container name (leave empty to use service name)",
+      display="Container Name"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="container_timezone",
+      type=VariableType.STR,
+      description="Container timezone (e.g., Europe/Berlin, America/New_York)",
+      display="Container Timezone"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="container_loglevel",
+      type=VariableType.ENUM,
+      description="Container log level",
+      display="Log Level",
+      default="info",
+      options=["debug", "info", "warn", "error"]
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="restart_policy",
+      type=VariableType.ENUM,
+      description="Container restart policy",
+      display="Restart Policy",
+      default="unless-stopped",
+      options=["unless-stopped", "always", "on-failure", "no"]
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="ports",
+      type=VariableType.BOOL,
+      description="Enable port mapping",
+      display="Enable Ports"
+    ))
+    
+    # Network variables
+    self.variables.register_variable(Variable(
+      name="network",
+      type=VariableType.BOOL,
+      description="Enable custom network configuration",
+      display="Enable Network"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="name",
+      type=VariableType.STR,
+      description="Docker network name (e.g., frontend, backend, bridge)",
+      display="Network Name",
+      default="bridge",
+      parent="network"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="external",
+      type=VariableType.BOOL,
+      description="Use existing network (must be created before running)",
+      display="External Network",
+      parent="network"
+    ))
+    
+    # Traefik variables
+    self.variables.register_variable(Variable(
+      name="traefik",
+      type=VariableType.BOOL,
+      description="Enable Traefik reverse proxy (requires Traefik to be running separately)",
+      display="Enable Traefik"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="host",
+      type=VariableType.STR,
+      description="Domain name for your service (e.g., app.example.com)",
+      display="Host Domain",
+      parent="traefik"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="entrypoint",
+      type=VariableType.STR,
+      description="HTTP entrypoint for non-TLS traffic (e.g., web, http)",
+      display="HTTP Entrypoint",
+      default="web",
+      parent="traefik"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="tls",
+      type=VariableType.BOOL,
+      description="Enable HTTPS/TLS (requires valid domain and DNS configuration)",
+      display="Enable TLS",
+      parent="traefik"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="entrypoint",
+      type=VariableType.STR,
+      description="TLS entrypoint for HTTPS traffic (e.g., websecure, https)",
+      display="TLS Entrypoint",
+      default="websecure",
+      parent="traefik.tls"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="certresolver",
+      type=VariableType.STR,
+      description="Certificate resolver name (e.g., letsencrypt, staging)",
+      display="Cert Resolver",
+      parent="traefik.tls"
+    ))
+    
+    # PostgreSQL variables
+    self.variables.register_variable(Variable(
+      name="postgres",
+      type=VariableType.BOOL,
+      description="Enable PostgreSQL database",
+      display="Enable PostgreSQL"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="host",
+      type=VariableType.STR,
+      description="PostgreSQL host (e.g., localhost, postgres, db.example.com)",
+      display="PostgreSQL Host",
+      parent="postgres"
+    ))
+    
+    # Docker Swarm variables
+    self.variables.register_variable(Variable(
+      name="swarm",
+      type=VariableType.BOOL,
+      description="Enable Docker Swarm mode (requires Docker Swarm to be initialized)",
+      display="Enable Swarm"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="replicas",
+      type=VariableType.INT,
+      description="Number of container instances",
+      display="Replicas",
+      default=1,
+      parent="swarm"
+    ))
 
 # Register the module
 registry.register(ComposeModule)

+ 31 - 17
library/compose/n8n/compose.yaml

@@ -9,12 +9,16 @@ tags:
   - automation
   - workflows
   - compose
+variables:
+  template.custom_config:
+    description: "Custom configuration for n8n"
+    hint: "Additional environment variables or settings"
+    type: "string"
+    default: ""
 ---
-{% set postgres.always = 'True' %}
 services:
-  {{ service_name | default('n8n') }}:
+  {{ service_name }}:
     image: n8nio/n8n:1.110.1
-    container_name: {{ container_name | default('n8n') }}
     environment:
       - N8N_LOG_LEVEL={{ container_loglevel | default('info') }}
       - GENERIC_TIMEZONE={{ container_timezone }}
@@ -22,15 +26,15 @@ services:
       - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
       - N8N_RUNNERS_ENABLED=true
       {% if traefik %}
-      {% if traefik_tls %}
-      - N8N_EDITOR_BASE_URL=https://{{ traefik_host }}
+      {% if traefik.tls %}
+      - N8N_EDITOR_BASE_URL=https://{{ traefik.host }}
       {% else %}
-      - N8N_EDITOR_BASE_URL=http://{{ traefik_host }}
+      - N8N_EDITOR_BASE_URL=http://{{ traefik.host }}
       {% endif %}
       {% endif %}
       {% if postgres %}
       - DB_TYPE=postgresdb
-      - DB_POSTGRESDB_HOST={{ postgres_host }}
+      - DB_POSTGRESDB_HOST={{ postgres.host }}
       - DB_POSTGRESDB_PORT=${DB_POSTGRESDB_PORT:-5432}
       - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
       - DB_POSTGRESDB_USER=${POSTGRES_USER}
@@ -39,25 +43,35 @@ services:
     volumes:
       - /etc/localtime:/etc/localtime:ro
       - data:/home/node/.n8n
+    {% if network %}
     networks:
-      - {{ docker_network | default('bridge') }}
+      - {{ network.name | default('bridge') }}
+    {% endif %}
     {% 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
+      - traefik.enable={{ traefik | default('true') }}
+      - traefik.http.routers.{{ service_name }}.rule=Host(`{{ traefik.host }}`)
+      {% if traefik.tls %}
+      - traefik.http.routers.{{ service_name }}.entrypoints={{ traefik.tls.entrypoint | default('websecure') }}
+      - traefik.http.routers.{{ service_name }}.tls=true
+      - traefik.http.routers.{{ service_name }}.tls.certresolver={{ traefik.tls.certresolver }}
+      {% else %}
+      - traefik.http.routers.{{ service_name }}.entrypoints={{ traefik.entrypoint | default('web') }}
       {% endif %}
+      - traefik.http.services.{{ service_name }}.loadbalancer.server.port=5678
     {% endif %}
-    restart: {{ restart_policy }}
+    restart: {{ restart_policy | default('unless-stopped') }}
+    {% if ports %}
 
 volumes:
   data:
     driver: local
 
+{% if network %}
 networks:
-  {{ docker_network | default('bridge') }}:
+  {{ network.name | default('bridge') }}:
+  {% if network.external %}
     external: true
+  {% endif %}
+{% endif %}
+{% endif %}