xcad 5 månader sedan
förälder
incheckning
fc3408241e
9 ändrade filer med 481 tillägg och 638 borttagningar
  1. 49 0
      WARP.md
  2. 1 1
      cli/__main__.py
  3. 47 164
      cli/core/module.py
  4. 44 148
      cli/core/processor.py
  5. 194 78
      cli/core/prompt.py
  6. 13 50
      cli/core/registry.py
  7. 69 125
      cli/core/variables.py
  8. 50 60
      cli/modules/compose.py
  9. 14 12
      cli/modules/terraform.py

+ 49 - 0
WARP.md

@@ -105,6 +105,55 @@ tags:
 - **Comment Anchors**: Use for TODOs, FIXMEs, notes, and links in source code
 - **Spaces in Python**: Prefer using 2 Spaces for indentation
 
+## Architecture Optimization (2025-09-07)
+
+The codebase has been optimized following the ARCHITECTURE_OPTIMIZATION.md plan:
+
+### Simplified Variable System
+- Reduced from 3 classes (Variable, VariableGroup, VariableManager) to 2 classes (Variable, VariableRegistry)
+- Removed complex enable/disable logic
+- Streamlined data transformations
+
+### Streamlined Module System
+- Removed decorator pattern (@register_module)
+- Direct module registration with registry.register()
+- Class attributes instead of runtime __init__ modification
+- Simplified module implementation
+
+### Clean Registry
+- Removed runtime __init__ modifications
+- Simple explicit registration
+- No decorator magic
+
+### Module Implementation Pattern
+```python
+from ..core.module import Module
+from ..core.registry import registry
+from ..core.variables import Variable
+
+class ExampleModule(Module):
+  """Module description."""
+  
+  name = "example"
+  description = "Manage example configurations"
+  files = ["example.conf", "example.yaml"]
+  
+  def _init_variables(self):
+    """Initialize module-specific variables."""
+    # Register groups
+    self.variables.register_group("general", "General Settings")
+    
+    # Register variables
+    self.variables.register_variable(Variable(
+      name="var_name",
+      description="Variable description",
+      group="general"
+    ))
+
+# Register the module
+registry.register(ExampleModule)
+```
+
 ## Configuration
 
 - YAML linting configured with max 160 character line length

+ 1 - 1
cli/__main__.py

@@ -55,7 +55,7 @@ def init_app():
     for module in registry.create_instances():
       try:
         logger.debug(f"Registering module: {module.__class__.__name__}")
-        module.register(app)
+        module.register_cli(app)
       except Exception as e:
         logger.error(f"Error registering {module.__class__.__name__}: {e}")
     

+ 47 - 164
cli/core/module.py

@@ -1,149 +1,61 @@
-from abc import ABC, abstractmethod
+from abc import ABC
 from pathlib import Path
-from typing import Any, Dict, Optional, Tuple, List
+from typing import List, Optional
 import logging
 from typer import Typer, Option, Argument
 from rich.console import Console
-from rich.panel import Panel
-from rich.text import Text
-from rich.syntax import Syntax
-from rich.table import Table
-from io import StringIO
-from rich import box
-
 from .library import LibraryManager
-from .prompt import PromptHandler
-from .template import Template
-from .variables import VariableGroup, VariableManager
-from .config import ConfigManager
+from .variables import VariableRegistry
 from .processor import VariableProcessor
 
 logger = logging.getLogger('boilerplates')
 
 
 class Module(ABC):
-  """
-  Base Module for all CLI Commands.
+  """Simplified base module with clearer responsibilities."""
   
-  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
+  # Class attributes set by subclasses
+  name: str = None
+  description: str = None  
+  files: List[str] = None
+  
+  def __init__(self):
+    # Validate required attributes
+    if not all([self.name, self.description, self.files]):
+      raise ValueError(f"Module {self.__class__.__name__} must define name, description, and files")
     
-    # Initialize ConfigManager and VariableManager with it
-    self.config = ConfigManager()
-    self.vars = VariableManager(vars if vars is not None else [], self.config)
-
     self.app = Typer()
-    self.libraries = LibraryManager()  # Initialize library manager
+    self.libraries = LibraryManager()
+    self.variables = VariableRegistry()
     
-    # 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")
-
-  def _validate_variables(self, variables: List[str]) -> Tuple[bool, List[str]]:
-    """Validate if all template variables exist in the variable groups.
+    # Allow subclasses to initialize their variables
+    self._init_variables()
     
