Просмотр исходного кода

feat: Add metadata system for variables with hints and icons

- Created module metadata files (.meta.yaml) for variable hints, descriptions, and icons
- Templates can override metadata via frontmatter 'variables' section
- Added hint display during prompting (e.g., webapp, api, database)
- Added tips for complex variables (shown with 💡)
- Icons now come from metadata only (no fallback emojis)
- Cleaned up formatting: removed indentation, consistent spacing
- Example compose.meta.yaml with comprehensive variable metadata

This creates a powerful two-tier metadata system where modules provide
defaults and templates can override or extend with specific needs.
xcad 5 месяцев назад
Родитель
Сommit
a16de71c4e
5 измененных файлов с 194 добавлено и 28 удалено
  1. 58 2
      cli/core/module.py
  2. 34 25
      cli/core/prompt.py
  3. 3 1
      cli/core/template.py
  4. 7 0
      cli/core/variables.py
  5. 92 0
      cli/modules/compose.meta.yaml

+ 58 - 2
cli/core/module.py

@@ -2,6 +2,7 @@ from abc import ABC
 from pathlib import Path
 from typing import List, Optional, Dict, Any
 import logging
+import yaml
 from typer import Typer, Option, Argument
 from rich.console import Console
 from .config import get_config
@@ -28,6 +29,23 @@ class Module(ABC):
       )
     
     self.libraries = LibraryManager()
+    self.metadata = self._load_metadata()
+  
+  def _load_metadata(self) -> Dict[str, Any]:
+    """Load module metadata from .meta.yaml file if it exists."""
+    import inspect
+    # Get the path to the actual module file
+    module_path = Path(inspect.getfile(self.__class__))
+    meta_file = module_path.with_suffix('.meta.yaml')
+    
+    if meta_file.exists():
+      try:
+        with open(meta_file, 'r') as f:
+          return yaml.safe_load(f) or {}
+      except Exception as e:
+        logger.debug(f"Failed to load metadata for {self.name}: {e}")
+    
+    return {}
 
 
   def list(self):
@@ -127,6 +145,9 @@ class Module(ABC):
     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():
@@ -142,8 +163,43 @@ class Module(ABC):
           values[var_name] = input(f"Enter {var_name}: ")
       return values
     
-    # Use the new simplified prompt handler with template's analyzed variables
-    return SimplifiedPromptHandler(template.variables)()
+    # 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
+    module_var_metadata = self.metadata.get('variables', {})
+    for var_name, var in variables.items():
+      if var_name in module_var_metadata:
+        meta = module_var_metadata[var_name]
+        if 'hint' in meta and not var.hint:
+          var.hint = meta['hint']
+        if 'description' in meta and not var.description:
+          var.description = meta['description']
+        if 'tip' in meta and not var.tip:
+          var.tip = meta['tip']
+        if 'validation' in meta and not var.validation:
+          var.validation = meta['validation']
+        if 'icon' in meta and not var.icon:
+          var.icon = meta['icon']
+    
+    # Then apply template metadata (overrides module metadata)
+    for var_name, var in variables.items():
+      if var_name in template_metadata:
+        meta = template_metadata[var_name]
+        if 'hint' in meta:
+          var.hint = meta['hint']
+        if 'description' in meta:
+          var.description = meta['description']
+        if 'tip' in meta:
+          var.tip = meta['tip']
+        if 'validation' in meta:
+          var.validation = meta['validation']
+        if 'icon' in meta:
+          var.icon = meta['icon']
   
   def register_cli(self, app: Typer):
     """Register module commands with the main app."""

+ 34 - 25
cli/core/prompt.py

@@ -21,6 +21,7 @@ class SimplifiedPromptHandler:
     """
     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."""
@@ -127,13 +128,16 @@ class SimplifiedPromptHandler:
       if display_optional:
         console.print()
         self._show_variables_compact(display_name, display_optional)