-    Args:
-        variables: List of variable names to validate
-        
-    Returns:
-        Tuple of (success: bool, missing_variables: List[str])
-    """
-    missing_variables = [var for var in variables if not self.vars.has_variable(var)]
-    success = len(missing_variables) == 0
-    return success, missing_variables
+  def _init_variables(self):
+    """Override in subclasses to register module-specific variables."""
+    pass
 
-  def _get_variable_defaults_for_template(self, template_vars: List[str]) -> Dict[str, Any]:
-    """Get default values for variables used in a template.
-    
-    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.vars.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 _get_groups_with_template_vars(self, template_vars: List[str]) -> List[VariableGroup]:
-    """Get groups that contain at least one template variable.
+
+  def _get_groups_with_template_vars(self, template_vars: List[str]) -> List[str]:
+    """Get group names that contain at least one template variable.
     
     Args:
         template_vars: List of variable names used in the template
         
     Returns:
-        List of VariableGroup objects that have variables used by the template
+        List of group names that have variables used by the template
     """
-    result = []
-    for group in self.vars.variable_groups:
-      if any(var.name in template_vars for var in group.vars):
-        result.append(group)
-    return result
+    grouped_vars = self.variables.get_variables_for_template(template_vars)
+    return list(grouped_vars.keys())
 
-  def _resolve_variable_defaults(self, template_vars: List[str], template_defaults: Dict[str, Any] = None) -> Dict[str, Any]:
-    """Resolve variable default values with priority handling.
-    
-    Priority order:
-    1. Module variable defaults (low priority)
-    2. Template's built-in defaults (medium priority)  
-    3. User config defaults (high priority)
-    """
-    if template_defaults is None:
-      template_defaults = {}
-    
-    # Start with module defaults, then override with template and user config
-    defaults = self._get_variable_defaults_for_template(template_vars)
-    defaults.update(template_defaults)
-    defaults.update({var: value for var, value in self.config.get_variable_defaults(self.name).items() if var in template_vars})
-    
-    return defaults
 
-  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."""
-    filtered_vars = {}
-    template_vars_set = set(template_vars)  # Convert to set for O(1) lookup
-    
-    for group in self._get_groups_with_template_vars(template_vars):
-      # Get variables that match template vars and convert to dict format
-      group_vars = {
-        var.name: var.to_dict() for var in group.vars if var.name in template_vars_set
-      }
-      
-      # Only include groups that have variables
-      if group_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 list(self):
-    """List all templates in the module."""
-    logger.debug(f"Listing templates for module: {self.name}")
+    """List all templates."""
     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})")
+      print(f"{template.id} - {template.name}")
     return templates
 
   def show(self, id: str = Argument(..., metavar="template", help="The template to show details for")):
@@ -178,9 +90,7 @@ class Module(ABC):
       metadata.append(f"Tags: [cyan]{', '.join(template.tags)}[/cyan]")
     
     # Find variable groups used by this template
-    template_var_groups = [
-      group.name for group in self._get_groups_with_template_vars(template.vars)
-    ]
+    template_var_groups = self._get_groups_with_template_vars(template.vars)
     
     if template_var_groups:
       metadata.append(f"Functions: [cyan]{', '.join(template_var_groups)}[/cyan]")
@@ -194,60 +104,33 @@ class Module(ABC):
       console.print(f"\n{template.content}")
 
 
-  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"""
-
-    # Find template by ID
-    template = self.libraries.find_by_id(module_name=self.name, files=self.files, template_id=id)
+  def generate(self, id: str = Argument(..., help="Template ID"),
+              out: Optional[Path] = Option(None, "--out", "-o")):
+    """Generate from template."""
+    # Find template
+    template = self.libraries.find_by_id(self.name, self.files, id)
     if not template:
       print(f"Template '{id}' not found.")
       return
-
-    # Validate if the variables in the template are valid ones
-    success, missing = self._validate_variables(template.vars)
-    if not success:
-      print(f"Template '{id}' has invalid variables: {missing}")
-      return
     
-    # Process variables using dedicated processor
-    try:
-      processor = VariableProcessor(self.vars, self.config, self.name)
-      final_variable_values = processor.process_variables_for_template(template)
-      logger.debug(f"Variable processing completed with {len(final_variable_values)} variables")
-      
-    except KeyboardInterrupt:
-      print("\n[red]Template generation cancelled.[/red]")
-      return
-    except Exception as e:
-      print(f"Error during variable prompting: {e}")
-      return
+    # Process variables
+    processor = VariableProcessor(self.variables)
+    values = processor.process(template)
     
-    # Step 7: Generate template with final variable values
-    try:
-      generated_content = template.render(final_variable_values)
-    except Exception as e:
-      print(f"Error rendering template: {e}")
-      return
+    # Render and output
+    content = template.render(values)
     
-    # Step 8: Output the 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}")
+      out.parent.mkdir(parents=True, exist_ok=True)
+      out.write_text(content)
+      print(f"✅ Generated to {out}")
     else:
-      print("\n" + "="*60)
-      print("📄 Generated Template Content:")
-      print("="*60)
-      print(generated_content)
+      print(f"\n{'='*60}\nGenerated Content:\n{'='*60}")
+      print(content)
   
-  def register(self, app: Typer):
+  def register_cli(self, app: Typer):
+    """Register this module with the CLI app."""
     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)
+    app.add_typer(self.app, name=self.name, help=self.description)

+ 44 - 148
cli/core/processor.py

@@ -1,7 +1,6 @@
 from typing import Any, Dict, List
 import logging
-from .variables import VariableManager
-from .config import ConfigManager
+from .variables import VariableRegistry
 from .template import Template
 from .prompt import PromptHandler
 
@@ -9,155 +8,52 @@ logger = logging.getLogger('boilerplates')
 
 
 class VariableProcessor:
-  """
-  Handles the complete variable population pipeline for template generation.
+  """Variable processor for template generation."""
   
-  This class implements a clean, step-by-step approach to:
-  1. Filter variables relevant to the template
-  2. Resolve defaults with proper priority handling  
-  3. Prompt users for missing values
-  4. Return final variable values for template rendering
-  """
+  def __init__(self, variable_registry: VariableRegistry):
+    self.registry = variable_registry
   
-  def __init__(self, vars_manager: VariableManager, config_manager: ConfigManager, module_name: str = None):
-    """Initialize the processor with required managers."""
-    self.vars = vars_manager
-    self.config = config_manager
-    self.module_name = module_name
-    self.logger = logging.getLogger('boilerplates')
-  
-  def process_variables_for_template(self, template: Template) -> Dict[str, Any]:
-    """
-    Execute the complete variable processing pipeline.
-    
-    Args:
-        template: Template object containing variables and defaults
-        
-    Returns:
-        Dictionary of final variable values ready for template rendering
-    """
-    self.logger.debug("Starting variable processing pipeline")
-    
-    # Step 1: Filter and prepare variables for this template
-    self._prepare_variables_for_template(template)
-    
-    # Step 2: Resolve defaults in priority order
-    resolved_defaults = self._resolve_defaults(template)
-    
-    # Step 3: Handle user interaction and prompting
-    final_values = self._prompt_for_values(template, resolved_defaults)
-    
-    self.logger.debug(f"Variable processing completed with {len(final_values)} variables")
-    return final_values
-  
-  def _prepare_variables_for_template(self, template: Template) -> None:
-    """
-    Step 1: Disable variables not needed by the template.
-    
-    This optimizes the user experience by only showing relevant variables.
-    """
-    self.logger.debug(f"Filtering variables for template with {len(template.vars)} required variables")
-    
-    disabled_count = 0
-    for var_group in self.vars.get_all_groups():
-      for var in var_group.get_all_vars():
-        if var.name not in template.vars:
-          var.disable()
-          disabled_count += 1
-    
-    self.logger.debug(f"Disabled {disabled_count} variables not needed by template")
-  
-  def _resolve_defaults(self, template: Template) -> Dict[str, Any]:
-    """
-    Step 2: Resolve variable defaults with proper priority handling.
-    
-    Priority order (low to high):
-    1. Module variable defaults
-    2. Template built-in defaults 
-    3. User configuration defaults
+  def process(self, template: Template) -> Dict[str, Any]:
+    """Process variables for a template."""
+    
+    # Get variables needed by template
+    grouped_vars = self.registry.get_variables_for_template(template.vars)
+    
+    if not grouped_vars:
+      return {}
+    
+    # Convert to format expected by PromptHandler
+    formatted_groups = {}
+    for group_name, variables in grouped_vars.items():
+      group_info = self.registry.groups.get(group_name, {
+        'display_name': group_name.title(),
+        'description': '',
+        'icon': ''
+      })
+      
+      # Convert variables to dict format expected by PromptHandler
+      vars_dict = {}
+      for var in variables:
+        vars_dict[var.name] = var.to_prompt_config()
+      
+      formatted_groups[group_name] = {
+        'display_name': group_info['display_name'],
+        'description': group_info['description'],
+        'icon': group_info['icon'],
+        'vars': vars_dict,
+        'enabler': self.registry.group_enablers.get(group_name, '')
+      }
     