-      
-      if display_optional and Confirm.ask("  Do you want to change any values?", default=False):
-        for var_name in optional:
-          var = self.variables[var_name]
-          self.values[var_name] = self._prompt_variable(
-            var, current_value=self.values[var_name]
-          )
+        
+        if Confirm.ask("Do you want to change any values?", default=False):
+          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_compact(self, category: str, var_names: List[str]):
     """Display variables in compact format with icon."""
@@ -154,22 +158,16 @@ class SimplifiedPromptHandler:
     if items:
       # Use different icons based on category
       icon = self._get_category_icon(category)
-      console.print(f"  {icon} [bold]{category}:[/bold] [dim white]{', '.join(items)}[/dim white]")
+      console.print(f"{icon}[bold]{category}:[/bold] [dim white]{', '.join(items)}[/dim white]")
   
   def _get_category_icon(self, category: str) -> str:
     """Get icon for a category."""
-    icons = {
-      'general': '📦',
-      'network': '🌐',
-      'traefik': '🔀',
-      'swarm': '🐝',
-      'nginx_dashboard': '📊',
-      'service_port': '🔌',
-      'security': '🔒',
-      'storage': '💾',
-      'monitoring': '📈',
-    }
-    return icons.get(category.lower(), '⚙️')  # Default gear icon
+    # 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 _prompt_variable(
     self, 
@@ -178,14 +176,25 @@ class SimplifiedPromptHandler:
     current_value: Any = None
   ) -> Any:
     """Prompt for a single variable value."""
-    # Build prompt message
-    parts = [f"Enter {var.display_name}"]
+    # 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:
-      parts.append(f"[dim]({current_value})[/dim]")
+      prompt_msg = f"Enter {display_text}{hint_text} [dim]({current_value})[/dim]"
     elif required:
-      parts.append("[red](Required)[/red]")
+      prompt_msg = f"Enter {display_text}{hint_text} [red](Required)[/red]"
+    else:
+      prompt_msg = f"Enter {display_text}{hint_text}"
     
-    prompt_msg = " ".join(parts)
+    # Show tip if available
+    if var.tip:
+      console.print(f"[dim cyan]💡 {var.tip}[/dim cyan]")
     
     # Handle different types
     if var.type == 'boolean':

+ 3 - 1
cli/core/template.py

@@ -26,6 +26,7 @@ class Template:
   module: str = ""
   tags: List[str] = field(default_factory=list)
   files: List[str] = field(default_factory=list)
+  variable_metadata: Dict[str, Dict[str, Any]] = field(default_factory=dict)  # Variable hints/tips from frontmatter
   
   # Computed properties (will be set in __post_init__)
   id: str = field(init=False)
@@ -82,7 +83,8 @@ class Template:
         version=frontmatter_data.get('version', ''),
         module=frontmatter_data.get('module', ''),
         tags=frontmatter_data.get('tags', []),
-        files=frontmatter_data.get('files', [])
+        files=frontmatter_data.get('files', []),
+        variable_metadata=frontmatter_data.get('variables', {})
       )
     except Exception:
       # If frontmatter parsing fails, create a basic Template object

+ 7 - 0
cli/core/variables.py

@@ -21,6 +21,13 @@ class TemplateVariable:
   # Grouping info (extracted from dotted notation)
   group: Optional[str] = None  # e.g., 'traefik' for 'traefik.host'
   
+  # 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
+  
   @property
   def display_name(self) -> str:
     """Get display name for prompts."""

+ 92 - 0
cli/modules/compose.meta.yaml

@@ -0,0 +1,92 @@
+# Metadata for Docker Compose module
+# Provides hints, icons, and descriptions for variables
+
+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"
+  service_port:
+    icon: "🔌"
+    description: "Port mappings"
+  nginx_dashboard:
+    icon: "📊"
+    description: "Nginx monitoring dashboard"
+
+variables:
+  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 variables
+  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 variables
+  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 variables
+  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]*$"
+  
+  # Port variables
+  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
+  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}$"