-    Returns:
-        Dictionary of resolved default values
-    """
-    self.logger.debug("Resolving variable defaults with priority handling")
+    # Resolve defaults (template defaults override variable defaults)
     defaults = {}
+    for group_vars in grouped_vars.values():
+      for var in group_vars:
+        if var.default is not None:
+          defaults[var.name] = var.default
     
-    # Priority 1: Module variable defaults (lowest priority)
-    for group in self.vars.get_all_groups():
-      for var in group.get_all_vars():
-        if var.name in template.vars and var.value is not None:
-          defaults[var.name] = var.value
-          self.logger.debug(f"Set module default for '{var.name}': {var.value}")
-    
-    # Priority 2: Template defaults (medium priority)
-    for var_name, default_value in template.var_defaults.items():
-      if var_name in template.vars:
-        defaults[var_name] = default_value
-        self.logger.debug(f"Set template default for '{var_name}': {default_value}")
-    
-    # Priority 3: User config defaults (highest priority)
-    user_defaults = self.config.get_variable_defaults(self.module_name or "unknown")
-    for var_name, default_value in user_defaults.items():
-      if var_name in template.vars:
-        defaults[var_name] = default_value
-        self.logger.debug(f"Set user config default for '{var_name}': {default_value}")
-    
-    self.logger.debug(f"Resolved {len(defaults)} default values")
-    return defaults
-  
-  def _prompt_for_values(self, template: Template, defaults: Dict[str, Any]) -> Dict[str, Any]:
-    """
-    Step 3: Handle user prompting for variable values.
-    
-    Args:
-        template: Template object
-        defaults: Resolved default values
-        
-    Returns:
-        Dictionary of final variable values
-    """
-    self.logger.debug("Starting user prompting phase")
-    
-    # Filter variable groups to only include those needed by the template
-    filtered_groups = self._filter_variables_for_template(template.vars)
-    
-    # Create and execute prompt handler
-    prompt_handler = PromptHandler(filtered_groups, defaults)
-    final_values = prompt_handler()
-    
-    self.logger.debug(f"User prompting completed with {len(final_values)} final values")
-    return final_values
-  
-  def _filter_variables_for_template(self, template_vars: List[str]) -> Dict[str, Any]:
-    """
-    Filter variable groups to only include variables needed by the template.
-    
-    This is adapted from the existing method in the Module class.
-    """
-    filtered_vars = {}
-    template_vars_set = set(template_vars)  # Convert to set for O(1) lookup
-    
-    for group in self.vars.get_all_groups():
-      # Only process enabled groups
-      if not group.enabled:
-        continue
-        
-      # Get variables that match template vars and are enabled
-      group_vars = {
-        var.name: var.to_dict() 
-        for var in group.vars 
-        if var.name in template_vars_set and var.enabled
-      }
-      
-      # Only include groups that have variables
-      if group_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
-        }
+    # Template defaults have higher priority
+    defaults.update(template.var_defaults)
     
-    self.logger.debug(f"Filtered to {len(filtered_vars)} variable groups for template")
-    return filtered_vars
+    # Prompt for values using the PromptHandler
+    prompt = PromptHandler(formatted_groups, defaults)
+    return prompt()  # Call the handler directly

+ 194 - 78
cli/core/prompt.py

@@ -31,7 +31,17 @@ class PromptHandler:
     logger.debug(f"Starting advanced prompt handler with {len(self.variable_groups)} variable groups")
 
     # Process each variable group with the complex logic
+    # Maintain order by processing 'general' group first if it exists
+    ordered_groups = []
+    if 'general' in self.variable_groups:
+      ordered_groups.append(('general', self.variable_groups['general']))
+    
+    # Add remaining groups in their original order
     for group_name, group_data in self.variable_groups.items():
+      if group_name != 'general':
+        ordered_groups.append((group_name, group_data))
+    
+    for group_name, group_data in ordered_groups:
       self._process_variable_group(group_name, group_data)
     
     self._show_summary()
@@ -51,40 +61,51 @@ class PromptHandler:
     if not variables:
       return
 
-    # Show group header
-    self.console.print(f"[bold cyan]{group_name.title()} Variables[/bold cyan]")
-    self.console.print()
-
-    # Step 1: Check for variables with no default values (always prompt)
+    # Show compact group header only if there are variables to configure
     vars_without_defaults = self._get_variables_without_defaults(variables)
+    vars_with_defaults = self._get_variables_with_defaults(variables)
     
-    # Step 2: Determine if group should be enabled
-    group_enabled = self._determine_group_enabled_status(group_name, variables, vars_without_defaults)
+    # Only show header if we need user interaction  
+    if not (vars_without_defaults or vars_with_defaults):
+      return
+      
+    # Use icon from group configuration
+    group_icon = group_data.get('icon', '')
+    group_display_name = group_data.get('display_name', group_name.title())
+    icon_display = f"{group_icon} " if group_icon else ""
+    self.console.print(f"\n{icon_display}[bold magenta]{group_display_name} Variables[/bold magenta]")
+
+    # Check if this group has an enabler variable
+    enabler_var_name = group_data.get('enabler', '')
     
-    # Always set default values for variables in this group, even if user doesn't want to configure them
-    vars_with_defaults = self._get_variables_with_defaults(variables)
+    # Always set default values for variables in this group
     for var_name in vars_with_defaults:
       default_value = self.resolved_defaults.get(var_name)
       self.final_values[var_name] = default_value
     
-    # When group is not enabled
-    if not group_enabled:
-      return
+    if enabler_var_name and enabler_var_name in variables:
+      # For groups with enablers, handle everything in _handle_group_with_enabler
+      self._handle_group_with_enabler(group_name, group_data, variables, vars_without_defaults, vars_with_defaults)
+    else:
+      # Original flow for groups without enablers
+      # Step 2: Determine if group should be enabled
+      group_enabled = self._determine_group_enabled_status(group_name, group_data, variables, vars_without_defaults)
       
-    # Step 3: Prompt for required variables (those without defaults)
-    if vars_without_defaults:
-      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
+      # When group is not enabled
+      if not group_enabled:
+        return
+        
+      # Step 3: Prompt for required variables (those without defaults)
+      if vars_without_defaults:
+        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
+      if vars_with_defaults:
+        self._handle_variables_with_defaults(group_name, vars_with_defaults, variables)
+    # Groups are now more compact, minimal spacing needed
   
   def _get_variables_without_defaults(self, variables: Dict[str, Any]) -> List[str]:
     """Get list of variable names that have no default values."""
@@ -100,95 +121,116 @@ class PromptHandler:
       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:
+  def _determine_group_enabled_status(self, group_name: str, group_data: Dict[str, Any], variables: Dict[str, Any], vars_without_defaults: List[str]) -> bool:
     """Determine if a variable group should be enabled based on complex logic."""
     
+    # Check if this group has an enabler variable
+    enabler_var_name = group_data.get('enabler', '')
+    if enabler_var_name and enabler_var_name in variables:
+      # This is a group controlled by an enabler variable
+      # The enabler variable will be prompted separately
+      # For now, assume it's enabled so we can prompt for the enabler
+      return True
+    
     # 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 any variable in the group is marked as required
+    has_required_vars = any(var_data.get('required', False) for var_data in variables.values())
+    if has_required_vars:
+      logger.debug(f"Group {group_name} has variables marked as required, 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]",
+      enable_group = Confirm.ask(
+        f"Do you want to enable [bold]{group_name}[/bold]?",
         default=False
       )
+      
+      # If group is enabled and has variables with defaults, ask if they want to change values
+      if enable_group and vars_with_defaults:
+        # This will be handled in the main flow after group is enabled
+        pass
+        
+      return enable_group
     except (EOFError, KeyboardInterrupt):
       # For optional group configuration, gracefully handle interruption
       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."""
+    """Show configured values in dim white below header."""
     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")
-    
+    # Create a clean display of configured values
+    var_previews = []
     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)
+      # Truncate long values for cleaner display
+      display_value = str(default_value)
+      if len(display_value) > 25:
+        display_value = display_value[:22] + "..."
+      var_previews.append(f"{var_name}={display_value}")
+    
+    # Show configured values in dim white
+    vars_text = ", ".join(var_previews)
+    self.console.print(f"[dim white]({vars_text})[/dim white]")
   
   def _handle_variables_with_defaults(self, group_name: str, vars_with_defaults: List[str], variables: Dict[str, Any]):
     """Handle variables that have default values."""
     
+    # Show preview of current values before asking if user wants to change them
+    self._show_group_preview(group_name, vars_with_defaults)
+    
     # Ask if user wants to customize any of these values (defaults already set earlier)
     try:
-      want_to_customize = Confirm.ask(f"[yellow]Do you want to customize any {group_name} variables?[/yellow]", default=False)
+      want_to_customize = Confirm.ask(f"Do you want to change {group_name} values?", default=False)
     except (EOFError, KeyboardInterrupt):
       logger.debug(f"User interrupted customization prompt for group {group_name}, using defaults")
       return
     
     if want_to_customize:
+      # Directly prompt for each variable without asking if they want to change it
       for var_name in vars_with_defaults:
         var_data = variables[var_name]
         current_value = self.final_values[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
+        # Directly prompt for the new value
+        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."""
+    """Prompt user for a single variable with new format: Enter VARIABLE_NAME (DESCRIPTION) (DEFAULT)."""
     
     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 required:
-      prompt_parts.append("[red](Required)[/red]")
+    # Build new format prompt: Enter VARIABLE_NAME (DESCRIPTION) (DEFAULT_VALUE)
+    prompt_parts = ["Enter", f"[bold]{var_name}[/bold]"]
+    
+    # Add description in parentheses if available
     if description:
-      prompt_parts.append(f"[dim]{description}[/dim]")
+      prompt_parts.append(f"({description})")
     
-    prompt_message = " ".join(prompt_parts)
+    # Show default value if available
+    if current_value is not None:
+      prompt_parts.append(f"[dim]({current_value})[/dim]")
+    elif required:
+      prompt_parts.append("[red](Required)[/red]")
     
-    # Add type information if not string
-    if var_type != 'string':
-      prompt_message += f" [dim]({var_type})[/dim]"
+    prompt_message = " ".join(prompt_parts)
     
     # Handle different variable types
     try:
@@ -221,6 +263,10 @@ class PromptHandler:
       try:
         value = Prompt.ask(prompt_message, default=default_val)
         
+        # Handle None values that can occur when user provides no input
+        if value is None:
+          value = ""
+        
         if required and not value.strip():
           self.console.print("[red]This field is required and cannot be empty[/red]")
           continue
@@ -313,30 +359,100 @@ class PromptHandler:
     except (EOFError, KeyboardInterrupt):
       raise KeyboardInterrupt("Template generation cancelled by user")
   
+  def _handle_group_with_enabler(self, group_name: str, group_data: Dict[str, Any], 
+                                 variables: Dict[str, Any], vars_without_defaults: List[str], 
+                                 vars_with_defaults: List[str]):
+    """Handle groups that have an enabler variable."""
+    enabler_var_name = group_data.get('enabler', '')
+    enabler_var_data = variables.get(enabler_var_name, {})
+    current_enabler_value = self.final_values.get(enabler_var_name, False)
+    
+    # Ask if they want to enable the feature
+    try:
+      enable_feature = Confirm.ask(
+        f"Do you want to enable [bold]{group_name}[/bold]?",
+        default=bool(current_enabler_value)
+      )
+      self.final_values[enabler_var_name] = enable_feature
+      
+      if not enable_feature:
+        # If the feature is disabled, skip all other variables in this group
+        return
+        
+    except (EOFError, KeyboardInterrupt):
+      logger.debug(f"User interrupted enabler prompt for group {group_name}, using default")
+      return
+    
+    # Now handle required variables (those without defaults)
+    if vars_without_defaults:
+      # Remove enabler from the list if it's there
+      vars_without_defaults = [v for v in vars_without_defaults if v != enabler_var_name]
+      
+      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
+    
+    # Handle variables with defaults
+    if vars_with_defaults:
+      # Remove enabler from the list
+      remaining_vars = [v for v in vars_with_defaults if v != enabler_var_name]
+      
+      if remaining_vars:
+        # Show preview and ask if they want to change values
+        self._show_group_preview(group_name, remaining_vars)
+        
+        try:
+          want_to_customize = Confirm.ask(f"Do you want to change {group_name} values?", 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 remaining_vars:
+            var_data = variables[var_name]
+            current_value = self.final_values[var_name]
+            new_value = self._prompt_for_variable(var_name, var_data, required=False, current_value=current_value)
+            self.final_values[var_name] = new_value
+  
   def _show_summary(self):
-    """Display a summary of all configured variables."""
+    """Display a compact summary of all configured variables."""
     if not self.final_values:
-      self.console.print("[yellow]No variables were configured.[/yellow]")
       return
     
-    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:
+    # Only show detailed table if there are many variables (>5)
+    if len(self.final_values) > 5:
+      table = Table(box=box.SIMPLE)
+      table.add_column("Variable", style="cyan")
+      table.add_column("Value", style="green")
+      
+      for var_name, value in self.final_values.items():
+        # Format value for display and truncate if too long
+        if isinstance(value, list):
+          display_value = ", ".join(str(item) for item in value)
+        else:
+          display_value = str(value)
+        
+        if len(display_value) > 50:
+          display_value = display_value[:47] + "..."
+        
+        table.add_row(var_name, display_value)
+      
+      self.console.print(table)
+    else:
+      # For few variables, show a compact inline summary
+      var_summaries = []
+      for var_name, value in self.final_values.items():
         display_value = str(value)
+        if len(display_value) > 20:
+          display_value = display_value[:17] + "..."
+        var_summaries.append(f"[cyan]{var_name}[/cyan]=[green]{display_value}[/green]")
       
-      table.add_row(var_name, display_value, var_type)
+      summary_text = ", ".join(var_summaries)
+      self.console.print(f"\n[dim]Using:[/dim] {summary_text}")
     
-    self.console.print(table)
     self.console.print()
     
     # Ask user if they want to proceed with template generation
-    if not Confirm.ask("[bold]Proceed with template generation?[/bold]", default=True):
+    if not Confirm.ask("Proceed with generation?", default=True):
       raise KeyboardInterrupt("Template generation cancelled by user")

+ 13 - 50
cli/core/registry.py

@@ -1,64 +1,27 @@
-"""Module registry system with decorator-based registration."""
+"""Module registry system."""
 from typing import Type, Dict, List
-from functools import wraps
 
 
-class Registry:
-  """Registry using decorators for explicit module registration."""
+class ModuleRegistry:
+  """Simple module registry without magic."""
   
   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 register(self, module_class: Type) -> None:
+    """Register a module class."""
+    # Module class defines its own name attribute
+    self._modules[module_class.name] = module_class
   
   def create_instances(self) -> List:
-    """Create instances of all registered modules, sorted alphabetically."""
+    """Create instances of all registered modules."""
     instances = []
-    for name, cls in sorted(self._modules.items()):
+    for name in sorted(self._modules.keys()):
       try:
-        instances.append(cls())
+        instances.append(self._modules[name]())
       except Exception as e:
-        print(f"Warning: Could not instantiate {cls.__name__}: {e}")
+        print(f"Warning: Could not instantiate {name}: {e}")
     return instances
 
-
-# Global registry instance
-registry = Registry()
-register_module = registry.register_module
+# Global registry
+registry = ModuleRegistry()

+ 69 - 125
cli/core/variables.py

@@ -1,143 +1,87 @@
-from typing import Any, Dict, List, Tuple
-from .config import ConfigManager
+from typing import Any, Dict, List
+from dataclasses import dataclass, field
+from collections import OrderedDict
 
 
+@dataclass
 class Variable:
-  """Data class for variable information."""
+  """Variable with all necessary properties."""
+  name: str
+  description: str = ""
+  default: Any = None
+  type: str = "string"
+  options: List[Any] = field(default_factory=list)
+  group: str = "general"
+  required: bool = False
   
-  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)
-  
-  def disable(self) -> None:
-    """Disable this variable."""
-    self.enabled = False
-
-  def to_dict(self) -> Dict[str, Any]:
-    """Convert Variable to dictionary for compatibility with PromptHandler."""
+  def to_prompt_config(self) -> Dict[str, Any]:
+    """Convert to prompt configuration."""
     return {
       'name': self.name,
-      'description': self.description,
-      'value': self.value,
+      'description': self.description, 
       'type': self.type,
       'options': self.options,
-      'enabled': self.enabled
+      'required': self.required,
+      'default': self.default
     }
 
 
-class VariableGroup():
-  """Data class for variable groups."""
+class VariableRegistry:
+  """Variable management for modules."""
   
-  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 __init__(self):
+    self.variables: Dict[str, Variable] = OrderedDict()
+    self.groups: Dict[str, Dict[str, Any]] = OrderedDict()
+    self.registration_order: Dict[str, List[str]] = {}  # group -> ordered list of variable names
+    self.group_enablers: Dict[str, str] = {}  # group -> enabler variable name
   
-  def disable(self) -> None:
-    """Disable this variable group and all its variables."""
-    self.enabled = False
-    for var in self.vars:
-      var.disable()
-
-  def get_all_vars(self) -> List[Variable]:
-    """Get all variables in this group."""
-    return self.vars
-
-  @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", {})
+  def register_variable(self, var: Variable) -> None:
+    """Register a variable."""
+    self.variables[var.name] = var
+    # Track registration order per group
+    if var.group not in self.registration_order:
+      self.registration_order[var.group] = []
+    self.registration_order[var.group].append(var.name)
     
-    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 get_all_groups(self) -> List[VariableGroup]:
-    """Get all variable groups."""
-    return self.variable_groups
-
-  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 get_variable_value(self, name: str, group_name: str = None) -> Any:
-    """Get the value of a variable by name.
+  def register_group(self, name: str, display_name: str, 
+                    description: str = "", icon: str = "", enabler: str = "") -> None:
+    """Register a variable group.
     
     Args:
-        name: Variable name to find
-        group_name: Optional group name to search within
-        
-    Returns:
-        Variable value if found, None otherwise
+        name: Internal group name
+        display_name: Display name for the group
+        description: Group description
+        icon: Optional icon for the group
+        enabler: Optional variable name that controls if this group is enabled
     """
-    for group in self.variable_groups:
-      if group_name is not None and group.name != group_name:
-        continue
-      for var in group.vars:
-        if var.name == name:
-          return var.value
-    return None
-
-  def get_variables_in_template(self, template_vars: List[str]) -> List[tuple]:
-    """Get all variables that exist in the template vars list.
+    self.groups[name] = {
+      'display_name': display_name,
+      'description': description,
+      'icon': icon
+    }
     
-    Args:
-        template_vars: List of variable names used in the template
-        
-    Returns:
-        List of tuples (group_name, variable) for variables found in template
-    """
-    result = []
-    for group in self.variable_groups:
-      for var in group.vars:
-        if var.name in template_vars:
-          result.append((group.name, var))
-    return result
+    if enabler:
+      self.group_enablers[name] = enabler
+  
+  def get_variables_for_template(self, template_vars: List[str]) -> Dict[str, List[Variable]]:
+    """Get variables grouped by their group name, preserving registration order."""
+    grouped = OrderedDict()
+    
+    # First, organize variables by group
+    temp_grouped = {}
+    for var_name in template_vars:
+      if var_name in self.variables:
+        var = self.variables[var_name]
+        if var.group not in temp_grouped:
+          temp_grouped[var.group] = []
+        temp_grouped[var.group].append(var)
+    
+    # Then, sort variables within each group by registration order
+    for group_name, vars_list in temp_grouped.items():
+      grouped[group_name] = sorted(
+        vars_list,
+        key=lambda v: self.registration_order.get(group_name, []).index(v.name) 
+        if v.name in self.registration_order.get(group_name, []) else float('inf')
+      )
+    
+    return grouped

+ 50 - 60
cli/modules/compose.py

@@ -1,65 +1,55 @@
 from ..core.module import Module
-from ..core.variables import VariableGroup, Variable, VariableManager
-from ..core.registry import register_module
+from ..core.variables import Variable
+from ..core.registry import registry
 
-@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."""
+  """Docker Compose module with variables."""
+  
+  name = "compose"
+  description = "Manage Docker Compose configurations"
+  files = ["docker-compose.yml", "compose.yml", "compose.yaml"]
+  
+  def _init_variables(self):
+    """Initialize Compose-specific variables."""
+    # Register groups
+    self.variables.register_group(
+      "general", "General Settings",
+      "Basic configuration for Docker Compose services"
+    )
     
-    # 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": {
-          "swarm": {"description": "Enable Docker Swarm mode", "value": False, "var_type": "boolean"},
-          "swarm_replicas": {"description": "Number of replicas in Swarm", "value": 1, "var_type": "integer"},
-          "replica_count": {"description": "Number of replicas in Swarm", "value": 1, "var_type": "integer"}
-        }
-      },
-      "networking": {
-        "description": "Network and port configuration",
-        "vars": {
-          "service_port": {"description": "Service port mapping", "value": {"http": 8080, "https": 8443}, "var_type": "dict"},
-          "docker_network": {"description": "Docker network name", "value": "bridge"}
-        }
-      },
-      "traefik": {
-        "description": "Variables for Traefik labels",
-        "vars": {
-          "traefik": {"description": "Enable Traefik labels", "value": False, "var_type": "boolean"},
-          "traefik_host": {"description": "Traefik host rule", "value": "example.com"},
-          "traefik_tls": {"description": "Enable TLS for Traefik", "value": True, "var_type": "boolean"},
-          "traefik_certresolver": {"description": "Traefik certificate resolver", "value": "letsencrypt"},
-          "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()]
+    self.variables.register_group(
+      "traefik", "Traefik Configuration", 
+      "Reverse proxy settings", icon="󰞉", enabler="traefik"
+    )
+    
+    # Register variables
+    self.variables.register_variable(Variable(
+      name="service_name",
+      description="Name of the service",
+      group="general",
+      required=True
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="container_name",
+      description="Container name",
+      group="general"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="traefik",
+      description="Enable Traefik",
+      type="boolean",
+      default=False,
+      group="traefik"
+    ))
+    
+    self.variables.register_variable(Variable(
+      name="traefik_host",
+      description="Traefik hostname",
+      default=None,
+      group="traefik"
+    ))
 
-  def register(self, app):
-    return super().register(app)
+# Register the module
+registry.register(ComposeModule)

+ 14 - 12
cli/modules/terraform.py

@@ -1,16 +1,18 @@
 from ..core.module import Module
-from ..core.registry import register_module
+from ..core.registry import registry
+from ..core.variables import Variable
 
-@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."""
+  """Terraform module - clean and simple."""
+  
+  name = "terraform"
+  description = "Manage Terraform configurations"
+  files = ["main.tf", "variables.tf", "outputs.tf", "versions.tf"]
+  
+  def _init_variables(self):
+    """Initialize Terraform-specific variables."""
+    # Only if module needs variables
+    pass
 
-  def __init__(self):
-    super().__init__(name=self.name, description=self.description, files=self.files)
-
-  def register(self, app):
-    return super().register(app)
+# Register the module
+registry.register(TerraformModule)