Prechádzať zdrojové kódy

fixed some ruff errors

xcad 5 mesiacov pred
rodič
commit
da1142b684

+ 17 - 18
cli/__main__.py

@@ -11,13 +11,16 @@ import logging
 import pkgutil
 import pkgutil
 import sys
 import sys
 from pathlib import Path
 from pathlib import Path
-from typing import Optional
-from typer import Typer, Option
+
+import click
 from rich.console import Console
 from rich.console import Console
+from typer import Option, Typer
+
 import cli.modules
 import cli.modules
-from cli.core.registry import registry
-from cli.core import repo
 from cli import __version__
 from cli import __version__
+from cli.core import repo
+from cli.core.registry import registry
+
 # Using standard Python exceptions instead of custom ones
 # Using standard Python exceptions instead of custom ones
 
 
 app = Typer(
 app = Typer(
@@ -54,12 +57,12 @@ def setup_logging(log_level: str = "WARNING") -> None:
         logger = logging.getLogger(__name__)
         logger = logging.getLogger(__name__)
         logger.setLevel(numeric_level)
         logger.setLevel(numeric_level)
     except Exception as e:
     except Exception as e:
-        raise RuntimeError(f"Failed to configure logging: {e}")
+        raise RuntimeError(f"Failed to configure logging: {e}") from e
 
 
 
 
 @app.callback(invoke_without_command=True)
 @app.callback(invoke_without_command=True)
 def main(
 def main(
-    version: Optional[bool] = Option(
+    version: bool | None = Option(
         None,
         None,
         "--version",
         "--version",
         "-v",
         "-v",
@@ -71,7 +74,7 @@ def main(
         else None,
         else None,
         is_eager=True,
         is_eager=True,
     ),
     ),
-    log_level: Optional[str] = Option(
+    log_level: str | None = Option(
         None,
         None,
         "--log-level",
         "--log-level",
         help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). If omitted, logging is disabled.",
         help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). If omitted, logging is disabled.",
@@ -88,8 +91,6 @@ def main(
         logging.disable(logging.CRITICAL)
         logging.disable(logging.CRITICAL)
 
 
     # Get context without type annotation (compatible with all Typer versions)
     # Get context without type annotation (compatible with all Typer versions)
-    import click
-
     ctx = click.get_current_context()
     ctx = click.get_current_context()
 
 
     # Store log level in context for potential use by other commands
     # Store log level in context for potential use by other commands
@@ -118,7 +119,7 @@ def init_app() -> None:
         modules_path = Path(cli.modules.__file__).parent
         modules_path = Path(cli.modules.__file__).parent
         logger.debug(f"Discovering modules in {modules_path}")
         logger.debug(f"Discovering modules in {modules_path}")
 
 
-        for finder, name, ispkg in pkgutil.iter_modules([str(modules_path)]):
+        for _finder, name, ispkg in pkgutil.iter_modules([str(modules_path)]):
             # Import both module files and packages (for multi-schema modules)
             # Import both module files and packages (for multi-schema modules)
             if not name.startswith("_") and name != "base":
             if not name.startswith("_") and name != "base":
                 try:
                 try:
@@ -127,11 +128,11 @@ def init_app() -> None:
                     )
                     )
                     importlib.import_module(f"cli.modules.{name}")
                     importlib.import_module(f"cli.modules.{name}")
                 except ImportError as e:
                 except ImportError as e:
-                    error_info = f"Import failed for '{name}': {str(e)}"
+                    error_info = f"Import failed for '{name}': {e!s}"
                     failed_imports.append(error_info)
                     failed_imports.append(error_info)
                     logger.warning(error_info)
                     logger.warning(error_info)
                 except Exception as e:
                 except Exception as e:
-                    error_info = f"Unexpected error importing '{name}': {str(e)}"
+                    error_info = f"Unexpected error importing '{name}': {e!s}"
                     failed_imports.append(error_info)
                     failed_imports.append(error_info)
                     logger.error(error_info)
                     logger.error(error_info)
 
 
@@ -140,7 +141,7 @@ def init_app() -> None:
             logger.debug("Registering repo command")
             logger.debug("Registering repo command")
             repo.register_cli(app)
             repo.register_cli(app)
         except Exception as e:
         except Exception as e:
-            error_info = f"Repo command registration failed: {str(e)}"
+            error_info = f"Repo command registration failed: {e!s}"
             failed_registrations.append(error_info)
             failed_registrations.append(error_info)
             logger.warning(error_info)
             logger.warning(error_info)
 
 
@@ -148,14 +149,12 @@ def init_app() -> None:
         module_classes = list(registry.iter_module_classes())
         module_classes = list(registry.iter_module_classes())
         logger.debug(f"Registering {len(module_classes)} template-based modules")
         logger.debug(f"Registering {len(module_classes)} template-based modules")
 
 
-        for name, module_cls in module_classes:
+        for _name, module_cls in module_classes:
             try:
             try:
                 logger.debug(f"Registering module class: {module_cls.__name__}")
                 logger.debug(f"Registering module class: {module_cls.__name__}")
                 module_cls.register_cli(app)
                 module_cls.register_cli(app)
             except Exception as e:
             except Exception as e:
-                error_info = (
-                    f"Registration failed for '{module_cls.__name__}': {str(e)}"
-                )
+                error_info = f"Registration failed for '{module_cls.__name__}': {e!s}"
                 failed_registrations.append(error_info)
                 failed_registrations.append(error_info)
                 # Log warning but don't raise exception for individual module failures
                 # Log warning but don't raise exception for individual module failures
                 logger.warning(error_info)
                 logger.warning(error_info)
@@ -189,7 +188,7 @@ def init_app() -> None:
             )
             )
 
 
         details = "\n".join(error_details) if error_details else str(e)
         details = "\n".join(error_details) if error_details else str(e)
-        raise RuntimeError(f"Application initialization failed: {details}")
+        raise RuntimeError(f"Application initialization failed: {details}") from e
 
 
 
 
 def run() -> None:
 def run() -> None:

+ 74 - 437
cli/core/config/config_manager.py

@@ -1,11 +1,10 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import logging
 import logging
-import re
 import shutil
 import shutil
 import tempfile
 import tempfile
 from pathlib import Path
 from pathlib import Path
-from typing import Any, Dict, Optional, Union
+from typing import Any
 
 
 import yaml
 import yaml
 
 
@@ -13,22 +12,12 @@ from ..exceptions import ConfigError, ConfigValidationError, YAMLParseError
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-# Valid Python identifier pattern for variable names
-VALID_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
-
-# Valid path pattern - prevents path traversal attempts
-VALID_PATH_PATTERN = re.compile(r'^[^\x00-\x1f<>:"|?*]+$')
-
-# Maximum allowed string lengths to prevent DOS attacks
-MAX_STRING_LENGTH = 1000
-MAX_PATH_LENGTH = 4096
-MAX_LIST_LENGTH = 100
 
 
 
 
 class ConfigManager:
 class ConfigManager:
     """Manages configuration for the CLI application."""
     """Manages configuration for the CLI application."""
 
 
-    def __init__(self, config_path: Optional[Union[str, Path]] = None) -> None:
+    def __init__(self, config_path: str | Path | None = None) -> None:
         """Initialize the configuration manager.
         """Initialize the configuration manager.
 
 
         Args:
         Args:
@@ -121,75 +110,8 @@ class ConfigManager:
         except Exception as e:
         except Exception as e:
             logger.warning(f"Config migration failed: {e}")
             logger.warning(f"Config migration failed: {e}")
 
 
-    @staticmethod
-    def _validate_string_length(
-        value: str, field_name: str, max_length: int = MAX_STRING_LENGTH
-    ) -> None:
-        """Validate string length to prevent DOS attacks.
-
-        Args:
-            value: String value to validate
-            field_name: Name of the field for error messages
-            max_length: Maximum allowed length
 
 
-        Raises:
-            ConfigValidationError: If string exceeds maximum length
-        """
-        if len(value) > max_length:
-            raise ConfigValidationError(
-                f"{field_name} exceeds maximum length of {max_length} characters "
-                f"(got {len(value)} characters)"
-            )
-
-    @staticmethod
-    def _validate_path_string(path: str, field_name: str) -> None:
-        """Validate path string for security concerns.
-
-        Args:
-            path: Path string to validate
-            field_name: Name of the field for error messages
-
-        Raises:
-            ConfigValidationError: If path contains invalid characters or patterns
-        """
-        # Check length
-        if len(path) > MAX_PATH_LENGTH:
-            raise ConfigValidationError(
-                f"{field_name} exceeds maximum path length of {MAX_PATH_LENGTH} characters"
-            )
-
-        # Check for null bytes and control characters
-        if "\x00" in path or any(ord(c) < 32 for c in path if c not in "\t\n\r"):
-            raise ConfigValidationError(
-                f"{field_name} contains invalid control characters"
-            )
-
-        # Check for path traversal attempts
-        if ".." in path.split("/"):
-            logger.warning(
-                f"Path '{path}' contains '..' - potential path traversal attempt"
-            )
-
-    @staticmethod
-    def _validate_list_length(
-        lst: list, field_name: str, max_length: int = MAX_LIST_LENGTH
-    ) -> None:
-        """Validate list length to prevent DOS attacks.
-
-        Args:
-            lst: List to validate
-            field_name: Name of the field for error messages
-            max_length: Maximum allowed length
-
-        Raises:
-            ConfigValidationError: If list exceeds maximum length
-        """
-        if len(lst) > max_length:
-            raise ConfigValidationError(
-                f"{field_name} exceeds maximum length of {max_length} items (got {len(lst)} items)"
-            )
-
-    def _read_config(self) -> Dict[str, Any]:
+    def _read_config(self) -> dict[str, Any]:
         """Read configuration from file.
         """Read configuration from file.
 
 
         Returns:
         Returns:
@@ -201,7 +123,7 @@ class ConfigManager:
             ConfigError: If reading fails for other reasons.
             ConfigError: If reading fails for other reasons.
         """
         """
         try:
         try:
-            with open(self.config_path, "r") as f:
+            with open(self.config_path) as f:
                 config = yaml.safe_load(f) or {}
                 config = yaml.safe_load(f) or {}
 
 
             # Validate config structure
             # Validate config structure
@@ -210,17 +132,17 @@ class ConfigManager:
             return config
             return config
         except yaml.YAMLError as e:
         except yaml.YAMLError as e:
             logger.error(f"Failed to parse YAML configuration: {e}")
             logger.error(f"Failed to parse YAML configuration: {e}")
-            raise YAMLParseError(str(self.config_path), e)
+            raise YAMLParseError(str(self.config_path), e) from e
         except ConfigValidationError:
         except ConfigValidationError:
             # Re-raise validation errors as-is
             # Re-raise validation errors as-is
             raise
             raise
-        except (IOError, OSError) as e:
+        except OSError as e:
             logger.error(f"Failed to read configuration file: {e}")
             logger.error(f"Failed to read configuration file: {e}")
             raise ConfigError(
             raise ConfigError(
                 f"Failed to read configuration file '{self.config_path}': {e}"
                 f"Failed to read configuration file '{self.config_path}': {e}"
-            )
+            ) from e
 
 
-    def _write_config(self, config: Dict[str, Any]) -> None:
+    def _write_config(self, config: dict[str, Any]) -> None:
         """Write configuration to file atomically using temp file + rename pattern.
         """Write configuration to file atomically using temp file + rename pattern.
 
 
         This prevents config file corruption if write operation fails partway through.
         This prevents config file corruption if write operation fails partway through.
@@ -260,20 +182,20 @@ class ConfigManager:
             if tmp_path:
             if tmp_path:
                 Path(tmp_path).unlink(missing_ok=True)
                 Path(tmp_path).unlink(missing_ok=True)
             raise
             raise
-        except (IOError, OSError, yaml.YAMLError) as e:
+        except (OSError, yaml.YAMLError) as e:
             # Clean up temp file if it exists
             # Clean up temp file if it exists
             if tmp_path:
             if tmp_path:
                 try:
                 try:
                     Path(tmp_path).unlink(missing_ok=True)
                     Path(tmp_path).unlink(missing_ok=True)
-                except (IOError, OSError):
+                except OSError:
                     logger.warning(f"Failed to clean up temporary file: {tmp_path}")
                     logger.warning(f"Failed to clean up temporary file: {tmp_path}")
             logger.error(f"Failed to write configuration file: {e}")
             logger.error(f"Failed to write configuration file: {e}")
             raise ConfigError(
             raise ConfigError(
                 f"Failed to write configuration to '{self.config_path}': {e}"
                 f"Failed to write configuration to '{self.config_path}': {e}"
-            )
+            ) from e
 
 
-    def _validate_config_structure(self, config: Dict[str, Any]) -> None:
-        """Validate the configuration structure with comprehensive checks.
+    def _validate_config_structure(self, config: dict[str, Any]) -> None:
+        """Validate the configuration structure - basic type checking.
 
 
         Args:
         Args:
             config: Configuration dictionary to validate.
             config: Configuration dictionary to validate.
@@ -284,197 +206,58 @@ class ConfigManager:
         if not isinstance(config, dict):
         if not isinstance(config, dict):
             raise ConfigValidationError("Configuration must be a dictionary")
             raise ConfigValidationError("Configuration must be a dictionary")
 
 
-        # Check top-level structure
-        if "defaults" in config and not isinstance(config["defaults"], dict):
-            raise ConfigValidationError("'defaults' must be a dictionary")
-
-        if "preferences" in config and not isinstance(config["preferences"], dict):
-            raise ConfigValidationError("'preferences' must be a dictionary")
+        # Validate top-level types
+        self._validate_top_level_types(config)
 
 
         # Validate defaults structure
         # Validate defaults structure
-        if "defaults" in config:
-            for module_name, module_defaults in config["defaults"].items():
-                if not isinstance(module_name, str):
-                    raise ConfigValidationError(
-                        f"Module name must be a string, got {type(module_name).__name__}"
-                    )
-
-                # Validate module name length
-                self._validate_string_length(module_name, "Module name", max_length=100)
-
-                if not isinstance(module_defaults, dict):
-                    raise ConfigValidationError(
-                        f"Defaults for module '{module_name}' must be a dictionary"
-                    )
-
-                # Validate number of defaults per module
-                self._validate_list_length(
-                    list(module_defaults.keys()), f"Defaults for module '{module_name}'"
-                )
+        self._validate_defaults_types(config)
 
 
-                # Validate variable names are valid Python identifiers
-                for var_name, var_value in module_defaults.items():
-                    if not isinstance(var_name, str):
-                        raise ConfigValidationError(
-                            f"Variable name must be a string, got {type(var_name).__name__}"
-                        )
+        # Validate libraries structure
+        self._validate_libraries_fields(config)
 
 
-                    # Validate variable name length
-                    self._validate_string_length(
-                        var_name, "Variable name", max_length=100
-                    )
+    def _validate_top_level_types(self, config: dict[str, Any]) -> None:
+        """Validate top-level config section types."""
+        if "defaults" in config and not isinstance(config["defaults"], dict):
+            raise ConfigValidationError("'defaults' must be a dictionary")
 
 
-                    if not VALID_IDENTIFIER_PATTERN.match(var_name):
-                        raise ConfigValidationError(
-                            f"Invalid variable name '{var_name}' in module '{module_name}'. "
-                            f"Variable names must be valid Python identifiers (letters, numbers, underscores, "
-                            f"cannot start with a number)"
-                        )
+        if "preferences" in config and not isinstance(config["preferences"], dict):
+            raise ConfigValidationError("'preferences' must be a dictionary")
 
 
-                    # Validate variable value types and lengths
-                    if isinstance(var_value, str):
-                        self._validate_string_length(
-                            var_value, f"Value for '{module_name}.{var_name}'"
-                        )
-                    elif isinstance(var_value, list):
-                        self._validate_list_length(
-                            var_value, f"Value for '{module_name}.{var_name}'"
-                        )
-                    elif var_value is not None and not isinstance(
-                        var_value, (bool, int, float)
-                    ):
-                        raise ConfigValidationError(
-                            f"Invalid value type for '{module_name}.{var_name}': "
-                            f"must be string, number, boolean, list, or null (got {type(var_value).__name__})"
-                        )
+        if "libraries" in config and not isinstance(config["libraries"], list):
+            raise ConfigValidationError("'libraries' must be a list")
 
 
-        # Validate preferences structure and types
-        if "preferences" in config:
-            preferences = config["preferences"]
+    def _validate_defaults_types(self, config: dict[str, Any]) -> None:
+        """Validate defaults section has correct types."""
+        if "defaults" not in config:
+            return
 
 
-            # Validate known preference types
-            if "editor" in preferences:
-                if not isinstance(preferences["editor"], str):
-                    raise ConfigValidationError("Preference 'editor' must be a string")
-                self._validate_string_length(
-                    preferences["editor"], "Preference 'editor'", max_length=100
+        for module_name, module_defaults in config["defaults"].items():
+            if not isinstance(module_defaults, dict):
+                raise ConfigValidationError(
+                    f"Defaults for module '{module_name}' must be a dictionary"
                 )
                 )
 
 
-            if "output_dir" in preferences:
-                output_dir = preferences["output_dir"]
-                if output_dir is not None:
-                    if not isinstance(output_dir, str):
-                        raise ConfigValidationError(
-                            "Preference 'output_dir' must be a string or null"
-                        )
-                    self._validate_path_string(output_dir, "Preference 'output_dir'")
-
-            if "library_paths" in preferences:
-                if not isinstance(preferences["library_paths"], list):
-                    raise ConfigValidationError(
-                        "Preference 'library_paths' must be a list"
-                    )
+    def _validate_libraries_fields(self, config: dict[str, Any]) -> None:
+        """Validate libraries have required fields."""
+        if "libraries" not in config:
+            return
 
 
-                self._validate_list_length(
-                    preferences["library_paths"], "Preference 'library_paths'"
-                )
+        for i, library in enumerate(config["libraries"]):
+            if not isinstance(library, dict):
+                raise ConfigValidationError(f"Library at index {i} must be a dictionary")
 
 
-                for i, path in enumerate(preferences["library_paths"]):
-                    if not isinstance(path, str):
-                        raise ConfigValidationError(
-                            f"Library path must be a string, got {type(path).__name__}"
-                        )
-                    self._validate_path_string(path, f"Library path at index {i}")
+            if "name" not in library:
+                raise ConfigValidationError(f"Library at index {i} missing required field 'name'")
 
 
-        # Validate libraries structure
-        if "libraries" in config:
-            libraries = config["libraries"]
-
-            if not isinstance(libraries, list):
-                raise ConfigValidationError("'libraries' must be a list")
-
-            self._validate_list_length(libraries, "Libraries list")
-
-            for i, library in enumerate(libraries):
-                if not isinstance(library, dict):
-                    raise ConfigValidationError(
-                        f"Library at index {i} must be a dictionary"
-                    )
-
-                # Validate name field (required for all library types)
-                if "name" not in library:
-                    raise ConfigValidationError(
-                        f"Library at index {i} missing required field 'name'"
-                    )
-                if not isinstance(library["name"], str):
-                    raise ConfigValidationError(
-                        f"Library 'name' at index {i} must be a string"
-                    )
-                self._validate_string_length(
-                    library["name"], f"Library 'name' at index {i}", max_length=500
+            lib_type = library.get("type", "git")
+            if lib_type == "git" and ("url" not in library or "directory" not in library):
+                raise ConfigValidationError(
+                    f"Git library at index {i} missing required fields 'url' and/or 'directory'"
+                )
+            elif lib_type == "static" and "path" not in library:
+                raise ConfigValidationError(
+                    f"Static library at index {i} missing required field 'path'"
                 )
                 )
-
-                # Validate type field (default to "git" for backward compatibility)
-                lib_type = library.get("type", "git")
-                if lib_type not in ("git", "static"):
-                    raise ConfigValidationError(
-                        f"Library type at index {i} must be 'git' or 'static', got '{lib_type}'"
-                    )
-
-                # Type-specific validation
-                if lib_type == "git":
-                    # Git libraries require: url, directory
-                    required_fields = ["url", "directory"]
-                    for field in required_fields:
-                        if field not in library:
-                            raise ConfigValidationError(
-                                f"Git library at index {i} missing required field '{field}'"
-                            )
-
-                        if not isinstance(library[field], str):
-                            raise ConfigValidationError(
-                                f"Library '{field}' at index {i} must be a string"
-                            )
-
-                        self._validate_string_length(
-                            library[field],
-                            f"Library '{field}' at index {i}",
-                            max_length=500,
-                        )
-
-                    # Validate optional branch field
-                    if "branch" in library:
-                        if not isinstance(library["branch"], str):
-                            raise ConfigValidationError(
-                                f"Library 'branch' at index {i} must be a string"
-                            )
-                        self._validate_string_length(
-                            library["branch"],
-                            f"Library 'branch' at index {i}",
-                            max_length=200,
-                        )
-
-                elif lib_type == "static":
-                    # Static libraries require: path
-                    if "path" not in library:
-                        raise ConfigValidationError(
-                            f"Static library at index {i} missing required field 'path'"
-                        )
-
-                    if not isinstance(library["path"], str):
-                        raise ConfigValidationError(
-                            f"Library 'path' at index {i} must be a string"
-                        )
-
-                    self._validate_path_string(
-                        library["path"], f"Library 'path' at index {i}"
-                    )
-
-                # Validate optional enabled field (applies to all types)
-                if "enabled" in library and not isinstance(library["enabled"], bool):
-                    raise ConfigValidationError(
-                        f"Library 'enabled' at index {i} must be a boolean"
-                    )
 
 
     def get_config_path(self) -> Path:
     def get_config_path(self) -> Path:
         """Get the path to the configuration file being used.
         """Get the path to the configuration file being used.
@@ -492,7 +275,7 @@ class ConfigManager:
         """
         """
         return self.is_local
         return self.is_local
 
 
-    def get_defaults(self, module_name: str) -> Dict[str, Any]:
+    def get_defaults(self, module_name: str) -> dict[str, Any]:
         """Get default variable values for a module.
         """Get default variable values for a module.
 
 
         Returns defaults in a flat format:
         Returns defaults in a flat format:
@@ -511,7 +294,7 @@ class ConfigManager:
         defaults = config.get("defaults", {})
         defaults = config.get("defaults", {})
         return defaults.get(module_name, {})
         return defaults.get(module_name, {})
 
 
-    def set_defaults(self, module_name: str, defaults: Dict[str, Any]) -> None:
+    def set_defaults(self, module_name: str, defaults: dict[str, Any]) -> None:
         """Set default variable values for a module with comprehensive validation.
         """Set default variable values for a module with comprehensive validation.
 
 
         Args:
         Args:
@@ -522,47 +305,13 @@ class ConfigManager:
         Raises:
         Raises:
             ConfigValidationError: If module name or variable names are invalid.
             ConfigValidationError: If module name or variable names are invalid.
         """
         """
-        # Validate module name
+        # Basic validation
         if not isinstance(module_name, str) or not module_name:
         if not isinstance(module_name, str) or not module_name:
             raise ConfigValidationError("Module name must be a non-empty string")
             raise ConfigValidationError("Module name must be a non-empty string")
 
 
-        self._validate_string_length(module_name, "Module name", max_length=100)
-
-        # Validate defaults dictionary
         if not isinstance(defaults, dict):
         if not isinstance(defaults, dict):
             raise ConfigValidationError("Defaults must be a dictionary")
             raise ConfigValidationError("Defaults must be a dictionary")
 
 
-        # Validate number of defaults
-        self._validate_list_length(list(defaults.keys()), "Defaults dictionary")
-
-        # Validate variable names and values
-        for var_name, var_value in defaults.items():
-            if not isinstance(var_name, str):
-                raise ConfigValidationError(
-                    f"Variable name must be a string, got {type(var_name).__name__}"
-                )
-
-            self._validate_string_length(var_name, "Variable name", max_length=100)
-
-            if not VALID_IDENTIFIER_PATTERN.match(var_name):
-                raise ConfigValidationError(
-                    f"Invalid variable name '{var_name}'. Variable names must be valid Python identifiers "
-                    f"(letters, numbers, underscores, cannot start with a number)"
-                )
-
-            # Validate value types and lengths
-            if isinstance(var_value, str):
-                self._validate_string_length(var_value, f"Value for '{var_name}'")
-            elif isinstance(var_value, list):
-                self._validate_list_length(var_value, f"Value for '{var_name}'")
-            elif var_value is not None and not isinstance(
-                var_value, (bool, int, float)
-            ):
-                raise ConfigValidationError(
-                    f"Invalid value type for '{var_name}': "
-                    f"must be string, number, boolean, list, or null (got {type(var_value).__name__})"
-                )
-
         config = self._read_config()
         config = self._read_config()
 
 
         if "defaults" not in config:
         if "defaults" not in config:
@@ -583,42 +332,19 @@ class ConfigManager:
         Raises:
         Raises:
             ConfigValidationError: If module name or variable name is invalid.
             ConfigValidationError: If module name or variable name is invalid.
         """
         """
-        # Validate inputs
+        # Basic validation
         if not isinstance(module_name, str) or not module_name:
         if not isinstance(module_name, str) or not module_name:
             raise ConfigValidationError("Module name must be a non-empty string")
             raise ConfigValidationError("Module name must be a non-empty string")
 
 
-        self._validate_string_length(module_name, "Module name", max_length=100)
-
-        if not isinstance(var_name, str):
-            raise ConfigValidationError(
-                f"Variable name must be a string, got {type(var_name).__name__}"
-            )
-
-        self._validate_string_length(var_name, "Variable name", max_length=100)
-
-        if not VALID_IDENTIFIER_PATTERN.match(var_name):
-            raise ConfigValidationError(
-                f"Invalid variable name '{var_name}'. Variable names must be valid Python identifiers "
-                f"(letters, numbers, underscores, cannot start with a number)"
-            )
-
-        # Validate value type and length
-        if isinstance(value, str):
-            self._validate_string_length(value, f"Value for '{var_name}'")
-        elif isinstance(value, list):
-            self._validate_list_length(value, f"Value for '{var_name}'")
-        elif value is not None and not isinstance(value, (bool, int, float)):
-            raise ConfigValidationError(
-                f"Invalid value type for '{var_name}': "
-                f"must be string, number, boolean, list, or null (got {type(value).__name__})"
-            )
+        if not isinstance(var_name, str) or not var_name:
+            raise ConfigValidationError("Variable name must be a non-empty string")
 
 
         defaults = self.get_defaults(module_name)
         defaults = self.get_defaults(module_name)
         defaults[var_name] = value
         defaults[var_name] = value
         self.set_defaults(module_name, defaults)
         self.set_defaults(module_name, defaults)
         logger.info(f"Set default for '{module_name}.{var_name}' = '{value}'")
         logger.info(f"Set default for '{module_name}.{var_name}' = '{value}'")
 
 
-    def get_default_value(self, module_name: str, var_name: str) -> Optional[Any]:
+    def get_default_value(self, module_name: str, var_name: str) -> Any | None:
         """Get a single default variable value.
         """Get a single default variable value.
 
 
         Args:
         Args:
@@ -644,7 +370,7 @@ class ConfigManager:
             self._write_config(config)
             self._write_config(config)
             logger.info(f"Cleared defaults for module '{module_name}'")
             logger.info(f"Cleared defaults for module '{module_name}'")
 
 
-    def get_preference(self, key: str) -> Optional[Any]:
+    def get_preference(self, key: str) -> Any | None:
         """Get a user preference value.
         """Get a user preference value.
 
 
         Args:
         Args:
@@ -667,46 +393,10 @@ class ConfigManager:
         Raises:
         Raises:
             ConfigValidationError: If key or value is invalid for known preference types.
             ConfigValidationError: If key or value is invalid for known preference types.
         """
         """
-        # Validate key
+        # Basic validation
         if not isinstance(key, str) or not key:
         if not isinstance(key, str) or not key:
             raise ConfigValidationError("Preference key must be a non-empty string")
             raise ConfigValidationError("Preference key must be a non-empty string")
 
 
-        self._validate_string_length(key, "Preference key", max_length=100)
-
-        # Validate known preference types
-        if key == "editor":
-            if not isinstance(value, str):
-                raise ConfigValidationError("Preference 'editor' must be a string")
-            self._validate_string_length(value, "Preference 'editor'", max_length=100)
-
-        elif key == "output_dir":
-            if value is not None:
-                if not isinstance(value, str):
-                    raise ConfigValidationError(
-                        "Preference 'output_dir' must be a string or null"
-                    )
-                self._validate_path_string(value, "Preference 'output_dir'")
-
-        elif key == "library_paths":
-            if not isinstance(value, list):
-                raise ConfigValidationError("Preference 'library_paths' must be a list")
-
-            self._validate_list_length(value, "Preference 'library_paths'")
-
-            for i, path in enumerate(value):
-                if not isinstance(path, str):
-                    raise ConfigValidationError(
-                        f"Library path must be a string, got {type(path).__name__}"
-                    )
-                self._validate_path_string(path, f"Library path at index {i}")
-
-        # For unknown preference keys, apply basic validation
-        else:
-            if isinstance(value, str):
-                self._validate_string_length(value, f"Preference '{key}'")
-            elif isinstance(value, list):
-                self._validate_list_length(value, f"Preference '{key}'")
-
         config = self._read_config()
         config = self._read_config()
 
 
         if "preferences" not in config:
         if "preferences" not in config:
@@ -716,7 +406,7 @@ class ConfigManager:
         self._write_config(config)
         self._write_config(config)
         logger.info(f"Set preference '{key}' = '{value}'")
         logger.info(f"Set preference '{key}' = '{value}'")
 
 
-    def get_all_preferences(self) -> Dict[str, Any]:
+    def get_all_preferences(self) -> dict[str, Any]:
         """Get all user preferences.
         """Get all user preferences.
 
 
         Returns:
         Returns:
@@ -725,7 +415,7 @@ class ConfigManager:
         config = self._read_config()
         config = self._read_config()
         return config.get("preferences", {})
         return config.get("preferences", {})
 
 
-    def get_libraries(self) -> list[Dict[str, Any]]:
+    def get_libraries(self) -> list[dict[str, Any]]:
         """Get all configured libraries.
         """Get all configured libraries.
 
 
         Returns:
         Returns:
@@ -734,7 +424,7 @@ class ConfigManager:
         config = self._read_config()
         config = self._read_config()
         return config.get("libraries", [])
         return config.get("libraries", [])
 
 
-    def get_library_by_name(self, name: str) -> Optional[Dict[str, Any]]:
+    def get_library_by_name(self, name: str) -> dict[str, Any] | None:
         """Get a specific library by name.
         """Get a specific library by name.
 
 
         Args:
         Args:
@@ -753,10 +443,10 @@ class ConfigManager:
         self,
         self,
         name: str,
         name: str,
         library_type: str = "git",
         library_type: str = "git",
-        url: Optional[str] = None,
-        directory: Optional[str] = None,
+        url: str | None = None,
+        directory: str | None = None,
         branch: str = "main",
         branch: str = "main",
-        path: Optional[str] = None,
+        path: str | None = None,
         enabled: bool = True,
         enabled: bool = True,
     ) -> None:
     ) -> None:
         """Add a new library to the configuration.
         """Add a new library to the configuration.
@@ -773,45 +463,22 @@ class ConfigManager:
         Raises:
         Raises:
             ConfigValidationError: If library with the same name already exists or validation fails
             ConfigValidationError: If library with the same name already exists or validation fails
         """
         """
-        # Validate name
+        # Basic validation
         if not isinstance(name, str) or not name:
         if not isinstance(name, str) or not name:
             raise ConfigValidationError("Library name must be a non-empty string")
             raise ConfigValidationError("Library name must be a non-empty string")
 
 
-        self._validate_string_length(name, "Library name", max_length=100)
-
-        # Validate type
         if library_type not in ("git", "static"):
         if library_type not in ("git", "static"):
             raise ConfigValidationError(
             raise ConfigValidationError(
                 f"Library type must be 'git' or 'static', got '{library_type}'"
                 f"Library type must be 'git' or 'static', got '{library_type}'"
             )
             )
 
 
-        # Check if library already exists
         if self.get_library_by_name(name):
         if self.get_library_by_name(name):
             raise ConfigValidationError(f"Library '{name}' already exists")
             raise ConfigValidationError(f"Library '{name}' already exists")
 
 
-        # Type-specific validation and config building
+        # Type-specific validation
         if library_type == "git":
         if library_type == "git":
-            if not url:
-                raise ConfigValidationError("Git libraries require 'url' parameter")
-            if not directory:
-                raise ConfigValidationError(
-                    "Git libraries require 'directory' parameter"
-                )
-
-            # Validate git-specific fields
-            if not isinstance(url, str) or not url:
-                raise ConfigValidationError("Library URL must be a non-empty string")
-            self._validate_string_length(url, "Library URL", max_length=500)
-
-            if not isinstance(directory, str) or not directory:
-                raise ConfigValidationError(
-                    "Library directory must be a non-empty string"
-                )
-            self._validate_string_length(directory, "Library directory", max_length=200)
-
-            if not isinstance(branch, str) or not branch:
-                raise ConfigValidationError("Library branch must be a non-empty string")
-            self._validate_string_length(branch, "Library branch", max_length=200)
+            if not url or not directory:
+                raise ConfigValidationError("Git libraries require 'url' and 'directory' parameters")
 
 
             library_config = {
             library_config = {
                 "name": name,
                 "name": name,
@@ -826,11 +493,6 @@ class ConfigManager:
             if not path:
             if not path:
                 raise ConfigValidationError("Static libraries require 'path' parameter")
                 raise ConfigValidationError("Static libraries require 'path' parameter")
 
 
-            # Validate static-specific fields
-            if not isinstance(path, str) or not path:
-                raise ConfigValidationError("Library path must be a non-empty string")
-            self._validate_path_string(path, "Library path")
-
             # For backward compatibility with older CLI versions,
             # For backward compatibility with older CLI versions,
             # add dummy values for git-specific fields
             # add dummy values for git-specific fields
             library_config = {
             library_config = {
@@ -897,41 +559,16 @@ class ConfigManager:
 
 
                 # Update allowed fields
                 # Update allowed fields
                 if "url" in kwargs:
                 if "url" in kwargs:
-                    url = kwargs["url"]
-                    if not isinstance(url, str) or not url:
-                        raise ConfigValidationError(
-                            "Library URL must be a non-empty string"
-                        )
-                    self._validate_string_length(url, "Library URL", max_length=500)
-                    library["url"] = url
+                    library["url"] = kwargs["url"]
 
 
                 if "branch" in kwargs:
                 if "branch" in kwargs:
-                    branch = kwargs["branch"]
-                    if not isinstance(branch, str) or not branch:
-                        raise ConfigValidationError(
-                            "Library branch must be a non-empty string"
-                        )
-                    self._validate_string_length(
-                        branch, "Library branch", max_length=200
-                    )
-                    library["branch"] = branch
+                    library["branch"] = kwargs["branch"]
 
 
                 if "directory" in kwargs:
                 if "directory" in kwargs:
-                    directory = kwargs["directory"]
-                    if not isinstance(directory, str) or not directory:
-                        raise ConfigValidationError(
-                            "Library directory must be a non-empty string"
-                        )
-                    self._validate_string_length(
-                        directory, "Library directory", max_length=200
-                    )
-                    library["directory"] = directory
+                    library["directory"] = kwargs["directory"]
 
 
                 if "enabled" in kwargs:
                 if "enabled" in kwargs:
-                    enabled = kwargs["enabled"]
-                    if not isinstance(enabled, bool):
-                        raise ConfigValidationError("Library enabled must be a boolean")
-                    library["enabled"] = enabled
+                    library["enabled"] = kwargs["enabled"]
 
 
                 break
                 break
 
 

+ 9 - 19
cli/core/display/display_manager.py

@@ -6,15 +6,19 @@ import logging
 from pathlib import Path
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
+from jinja2 import Template as Jinja2Template
 from rich.console import Console
 from rich.console import Console
+from rich.progress import Progress
+from rich.syntax import Syntax
+from rich.table import Table
 from rich.tree import Tree
 from rich.tree import Tree
 
 
 from .display_settings import DisplaySettings
 from .display_settings import DisplaySettings
 from .icon_manager import IconManager
 from .icon_manager import IconManager
-from .variable_display import VariableDisplayManager
-from .template_display import TemplateDisplayManager
 from .status_display import StatusDisplayManager
 from .status_display import StatusDisplayManager
 from .table_display import TableDisplayManager
 from .table_display import TableDisplayManager
+from .template_display import TemplateDisplayManager
+from .variable_display import VariableDisplayManager
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from ..exceptions import TemplateRenderError
     from ..exceptions import TemplateRenderError
@@ -122,7 +126,7 @@ class DisplayManager:
         """Delegate to TableDisplayManager."""
         """Delegate to TableDisplayManager."""
         return self.tables.render_templates_table(templates, module_name, title)
         return self.tables.render_templates_table(templates, module_name, title)
 
 
-    def display_template(self, template: "Template", template_id: str) -> None:
+    def display_template(self, template: Template, template_id: str) -> None:
         """Delegate to TemplateDisplayManager."""
         """Delegate to TemplateDisplayManager."""
         return self.templates.render_template(template, template_id)
         return self.templates.render_template(template, template_id)
 
 
@@ -209,7 +213,7 @@ class DisplayManager:
         return self.status.display_skipped(message, reason)
         return self.status.display_skipped(message, reason)
 
 
     def display_template_render_error(
     def display_template_render_error(
-        self, error: "TemplateRenderError", context: str | None = None
+        self, error: TemplateRenderError, context: str | None = None
     ) -> None:
     ) -> None:
         """Delegate to StatusDisplayManager."""
         """Delegate to StatusDisplayManager."""
         return self.status.display_template_render_error(error, context)
         return self.status.display_template_render_error(error, context)
@@ -289,8 +293,6 @@ class DisplayManager:
             text: Error text to display
             text: Error text to display
             style: Optional Rich style markup (defaults to red)
             style: Optional Rich style markup (defaults to red)
         """
         """
-        from rich.console import Console
-
         console_err = Console(stderr=True)
         console_err = Console(stderr=True)
         if style is None:
         if style is None:
             style = "red"
             style = "red"
@@ -303,8 +305,6 @@ class DisplayManager:
             text: Warning text to display
             text: Warning text to display
             style: Optional Rich style markup (defaults to yellow)
             style: Optional Rich style markup (defaults to yellow)
         """
         """
-        from rich.console import Console
-
         console_err = Console(stderr=True)
         console_err = Console(stderr=True)
         if style is None:
         if style is None:
             style = "yellow"
             style = "yellow"
@@ -327,13 +327,11 @@ class DisplayManager:
             show_header: Whether to show header row
             show_header: Whether to show header row
             borderless: If True, use borderless style (box=None)
             borderless: If True, use borderless style (box=None)
         """
         """
-        from rich.table import Table
-
         table = Table(
         table = Table(
             title=title,
             title=title,
             show_header=show_header and headers is not None,
             show_header=show_header and headers is not None,
             header_style=self.settings.STYLE_TABLE_HEADER,
             header_style=self.settings.STYLE_TABLE_HEADER,
-            box=None if borderless else None,  # Use default box unless borderless
+            box=None,
             padding=self.settings.PADDING_TABLE_NORMAL if borderless else (0, 1),
             padding=self.settings.PADDING_TABLE_NORMAL if borderless else (0, 1),
         )
         )
 
 
@@ -360,8 +358,6 @@ class DisplayManager:
             root_label: Label for the root node
             root_label: Label for the root node
             nodes: Hierarchical structure (dict or list)
             nodes: Hierarchical structure (dict or list)
         """
         """
-        from rich.tree import Tree
-
         tree = Tree(root_label)
         tree = Tree(root_label)
         self._build_tree_nodes(tree, nodes)
         self._build_tree_nodes(tree, nodes)
         console.print(tree)
         console.print(tree)
@@ -414,8 +410,6 @@ class DisplayManager:
             code_text: Code to display
             code_text: Code to display
             language: Programming language for syntax highlighting
             language: Programming language for syntax highlighting
         """
         """
-        from rich.syntax import Syntax
-
         if language:
         if language:
             syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
             syntax = Syntax(code_text, language, theme="monokai", line_numbers=False)
             console.print(syntax)
             console.print(syntax)
@@ -438,8 +432,6 @@ class DisplayManager:
                 # do work
                 # do work
                 progress.remove_task(task)
                 progress.remove_task(task)
         """
         """
-        from rich.progress import Progress
-
         return Progress(*columns, console=console)
         return Progress(*columns, console=console)
 
 
     def get_lock_icon(self) -> str:
     def get_lock_icon(self) -> str:
@@ -481,8 +473,6 @@ class DisplayManager:
         console.print("\n[bold cyan]Next Steps:[/bold cyan]")
         console.print("\n[bold cyan]Next Steps:[/bold cyan]")
 
 
         try:
         try:
-            from jinja2 import Template as Jinja2Template
-
             next_steps_template = Jinja2Template(next_steps)
             next_steps_template = Jinja2Template(next_steps)
             rendered_next_steps = next_steps_template.render(variable_values)
             rendered_next_steps = next_steps_template.render(variable_values)
             console.print(rendered_next_steps)
             console.print(rendered_next_steps)

+ 14 - 14
cli/core/display/icon_manager.py

@@ -18,17 +18,17 @@ class IconManager:
     """
     """
 
 
     # File Type Icons
     # File Type Icons
-    FILE_FOLDER = "\uf07b"  #
-    FILE_DEFAULT = "\uf15b"  #
-    FILE_YAML = "\uf15c"  #
-    FILE_JSON = "\ue60b"  #
-    FILE_MARKDOWN = "\uf48a"  #
-    FILE_JINJA2 = "\ue235"  #
-    FILE_DOCKER = "\uf308"  #
-    FILE_COMPOSE = "\uf308"  #
-    FILE_SHELL = "\uf489"  #
-    FILE_PYTHON = "\ue73c"  #
-    FILE_TEXT = "\uf15c"  #
+    FILE_FOLDER = "\uf07b"
+    FILE_DEFAULT = "\uf15b"
+    FILE_YAML = "\uf15c"
+    FILE_JSON = "\ue60b"
+    FILE_MARKDOWN = "\uf48a"
+    FILE_JINJA2 = "\ue235"
+    FILE_DOCKER = "\uf308"
+    FILE_COMPOSE = "\uf308"
+    FILE_SHELL = "\uf489"
+    FILE_PYTHON = "\ue73c"
+    FILE_TEXT = "\uf15c"
 
 
     # Status Indicators
     # Status Indicators
     STATUS_SUCCESS = "\uf00c"  #  (check)
     STATUS_SUCCESS = "\uf00c"  #  (check)
@@ -38,9 +38,9 @@ class IconManager:
     STATUS_SKIPPED = "\uf05e"  #  (ban/circle-slash)
     STATUS_SKIPPED = "\uf05e"  #  (ban/circle-slash)
 
 
     # UI Elements
     # UI Elements
-    UI_CONFIG = "\ue5fc"  #
-    UI_LOCK = "\uf084"  #
-    UI_SETTINGS = "\uf013"  #
+    UI_CONFIG = "\ue5fc"
+    UI_LOCK = "\uf084"
+    UI_SETTINGS = "\uf013"
     UI_ARROW_RIGHT = "\uf061"  #  (arrow-right)
     UI_ARROW_RIGHT = "\uf061"  #  (arrow-right)
     UI_BULLET = "\uf111"  #  (circle)
     UI_BULLET = "\uf111"  #  (circle)
     UI_LIBRARY_GIT = "\uf418"  #  (git icon)
     UI_LIBRARY_GIT = "\uf418"  #  (git icon)

+ 8 - 16
cli/core/display/status_display.py

@@ -9,9 +9,11 @@ from rich.panel import Panel
 from rich.prompt import Confirm
 from rich.prompt import Confirm
 from rich.syntax import Syntax
 from rich.syntax import Syntax
 
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..exceptions import TemplateRenderError
     from ..exceptions import TemplateRenderError
+    from . import DisplayManager
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 console_err = Console(stderr=True)  # Keep for error output
 console_err = Console(stderr=True)  # Keep for error output
@@ -24,7 +26,7 @@ class StatusDisplayManager:
     and informational messages with consistent formatting.
     and informational messages with consistent formatting.
     """
     """
 
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize StatusDisplayManager.
         """Initialize StatusDisplayManager.
 
 
         Args:
         Args:
@@ -42,8 +44,6 @@ class StatusDisplayManager:
             message: The message to display
             message: The message to display
             context: Optional context information
             context: Optional context information
         """
         """
-        from . import IconManager
-
         # Errors and warnings always go to stderr, even in quiet mode
         # Errors and warnings always go to stderr, even in quiet mode
         # Success and info respect quiet mode and go to stdout
         # Success and info respect quiet mode and go to stdout
         use_stderr = level in ("error", "warning")
         use_stderr = level in ("error", "warning")
@@ -66,13 +66,13 @@ class StatusDisplayManager:
         if context:
         if context:
             text = (
             text = (
                 f"{level.capitalize()} in {context}: {message}"
                 f"{level.capitalize()} in {context}: {message}"
-                if level == "error" or level == "warning"
+                if level in {"error", "warning"}
                 else f"{context}: {message}"
                 else f"{context}: {message}"
             )
             )
         else:
         else:
             text = (
             text = (
                 f"{level.capitalize()}: {message}"
                 f"{level.capitalize()}: {message}"
-                if level == "error" or level == "warning"
+                if level in {"error", "warning"}
                 else message
                 else message
             )
             )
 
 
@@ -146,8 +146,6 @@ class StatusDisplayManager:
             required_version: Minimum CLI version required by template
             required_version: Minimum CLI version required by template
             current_version: Current CLI version
             current_version: Current CLI version
         """
         """
-        from . import IconManager
-
         console_err.print()
         console_err.print()
         console_err.print(
         console_err.print(
             f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
             f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
@@ -179,8 +177,6 @@ class StatusDisplayManager:
             message: The main message to display
             message: The main message to display
             reason: Optional reason why it was skipped
             reason: Optional reason why it was skipped
         """
         """
-        from . import IconManager
-
         icon = IconManager.get_status_icon("skipped")
         icon = IconManager.get_status_icon("skipped")
         if reason:
         if reason:
             self.parent.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
             self.parent.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
@@ -200,8 +196,6 @@ class StatusDisplayManager:
         Returns:
         Returns:
             True if user confirms, False otherwise
             True if user confirms, False otherwise
         """
         """
-        from . import IconManager
-
         icon = IconManager.get_status_icon("warning")
         icon = IconManager.get_status_icon("warning")
         self.parent.text(f"\n{icon} {message}", style="yellow")
         self.parent.text(f"\n{icon} {message}", style="yellow")
 
 
@@ -212,7 +206,7 @@ class StatusDisplayManager:
         return Confirm.ask("Continue?", default=default)
         return Confirm.ask("Continue?", default=default)
 
 
     def display_template_render_error(
     def display_template_render_error(
-        self, error: "TemplateRenderError", context: str | None = None
+        self, error: TemplateRenderError, context: str | None = None
     ) -> None:
     ) -> None:
         """Display a detailed template rendering error with context and suggestions.
         """Display a detailed template rendering error with context and suggestions.
 
 
@@ -220,8 +214,6 @@ class StatusDisplayManager:
             error: TemplateRenderError exception with detailed error information
             error: TemplateRenderError exception with detailed error information
             context: Optional context information (e.g., template ID)
             context: Optional context information (e.g., template ID)
         """
         """
-        from . import IconManager
-
         # Always display errors to stderr
         # Always display errors to stderr
         icon = IconManager.get_status_icon("error")
         icon = IconManager.get_status_icon("error")
         if context:
         if context:
@@ -287,7 +279,7 @@ class StatusDisplayManager:
         # Display suggestions if available
         # Display suggestions if available
         if error.suggestions:
         if error.suggestions:
             console_err.print("[bold yellow]Suggestions:[/bold yellow]")
             console_err.print("[bold yellow]Suggestions:[/bold yellow]")
-            for i, suggestion in enumerate(error.suggestions, 1):
+            for _i, suggestion in enumerate(error.suggestions, 1):
                 bullet = IconManager.UI_BULLET
                 bullet = IconManager.UI_BULLET
                 console_err.print(f"  [yellow]{bullet}[/yellow] {suggestion}")
                 console_err.print(f"  [yellow]{bullet}[/yellow] {suggestion}")
             console_err.print()
             console_err.print()

+ 3 - 5
cli/core/display/table_display.py

@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
 from rich.table import Table
 from rich.table import Table
 from rich.tree import Tree
 from rich.tree import Tree
 
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from . import DisplayManager
     from . import DisplayManager
 
 
@@ -19,7 +21,7 @@ class TableDisplayManager:
     including templates lists, status tables, and summaries.
     including templates lists, status tables, and summaries.
     """
     """
 
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize TableDisplayManager.
         """Initialize TableDisplayManager.
 
 
         Args:
         Args:
@@ -89,8 +91,6 @@ class TableDisplayManager:
             rows: List of tuples (name, message, success_bool)
             rows: List of tuples (name, message, success_bool)
             columns: Column headers (name_header, status_header)
             columns: Column headers (name_header, status_header)
         """
         """
-        from . import IconManager
-
         table = Table(show_header=True)
         table = Table(show_header=True)
         table.add_column(columns[0], style="cyan", no_wrap=True)
         table.add_column(columns[0], style="cyan", no_wrap=True)
         table.add_column(columns[1])
         table.add_column(columns[1])
@@ -159,8 +159,6 @@ class TableDisplayManager:
             module_name: Name of the module
             module_name: Name of the module
             show_all: If True, show all details including descriptions
             show_all: If True, show all details including descriptions
         """
         """
-        from . import IconManager
-
         if not spec:
         if not spec:
             self.parent.text(
             self.parent.text(
                 f"No configuration found for module '{module_name}'", style="yellow"
                 f"No configuration found for module '{module_name}'", style="yellow"

+ 7 - 9
cli/core/display/template_display.py

@@ -3,9 +3,11 @@ from __future__ import annotations
 from pathlib import Path
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..template import Template
     from ..template import Template
+    from . import DisplayManager
 
 
 
 
 class TemplateDisplayManager:
 class TemplateDisplayManager:
@@ -15,7 +17,7 @@ class TemplateDisplayManager:
     file trees, and metadata.
     file trees, and metadata.
     """
     """
 
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize TemplateDisplayManager.
         """Initialize TemplateDisplayManager.
 
 
         Args:
         Args:
@@ -23,7 +25,7 @@ class TemplateDisplayManager:
         """
         """
         self.parent = parent
         self.parent = parent
 
 
-    def render_template(self, template: "Template", template_id: str) -> None:
+    def render_template(self, template: Template, template_id: str) -> None:
         """Display template information panel and variables table.
         """Display template information panel and variables table.
 
 
         Args:
         Args:
@@ -34,7 +36,7 @@ class TemplateDisplayManager:
         self.render_file_tree(template)
         self.render_file_tree(template)
         self.parent.variables.render_variables_table(template)
         self.parent.variables.render_variables_table(template)
 
 
-    def render_template_header(self, template: "Template", template_id: str) -> None:
+    def render_template_header(self, template: Template, template_id: str) -> None:
         """Display the header for a template with library information.
         """Display the header for a template with library information.
 
 
         Args:
         Args:
@@ -67,14 +69,12 @@ class TemplateDisplayManager:
         )
         )
         self.parent.text(description)
         self.parent.text(description)
 
 
-    def render_file_tree(self, template: "Template") -> None:
+    def render_file_tree(self, template: Template) -> None:
         """Display the file structure of a template.
         """Display the file structure of a template.
 
 
         Args:
         Args:
             template: Template instance
             template: Template instance
         """
         """
-        from . import IconManager
-
         self.parent.text("")
         self.parent.text("")
         self.parent.heading("Template File Structure")
         self.parent.heading("Template File Structure")
 
 
@@ -108,8 +108,6 @@ class TemplateDisplayManager:
             files: Dictionary of file paths to content
             files: Dictionary of file paths to content
             existing_files: List of existing files that will be overwritten
             existing_files: List of existing files that will be overwritten
         """
         """
-        from . import IconManager
-
         self.parent.text("")
         self.parent.text("")
         self.parent.text("Files to be generated:", style="bold")
         self.parent.text("Files to be generated:", style="bold")
 
 

+ 5 - 7
cli/core/display/variable_display.py

@@ -4,9 +4,11 @@ from typing import TYPE_CHECKING
 
 
 from rich.table import Table
 from rich.table import Table
 
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..template import Template
     from ..template import Template
+    from . import DisplayManager
 
 
 
 
 class VariableDisplayManager:
 class VariableDisplayManager:
@@ -16,7 +18,7 @@ class VariableDisplayManager:
     and their values with appropriate formatting based on context.
     and their values with appropriate formatting based on context.
     """
     """
 
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize VariableDisplayManager.
         """Initialize VariableDisplayManager.
 
 
         Args:
         Args:
@@ -42,8 +44,6 @@ class VariableDisplayManager:
         Returns:
         Returns:
             Formatted string representation of the variable value
             Formatted string representation of the variable value
         """
         """
-        from . import IconManager
-
         # Handle disabled bool variables
         # Handle disabled bool variables
         if (is_dimmed or not var_satisfied) and variable.type == "bool":
         if (is_dimmed or not var_satisfied) and variable.type == "bool":
             if (
             if (
@@ -171,8 +171,6 @@ class VariableDisplayManager:
         Returns:
         Returns:
             Tuple of (var_display, type, default_val, description, row_style)
             Tuple of (var_display, type, default_val, description, row_style)
         """
         """
-        from . import IconManager
-
         settings = self.parent.settings
         settings = self.parent.settings
 
 
         # Build row style
         # Build row style
@@ -200,7 +198,7 @@ class VariableDisplayManager:
             row_style,
             row_style,
         )
         )
 
 
-    def render_variables_table(self, template: "Template") -> None:
+    def render_variables_table(self, template: Template) -> None:
         """Display a table of variables for a template.
         """Display a table of variables for a template.
 
 
         All variables and sections are always shown. Disabled sections/variables
         All variables and sections are always shown. Disabled sections/variables

+ 9 - 11
cli/core/exceptions.py

@@ -4,8 +4,6 @@ This module defines specific exception types for better error handling
 and diagnostics throughout the application.
 and diagnostics throughout the application.
 """
 """
 
 
-from typing import Optional, List, Dict
-
 
 
 class BoilerplatesError(Exception):
 class BoilerplatesError(Exception):
     """Base exception for all boilerplates CLI errors."""
     """Base exception for all boilerplates CLI errors."""
@@ -34,7 +32,7 @@ class TemplateError(BoilerplatesError):
 class TemplateNotFoundError(TemplateError):
 class TemplateNotFoundError(TemplateError):
     """Raised when a template cannot be found."""
     """Raised when a template cannot be found."""
 
 
-    def __init__(self, template_id: str, module_name: Optional[str] = None):
+    def __init__(self, template_id: str, module_name: str | None = None):
         self.template_id = template_id
         self.template_id = template_id
         self.module_name = module_name
         self.module_name = module_name
         msg = f"Template '{template_id}' not found"
         msg = f"Template '{template_id}' not found"
@@ -64,7 +62,7 @@ class TemplateLoadError(TemplateError):
 class TemplateSyntaxError(TemplateError):
 class TemplateSyntaxError(TemplateError):
     """Raised when a Jinja2 template has syntax errors."""
     """Raised when a Jinja2 template has syntax errors."""
 
 
-    def __init__(self, template_id: str, errors: List[str]):
+    def __init__(self, template_id: str, errors: list[str]):
         self.template_id = template_id
         self.template_id = template_id
         self.errors = errors
         self.errors = errors
         msg = f"Jinja2 syntax errors in template '{template_id}':\n" + "\n".join(errors)
         msg = f"Jinja2 syntax errors in template '{template_id}':\n" + "\n".join(errors)
@@ -107,13 +105,13 @@ class TemplateRenderError(TemplateError):
     def __init__(
     def __init__(
         self,
         self,
         message: str,
         message: str,
-        file_path: Optional[str] = None,
-        line_number: Optional[int] = None,
-        column: Optional[int] = None,
-        context_lines: Optional[List[str]] = None,
-        variable_context: Optional[Dict[str, str]] = None,
-        suggestions: Optional[List[str]] = None,
-        original_error: Optional[Exception] = None,
+        file_path: str | None = None,
+        line_number: int | None = None,
+        column: int | None = None,
+        context_lines: list[str] | None = None,
+        variable_context: dict[str, str] | None = None,
+        suggestions: list[str] | None = None,
+        original_error: Exception | None = None,
     ):
     ):
         self.file_path = file_path
         self.file_path = file_path
         self.line_number = line_number
         self.line_number = line_number

+ 5 - 6
cli/core/input/prompt_manager.py

@@ -1,9 +1,10 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from typing import Dict, Any, Callable
 import logging
 import logging
+from typing import Any, Callable
+
 from rich.console import Console
 from rich.console import Console
-from rich.prompt import Prompt, Confirm, IntPrompt
+from rich.prompt import Confirm, IntPrompt, Prompt
 
 
 from .display import DisplayManager
 from .display import DisplayManager
 from .template import Variable, VariableCollection
 from .template import Variable, VariableCollection
@@ -31,7 +32,7 @@ class PromptHandler:
             logger.info("User opted to keep all default values")
             logger.info("User opted to keep all default values")
             return {}
             return {}
 
 
-        collected: Dict[str, Any] = {}
+        collected: dict[str, Any] = {}
         prompted_variables: set[str] = (
         prompted_variables: set[str] = (
             set()
             set()
         )  # Track which variables we've already prompted for
         )  # Track which variables we've already prompted for
@@ -167,9 +168,7 @@ class PromptHandler:
                 self._show_validation_error(str(exc))
                 self._show_validation_error(str(exc))
             except Exception as e:
             except Exception as e:
                 # Unexpected error — log and retry using the stored (unconverted) value
                 # Unexpected error — log and retry using the stored (unconverted) value
-                logger.error(
-                    f"Error prompting for variable '{variable.name}': {str(e)}"
-                )
+                logger.error(f"Error prompting for variable '{variable.name}': {e!s}")
                 default_value = variable.value
                 default_value = variable.value
                 handler = self._get_prompt_handler(variable)
                 handler = self._get_prompt_handler(variable)
 
 

+ 12 - 12
cli/core/library.py

@@ -1,14 +1,18 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from pathlib import Path
 import logging
 import logging
-from typing import Optional
+from pathlib import Path
+
 import yaml
 import yaml
 
 
-from .exceptions import LibraryError, TemplateNotFoundError, DuplicateTemplateError
+from .config import ConfigManager
+from .exceptions import DuplicateTemplateError, LibraryError, TemplateNotFoundError
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# Qualified ID format: "template_id.library_name"
+QUALIFIED_ID_PARTS = 2
+
 
 
 class Library:
 class Library:
     """Represents a single library with a specific path."""
     """Represents a single library with a specific path."""
@@ -45,12 +49,12 @@ class Library:
             return False
             return False
 
 
         try:
         try:
-            with open(template_file, "r", encoding="utf-8") as f:
+            with open(template_file, encoding="utf-8") as f:
                 docs = [doc for doc in yaml.safe_load_all(f) if doc]
                 docs = [doc for doc in yaml.safe_load_all(f) if doc]
                 return (
                 return (
                     docs[0].get("metadata", {}).get("draft", False) if docs else False
                     docs[0].get("metadata", {}).get("draft", False) if docs else False
                 )
                 )
-        except (yaml.YAMLError, IOError, OSError) as e:
+        except (yaml.YAMLError, OSError) as e:
             logger.warning(f"Error checking draft status for {template_path}: {e}")
             logger.warning(f"Error checking draft status for {template_path}: {e}")
             return False
             return False
 
 
@@ -137,7 +141,7 @@ class Library:
         except PermissionError as e:
         except PermissionError as e:
             raise LibraryError(
             raise LibraryError(
                 f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}"
                 f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}"
-            )
+            ) from e
 
 
         # Sort if requested
         # Sort if requested
         if sort_results:
         if sort_results:
@@ -152,8 +156,6 @@ class LibraryManager:
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
         """Initialize LibraryManager with git-based libraries from config."""
         """Initialize LibraryManager with git-based libraries from config."""
-        from .config import ConfigManager
-
         self.config = ConfigManager()
         self.config = ConfigManager()
         self.libraries = self._load_libraries_from_config()
         self.libraries = self._load_libraries_from_config()
 
 
@@ -246,9 +248,7 @@ class LibraryManager:
 
 
         return libraries
         return libraries
 
 
-    def find_by_id(
-        self, module_name: str, template_id: str
-    ) -> Optional[tuple[Path, str]]:
+    def find_by_id(self, module_name: str, template_id: str) -> tuple[Path, str] | None:
         """Find a template by its ID across all libraries.
         """Find a template by its ID across all libraries.
 
 
         Supports both simple IDs and qualified IDs (template.library format).
         Supports both simple IDs and qualified IDs (template.library format).
@@ -267,7 +267,7 @@ class LibraryManager:
         # Check if this is a qualified ID (contains '.')
         # Check if this is a qualified ID (contains '.')
         if "." in template_id:
         if "." in template_id:
             parts = template_id.rsplit(".", 1)
             parts = template_id.rsplit(".", 1)
-            if len(parts) == 2:
+            if len(parts) == QUALIFIED_ID_PARTS:
                 base_id, requested_lib = parts
                 base_id, requested_lib = parts
                 logger.debug(
                 logger.debug(
                     f"Parsing qualified ID: base='{base_id}', library='{requested_lib}'"
                     f"Parsing qualified ID: base='{base_id}', library='{requested_lib}'"

+ 170 - 178
cli/core/module/base_commands.py

@@ -5,26 +5,32 @@ from __future__ import annotations
 import logging
 import logging
 import os
 import os
 from pathlib import Path
 from pathlib import Path
-from typing import Dict, List, Optional
 
 
 from rich.prompt import Confirm
 from rich.prompt import Confirm
 from typer import Exit
 from typer import Exit
 
 
+from ..config import ConfigManager
 from ..display import DisplayManager
 from ..display import DisplayManager
 from ..exceptions import (
 from ..exceptions import (
     TemplateRenderError,
     TemplateRenderError,
     TemplateSyntaxError,
     TemplateSyntaxError,
     TemplateValidationError,
     TemplateValidationError,
 )
 )
+from ..template import Template
+from ..validators import get_validator_registry
 from .helpers import (
 from .helpers import (
-    apply_variable_defaults,
-    apply_var_file,
     apply_cli_overrides,
     apply_cli_overrides,
+    apply_var_file,
+    apply_variable_defaults,
     collect_variable_values,
     collect_variable_values,
 )
 )
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# File size thresholds for display formatting
+BYTES_PER_KB = 1024
+BYTES_PER_MB = 1024 * 1024
+
 
 
 def list_templates(module_instance, raw: bool = False) -> list:
 def list_templates(module_instance, raw: bool = False) -> list:
     """List all templates."""
     """List all templates."""
@@ -105,8 +111,6 @@ def show_template(module_instance, id: str) -> None:
     # Apply config defaults (same as in generate)
     # Apply config defaults (same as in generate)
     # This ensures the display shows the actual defaults that will be used
     # This ensures the display shows the actual defaults that will be used
     if template.variables:
     if template.variables:
-        from ..config import ConfigManager
-
         config = ConfigManager()
         config = ConfigManager()
         config_defaults = config.get_defaults(module_instance.name)
         config_defaults = config.get_defaults(module_instance.name)
 
 
@@ -130,10 +134,10 @@ def show_template(module_instance, id: str) -> None:
 
 
 def check_output_directory(
 def check_output_directory(
     output_dir: Path,
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     interactive: bool,
     interactive: bool,
     display: DisplayManager,
     display: DisplayManager,
-) -> Optional[List[Path]]:
+) -> list[Path] | None:
     """Check output directory for conflicts and get user confirmation if needed."""
     """Check output directory for conflicts and get user confirmation if needed."""
     dir_exists = output_dir.exists()
     dir_exists = output_dir.exists()
     dir_not_empty = dir_exists and any(output_dir.iterdir())
     dir_not_empty = dir_exists and any(output_dir.iterdir())
@@ -141,7 +145,7 @@ def check_output_directory(
     # Check which files already exist
     # Check which files already exist
     existing_files = []
     existing_files = []
     if dir_exists:
     if dir_exists:
-        for file_path in rendered_files.keys():
+        for file_path in rendered_files:
             full_path = output_dir / file_path
             full_path = output_dir / file_path
             if full_path.exists():
             if full_path.exists():
                 existing_files.append(full_path)
                 existing_files.append(full_path)
@@ -171,8 +175,8 @@ def check_output_directory(
 
 
 def get_generation_confirmation(
 def get_generation_confirmation(
     output_dir: Path,
     output_dir: Path,
-    rendered_files: Dict[str, str],
-    existing_files: Optional[List[Path]],
+    rendered_files: dict[str, str],
+    existing_files: list[Path] | None,
     dir_not_empty: bool,
     dir_not_empty: bool,
     dry_run: bool,
     dry_run: bool,
     interactive: bool,
     interactive: bool,
@@ -187,10 +191,13 @@ def get_generation_confirmation(
     )
     )
 
 
     # Final confirmation (only if we didn't already ask about overwriting)
     # Final confirmation (only if we didn't already ask about overwriting)
-    if not dir_not_empty and not dry_run:
-        if not Confirm.ask("Generate these files?", default=True):
-            display.display_info("Generation cancelled")
-            return False
+    if (
+        not dir_not_empty
+        and not dry_run
+        and not Confirm.ask("Generate these files?", default=True)
+    ):
+        display.display_info("Generation cancelled")
+        return False
 
 
     return True
     return True
 
 
@@ -198,7 +205,7 @@ def get_generation_confirmation(
 def execute_dry_run(
 def execute_dry_run(
     id: str,
     id: str,
     output_dir: Path,
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     show_files: bool,
     show_files: bool,
     display: DisplayManager,
     display: DisplayManager,
 ) -> None:
 ) -> None:
@@ -233,7 +240,7 @@ def execute_dry_run(
 
 
     # Collect unique subdirectories that would be created
     # Collect unique subdirectories that would be created
     subdirs = set()
     subdirs = set()
-    for file_path in rendered_files.keys():
+    for file_path in rendered_files:
         parts = Path(file_path).parts
         parts = Path(file_path).parts
         for i in range(1, len(parts)):
         for i in range(1, len(parts)):
             subdirs.add(Path(*parts[:i]))
             subdirs.add(Path(*parts[:i]))
@@ -274,12 +281,12 @@ def execute_dry_run(
     display.display_info("")
     display.display_info("")
 
 
     # Summary statistics
     # Summary statistics
-    if total_size < 1024:
+    if total_size < BYTES_PER_KB:
         size_str = f"{total_size}B"
         size_str = f"{total_size}B"
-    elif total_size < 1024 * 1024:
-        size_str = f"{total_size / 1024:.1f}KB"
+    elif total_size < BYTES_PER_MB:
+        size_str = f"{total_size / BYTES_PER_KB:.1f}KB"
     else:
     else:
-        size_str = f"{total_size / (1024 * 1024):.1f}MB"
+        size_str = f"{total_size / BYTES_PER_MB:.1f}MB"
 
 
     summary_items = {
     summary_items = {
         "Total files:": str(len(rendered_files)),
         "Total files:": str(len(rendered_files)),
@@ -312,7 +319,7 @@ def execute_dry_run(
 
 
 def write_generated_files(
 def write_generated_files(
     output_dir: Path,
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     quiet: bool,
     quiet: bool,
     display: DisplayManager,
     display: DisplayManager,
 ) -> None:
 ) -> None:
@@ -335,10 +342,10 @@ def write_generated_files(
 def generate_template(
 def generate_template(
     module_instance,
     module_instance,
     id: str,
     id: str,
-    directory: Optional[str],
+    directory: str | None,
     interactive: bool,
     interactive: bool,
-    var: Optional[list[str]],
-    var_file: Optional[str],
+    var: list[str] | None,
+    var_file: str | None,
     dry_run: bool,
     dry_run: bool,
     show_files: bool,
     show_files: bool,
     quiet: bool,
     quiet: bool,
@@ -354,8 +361,6 @@ def generate_template(
     template = module_instance._load_template_by_id(id)
     template = module_instance._load_template_by_id(id)
 
 
     # Apply defaults and overrides (in precedence order)
     # Apply defaults and overrides (in precedence order)
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
     apply_variable_defaults(template, config, module_instance.name)
     apply_variable_defaults(template, config, module_instance.name)
     apply_var_file(template, var_file, display)
     apply_var_file(template, var_file, display)
@@ -450,192 +455,179 @@ def generate_template(
     except TemplateRenderError as e:
     except TemplateRenderError as e:
         # Display enhanced error information for template rendering errors (always show errors)
         # Display enhanced error information for template rendering errors (always show errors)
         display.display_template_render_error(e, context=f"template '{id}'")
         display.display_template_render_error(e, context=f"template '{id}'")
-        raise Exit(code=1)
+        raise Exit(code=1) from None
     except Exception as e:
     except Exception as e:
         display.display_error(str(e), context=f"generating template '{id}'")
         display.display_error(str(e), context=f"generating template '{id}'")
-        raise Exit(code=1)
+        raise Exit(code=1) from None
 
 
 
 
 def validate_templates(
 def validate_templates(
     module_instance,
     module_instance,
     template_id: str,
     template_id: str,
-    path: Optional[str],
+    path: str | None,
     verbose: bool,
     verbose: bool,
     semantic: bool,
     semantic: bool,
 ) -> None:
 ) -> None:
     """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
     """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
-    from ..validators import get_validator_registry
+    # Load template based on input
+    template = _load_template_for_validation(module_instance, template_id, path)
 
 
-    # Validate from path takes precedence
-    if path:
-        try:
-            template_path = Path(path).resolve()
-            if not template_path.exists():
-                module_instance.display.display_error(f"Path does not exist: {path}")
-                raise Exit(code=1)
-            if not template_path.is_dir():
-                module_instance.display.display_error(
-                    f"Path is not a directory: {path}"
-                )
-                raise Exit(code=1)
+    if template:
+        _validate_single_template(module_instance, template, template_id, verbose, semantic)
+    else:
+        _validate_all_templates(module_instance, verbose)
 
 
-            module_instance.display.display_info(
-                f"[bold]Validating template from path:[/bold] [cyan]{template_path}[/cyan]"
-            )
-            from ..template import Template
 
 
-            template = Template(template_path, library_name="local")
-            template_id = template.id
+def _load_template_for_validation(module_instance, template_id: str, path: str | None):
+    """Load a template from path or ID for validation."""
+    if path:
+        template_path = Path(path).resolve()
+        if not template_path.exists():
+            module_instance.display.display_error(f"Path does not exist: {path}")
+            raise Exit(code=1) from None
+        if not template_path.is_dir():
+            module_instance.display.display_error(f"Path is not a directory: {path}")
+            raise Exit(code=1) from None
+
+        module_instance.display.display_info(
+            f"[bold]Validating template from path:[/bold] [cyan]{template_path}[/cyan]"
+        )
+        try:
+            return Template(template_path, library_name="local")
         except Exception as e:
         except Exception as e:
             module_instance.display.display_error(
             module_instance.display.display_error(
                 f"Failed to load template from path '{path}': {e}"
                 f"Failed to load template from path '{path}': {e}"
             )
             )
-            raise Exit(code=1)
-    elif template_id:
-        # Validate a specific template by ID
+            raise Exit(code=1) from None
+
+    if template_id:
         try:
         try:
             template = module_instance._load_template_by_id(template_id)
             template = module_instance._load_template_by_id(template_id)
             module_instance.display.display_info(
             module_instance.display.display_info(
                 f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]"
                 f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]"
             )
             )
+            return template
         except Exception as e:
         except Exception as e:
-            module_instance.display.display_error(
-                f"Failed to load template '{template_id}': {e}"
-            )
-            raise Exit(code=1)
-    else:
-        # Validate all templates - handled separately below
-        template = None
+            module_instance.display.display_error(f"Failed to load template '{template_id}': {e}")
+            raise Exit(code=1) from None
 
 
-    # Single template validation
-    if template:
-        try:
-            # Trigger validation by accessing used_variables
-            _ = template.used_variables
-            # Trigger variable definition validation by accessing variables
-            _ = template.variables
-            module_instance.display.display_success("Jinja2 validation passed")
+    return None
 
 
-            # Semantic validation
-            if semantic:
-                module_instance.display.display_info("")
-                module_instance.display.display_info(
-                    "[bold cyan]Running semantic validation...[/bold cyan]"
-                )
-                registry = get_validator_registry()
-                has_semantic_errors = False
 
 
-                # Render template with default values for validation
-                debug_mode = logger.isEnabledFor(logging.DEBUG)
-                rendered_files, _ = template.render(
-                    template.variables, debug=debug_mode
-                )
+def _validate_single_template(module_instance, template, template_id: str, verbose: bool, semantic: bool) -> None:
+    """Validate a single template."""
+    try:
+        # Jinja2 validation
+        _ = template.used_variables
+        _ = template.variables
+        module_instance.display.display_success("Jinja2 validation passed")
+
+        # Semantic validation
+        if semantic:
+            _run_semantic_validation(module_instance, template, verbose)
+
+        # Verbose output
+        if verbose:
+            _display_validation_details(module_instance, template, semantic)
+
+    except TemplateRenderError as e:
+        module_instance.display.display_template_render_error(
+            e, context=f"template '{template_id}'"
+        )
+        raise Exit(code=1) from None
+    except (TemplateSyntaxError, TemplateValidationError, ValueError) as e:
+        module_instance.display.display_error(f"Validation failed for '{template_id}':")
+        module_instance.display.display_info(f"\n{e}")
+        raise Exit(code=1) from None
+    except Exception as e:
+        module_instance.display.display_error(f"Unexpected error validating '{template_id}': {e}")
+        raise Exit(code=1) from None
+
 
 
-                for file_path, content in rendered_files.items():
-                    result = registry.validate_file(content, file_path)
+def _run_semantic_validation(module_instance, template, verbose: bool) -> None:
+    """Run semantic validation on rendered template files."""
+    module_instance.display.display_info("")
+    module_instance.display.display_info("[bold cyan]Running semantic validation...[/bold cyan]")
 
 
-                    if result.errors or result.warnings or (verbose and result.info):
-                        module_instance.display.display_info(
-                            f"\n[cyan]File:[/cyan] {file_path}"
-                        )
-                        result.display(f"{file_path}")
+    registry = get_validator_registry()
+    debug_mode = logger.isEnabledFor(logging.DEBUG)
+    rendered_files, _ = template.render(template.variables, debug=debug_mode)
 
 
-                        if result.errors:
-                            has_semantic_errors = True
+    has_semantic_errors = False
+    for file_path, content in rendered_files.items():
+        result = registry.validate_file(content, file_path)
+
+        if result.errors or result.warnings or (verbose and result.info):
+            module_instance.display.display_info(f"\n[cyan]File:[/cyan] {file_path}")
+            result.display(f"{file_path}")
+
+            if result.errors:
+                has_semantic_errors = True
+
+    if has_semantic_errors:
+        module_instance.display.display_error("Semantic validation found errors")
+        raise Exit(code=1) from None
+
+    module_instance.display.display_success("Semantic validation passed")
+
+
+def _display_validation_details(module_instance, template, semantic: bool) -> None:
+    """Display verbose validation details."""
+    module_instance.display.display_info(f"\n[dim]Template path: {template.template_dir}[/dim]")
+    module_instance.display.display_info(f"[dim]Found {len(template.used_variables)} variables[/dim]")
+    if semantic:
+        debug_mode = logger.isEnabledFor(logging.DEBUG)
+        rendered_files, _ = template.render(template.variables, debug=debug_mode)
+        module_instance.display.display_info(f"[dim]Generated {len(rendered_files)} files[/dim]")
+
+
+def _validate_all_templates(module_instance, verbose: bool) -> None:
+    """Validate all templates in the module."""
+    module_instance.display.display_info(
+        f"[bold]Validating all {module_instance.name} templates...[/bold]"
+    )
 
 
-                if not has_semantic_errors:
-                    module_instance.display.display_success(
-                        "Semantic validation passed"
-                    )
-                else:
-                    module_instance.display.display_error(
-                        "Semantic validation found errors"
-                    )
-                    raise Exit(code=1)
+    valid_count = 0
+    invalid_count = 0
+    errors = []
 
 
+    all_templates = module_instance._load_all_templates()
+    total = len(all_templates)
+
+    for template in all_templates:
+        try:
+            _ = template.used_variables
+            _ = template.variables
+            valid_count += 1
             if verbose:
             if verbose:
-                module_instance.display.display_info(
-                    f"\n[dim]Template path: {template.template_dir}[/dim]"
-                )
-                module_instance.display.display_info(
-                    f"[dim]Found {len(template.used_variables)} variables[/dim]"
-                )
-                if semantic:
-                    module_instance.display.display_info(
-                        f"[dim]Generated {len(rendered_files)} files[/dim]"
-                    )
-
-        except TemplateRenderError as e:
-            # Display enhanced error information for template rendering errors
-            module_instance.display.display_template_render_error(
-                e, context=f"template '{template_id}'"
-            )
-            raise Exit(code=1)
-        except (TemplateSyntaxError, TemplateValidationError, ValueError) as e:
-            module_instance.display.display_error(
-                f"Validation failed for '{template_id}':"
-            )
-            module_instance.display.display_info(f"\n{e}")
-            raise Exit(code=1)
+                module_instance.display.display_success(template.id)
+        except ValueError as e:
+            invalid_count += 1
+            errors.append((template.id, str(e)))
+            if verbose:
+                module_instance.display.display_error(template.id)
         except Exception as e:
         except Exception as e:
-            module_instance.display.display_error(
-                f"Unexpected error validating '{template_id}': {e}"
-            )
-            raise Exit(code=1)
+            invalid_count += 1
+            errors.append((template.id, f"Load error: {e}"))
+            if verbose:
+                module_instance.display.display_warning(template.id)
 
 
-        return
-    else:
-        # Validate all templates
-        module_instance.display.display_info(
-            f"[bold]Validating all {module_instance.name} templates...[/bold]"
-        )
+    # Display summary
+    summary_items = {
+        "Total templates:": str(total),
+        "[green]Valid:[/green]": str(valid_count),
+        "[red]Invalid:[/red]": str(invalid_count),
+    }
+    module_instance.display.display_summary_table("Validation Summary", summary_items)
 
 
-        valid_count = 0
-        invalid_count = 0
-        errors = []
-
-        # Use centralized helper to load all templates
-        all_templates = module_instance._load_all_templates()
-        total = len(all_templates)
-
-        for template in all_templates:
-            try:
-                # Trigger validation
-                _ = template.used_variables
-                _ = template.variables
-                valid_count += 1
-                if verbose:
-                    module_instance.display.display_success(template.id)
-            except ValueError as e:
-                invalid_count += 1
-                errors.append((template.id, str(e)))
-                if verbose:
-                    module_instance.display.display_error(template.id)
-            except Exception as e:
-                invalid_count += 1
-                errors.append((template.id, f"Load error: {e}"))
-                if verbose:
-                    module_instance.display.display_warning(template.id)
-
-        # Summary
-        summary_items = {
-            "Total templates:": str(total),
-            "[green]Valid:[/green]": str(valid_count),
-            "[red]Invalid:[/red]": str(invalid_count),
-        }
-        module_instance.display.display_summary_table(
-            "Validation Summary", summary_items
-        )
+    if errors:
+        module_instance.display.display_info("")
+        module_instance.display.display_error("Validation Errors:")
+        for template_id, error_msg in errors:
+            module_instance.display.display_info(
+                f"\n[yellow]Template:[/yellow] [cyan]{template_id}[/cyan]"
+            )
+            module_instance.display.display_info(f"[dim]{error_msg}[/dim]")
+        raise Exit(code=1)
 
 
-        # Show errors if any
-        if errors:
-            module_instance.display.display_info("")
-            module_instance.display.display_error("Validation Errors:")
-            for template_id, error_msg in errors:
-                module_instance.display.display_info(
-                    f"\n[yellow]Template:[/yellow] [cyan]{template_id}[/cyan]"
-                )
-                module_instance.display.display_info(f"[dim]{error_msg}[/dim]")
-            raise Exit(code=1)
-        else:
-            module_instance.display.display_success("All templates are valid!")
+    module_instance.display.display_success("All templates are valid!")

+ 90 - 80
cli/core/module/base_module.py

@@ -4,40 +4,54 @@ from __future__ import annotations
 
 
 import logging
 import logging
 from abc import ABC
 from abc import ABC
-from typing import Optional
+from typing import Annotated
 
 
 from typer import Argument, Option, Typer
 from typer import Argument, Option, Typer
 
 
 from ..display import DisplayManager
 from ..display import DisplayManager
 from ..library import LibraryManager
 from ..library import LibraryManager
+from ..template import Template
 from .base_commands import (
 from .base_commands import (
+    generate_template,
     list_templates,
     list_templates,
     search_templates,
     search_templates,
     show_template,
     show_template,
-    generate_template,
     validate_templates,
     validate_templates,
 )
 )
 from .config_commands import (
 from .config_commands import (
-    config_get,
-    config_set,
-    config_remove,
     config_clear,
     config_clear,
+    config_get,
     config_list,
     config_list,
+    config_remove,
+    config_set,
 )
 )
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
+# Expected length of library entry tuple: (path, library_name, needs_qualification)
+LIBRARY_ENTRY_MIN_LENGTH = 2
+
 
 
 class Module(ABC):
 class Module(ABC):
-    """Streamlined base module that auto-detects variables from templates."""
+    """Streamlined base module that auto-detects variables from templates.
+
+    Subclasses must define:
+    - name: str (class attribute)
+    - description: str (class attribute)
+    """
+
+    # Class attributes that must be defined by subclasses
+    name: str
+    description: str
 
 
     # Schema version supported by this module (override in subclasses)
     # Schema version supported by this module (override in subclasses)
     schema_version: str = "1.0"
     schema_version: str = "1.0"
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
-        if not all([self.name, self.description]):
-            raise ValueError(
-                f"Module {self.__class__.__name__} must define name and description"
+        # Validate required class attributes
+        if not hasattr(self.__class__, 'name') or not hasattr(self.__class__, 'description'):
+            raise TypeError(
+                f"Module {self.__class__.__name__} must define 'name' and 'description' class attributes"
             )
             )
 
 
         logger.info(f"Initializing module '{self.name}'")
         logger.info(f"Initializing module '{self.name}'")
@@ -49,8 +63,6 @@ class Module(ABC):
 
 
     def _load_all_templates(self, filter_fn=None) -> list:
     def _load_all_templates(self, filter_fn=None) -> list:
         """Load all templates for this module with optional filtering."""
         """Load all templates for this module with optional filtering."""
-        from ..template import Template
-
         templates = []
         templates = []
         entries = self.libraries.find(self.name, sort_results=True)
         entries = self.libraries.find(self.name, sort_results=True)
 
 
@@ -58,7 +70,9 @@ class Module(ABC):
             # Unpack entry - returns (path, library_name, needs_qualification)
             # Unpack entry - returns (path, library_name, needs_qualification)
             template_dir = entry[0]
             template_dir = entry[0]
             library_name = entry[1]
             library_name = entry[1]
-            needs_qualification = entry[2] if len(entry) > 2 else False
+            needs_qualification = (
+                entry[2] if len(entry) > LIBRARY_ENTRY_MIN_LENGTH else False
+            )
 
 
             try:
             try:
                 # Get library object to determine type
                 # Get library object to determine type
@@ -95,8 +109,6 @@ class Module(ABC):
 
 
     def _load_template_by_id(self, id: str):
     def _load_template_by_id(self, id: str):
         """Load a template by its ID, supporting qualified IDs."""
         """Load a template by its ID, supporting qualified IDs."""
-        from ..template import Template
-
         logger.debug(f"Loading template with ID '{id}' from module '{self.name}'")
         logger.debug(f"Loading template with ID '{id}' from module '{self.name}'")
 
 
         # find_by_id now handles both simple and qualified IDs
         # find_by_id now handles both simple and qualified IDs
@@ -136,15 +148,16 @@ class Module(ABC):
 
 
     def list(
     def list(
         self,
         self,
-        raw: bool = Option(
-            False, "--raw", help="Output raw list format instead of rich table"
-        ),
+        raw: Annotated[
+            bool, Option("--raw", help="Output raw list format instead of rich table")
+        ] = False,
     ) -> list:
     ) -> list:
         """List all templates."""
         """List all templates."""
         return list_templates(self, raw)
         return list_templates(self, raw)
 
 
     def search(
     def search(
-        self, query: str = Argument(..., help="Search string to filter templates by ID")
+        self,
+        query: Annotated[str, Argument(help="Search string to filter templates by ID")],
     ) -> list:
     ) -> list:
         """Search for templates by ID containing the search string."""
         """Search for templates by ID containing the search string."""
         return search_templates(self, query)
         return search_templates(self, query)
@@ -155,39 +168,47 @@ class Module(ABC):
 
 
     def generate(
     def generate(
         self,
         self,
-        id: str = Argument(..., help="Template ID"),
-        directory: Optional[str] = Argument(
-            None, help="Output directory (defaults to template ID)"
-        ),
-        interactive: bool = Option(
-            True,
-            "--interactive/--no-interactive",
-            "-i/-n",
-            help="Enable interactive prompting for variables",
-        ),
-        var: Optional[list[str]] = Option(
-            None,
-            "--var",
-            "-v",
-            help="Variable override (repeatable). Supports: KEY=VALUE or KEY VALUE",
-        ),
-        var_file: Optional[str] = Option(
-            None,
-            "--var-file",
-            "-f",
-            help="Load variables from YAML file (overrides config defaults, overridden by --var)",
-        ),
-        dry_run: bool = Option(
-            False, "--dry-run", help="Preview template generation without writing files"
-        ),
-        show_files: bool = Option(
-            False,
-            "--show-files",
-            help="Display generated file contents in plain text (use with --dry-run)",
-        ),
-        quiet: bool = Option(
-            False, "--quiet", "-q", help="Suppress all non-error output"
-        ),
+        id: Annotated[str, Argument(help="Template ID")],
+        directory: Annotated[
+            str | None, Argument(help="Output directory (defaults to template ID)")
+        ] = None,
+        interactive: Annotated[
+            bool,
+            Option(
+                "--interactive/--no-interactive",
+                "-i/-n",
+                help="Enable interactive prompting for variables",
+            ),
+        ] = True,
+        var: Annotated[
+            list[str] | None,
+            Option(
+                "--var",
+                "-v",
+                help="Variable override (repeatable). Supports: KEY=VALUE or KEY VALUE",
+            ),
+        ] = None,
+        var_file: Annotated[
+            str | None,
+            Option(
+                "--var-file",
+                "-f",
+                help="Load variables from YAML file (overrides config defaults, overridden by --var)",
+            ),
+        ] = None,
+        dry_run: Annotated[
+            bool, Option("--dry-run", help="Preview template generation without writing files")
+        ] = False,
+        show_files: Annotated[
+            bool,
+            Option(
+                "--show-files",
+                help="Display generated file contents in plain text (use with --dry-run)",
+            ),
+        ] = False,
+        quiet: Annotated[
+            bool, Option("--quiet", "-q", help="Suppress all non-error output")
+        ] = False,
     ) -> None:
     ) -> None:
         """Generate from template.
         """Generate from template.
 
 
@@ -204,59 +225,48 @@ class Module(ABC):
 
 
     def validate(
     def validate(
         self,
         self,
-        template_id: str = Argument(
-            None, help="Template ID to validate (if omitted, validates all templates)"
-        ),
-        path: Optional[str] = Option(
-            None,
-            "--path",
-            "-p",
-            help="Validate a template from a specific directory path",
-        ),
-        verbose: bool = Option(
-            False, "--verbose", "-v", help="Show detailed validation information"
-        ),
-        semantic: bool = Option(
-            True,
-            "--semantic/--no-semantic",
-            help="Enable semantic validation (Docker Compose schema, etc.)",
-        ),
+        template_id: str | None = None,
+        path: str | None = None,
+        verbose: Annotated[
+            bool, Option("--verbose", "-v", help="Show detailed validation information")
+        ] = False,
+        semantic: Annotated[
+            bool,
+            Option(
+                "--semantic/--no-semantic",
+                help="Enable semantic validation (Docker Compose schema, etc.)",
+            ),
+        ] = True,
     ) -> None:
     ) -> None:
         """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
         """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
         return validate_templates(self, template_id, path, verbose, semantic)
         return validate_templates(self, template_id, path, verbose, semantic)
 
 
     def config_get(
     def config_get(
         self,
         self,
-        var_name: Optional[str] = Argument(
-            None, help="Variable name to get (omit to show all defaults)"
-        ),
+        var_name: str | None = None,
     ) -> None:
     ) -> None:
         """Get default value(s) for this module."""
         """Get default value(s) for this module."""
         return config_get(self, var_name)
         return config_get(self, var_name)
 
 
     def config_set(
     def config_set(
         self,
         self,
-        var_name: str = Argument(..., help="Variable name or var=value format"),
-        value: Optional[str] = Argument(
-            None, help="Default value (not needed if using var=value format)"
-        ),
+        var_name: str,
+        value: str | None = None,
     ) -> None:
     ) -> None:
         """Set a default value for a variable."""
         """Set a default value for a variable."""
         return config_set(self, var_name, value)
         return config_set(self, var_name, value)
 
 
     def config_remove(
     def config_remove(
         self,
         self,
-        var_name: str = Argument(..., help="Variable name to remove"),
+        var_name: Annotated[str, Argument(help="Variable name to remove")],
     ) -> None:
     ) -> None:
         """Remove a specific default variable value."""
         """Remove a specific default variable value."""
         return config_remove(self, var_name)
         return config_remove(self, var_name)
 
 
     def config_clear(
     def config_clear(
         self,
         self,
-        var_name: Optional[str] = Argument(
-            None, help="Variable name to clear (omit to clear all defaults)"
-        ),
-        force: bool = Option(False, "--force", "-f", help="Skip confirmation prompt"),
+        var_name: str | None = None,
+        force: bool = False,
     ) -> None:
     ) -> None:
         """Clear default value(s) for this module."""
         """Clear default value(s) for this module."""
         return config_clear(self, var_name, force)
         return config_clear(self, var_name, force)

+ 16 - 31
cli/core/module/config_commands.py

@@ -3,18 +3,16 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import logging
 import logging
-from typing import Optional
 
 
 from rich.prompt import Confirm
 from rich.prompt import Confirm
 from typer import Exit
 from typer import Exit
 
 
-logger = logging.getLogger(__name__)
+from ..config import ConfigManager
 
 
+logger = logging.getLogger(__name__)
 
 
-def config_get(module_instance, var_name: Optional[str] = None) -> None:
+def config_get(module_instance, var_name: str | None = None) -> None:
     """Get default value(s) for this module."""
     """Get default value(s) for this module."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
 
 
     if var_name:
     if var_name:
@@ -36,9 +34,9 @@ def config_get(module_instance, var_name: Optional[str] = None) -> None:
             module_instance.display.display_info(
             module_instance.display.display_info(
                 f"[bold]Config defaults for module '{module_instance.name}':[/bold]"
                 f"[bold]Config defaults for module '{module_instance.name}':[/bold]"
             )
             )
-            for var_name, var_value in defaults.items():
+            for config_var_name, var_value in defaults.items():
                 module_instance.display.display_info(
                 module_instance.display.display_info(
-                    f"  [green]{var_name}[/green] = [yellow]{var_value}[/yellow]"
+                    f"  [green]{config_var_name}[/green] = [yellow]{var_value}[/yellow]"
                 )
                 )
         else:
         else:
             module_instance.display.display_warning(
             module_instance.display.display_warning(
@@ -46,10 +44,8 @@ def config_get(module_instance, var_name: Optional[str] = None) -> None:
             )
             )
 
 
 
 
-def config_set(module_instance, var_name: str, value: Optional[str] = None) -> None:
+def config_set(module_instance, var_name: str, value: str | None = None) -> None:
     """Set a default value for a variable."""
     """Set a default value for a variable."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
 
 
     # Parse var_name and value - support both "var value" and "var=value" formats
     # Parse var_name and value - support both "var value" and "var=value" formats
@@ -83,8 +79,6 @@ def config_set(module_instance, var_name: str, value: Optional[str] = None) -> N
 
 
 def config_remove(module_instance, var_name: str) -> None:
 def config_remove(module_instance, var_name: str) -> None:
     """Remove a specific default variable value."""
     """Remove a specific default variable value."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
     defaults = config.get_defaults(module_instance.name)
     defaults = config.get_defaults(module_instance.name)
 
 
@@ -105,11 +99,9 @@ def config_remove(module_instance, var_name: str) -> None:
 
 
 
 
 def config_clear(
 def config_clear(
-    module_instance, var_name: Optional[str] = None, force: bool = False
+    module_instance, var_name: str | None = None, force: bool = False
 ) -> None:
 ) -> None:
     """Clear default value(s) for this module."""
     """Clear default value(s) for this module."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
     defaults = config.get_defaults(module_instance.name)
     defaults = config.get_defaults(module_instance.name)
 
 
@@ -136,9 +128,9 @@ def config_clear(
                 f"This will clear ALL defaults for module '{module_instance.name}':",
                 f"This will clear ALL defaults for module '{module_instance.name}':",
                 "",
                 "",
             ]
             ]
-            for var_name, var_value in defaults.items():
+            for clear_var_name, var_value in defaults.items():
                 detail_lines.append(
                 detail_lines.append(
-                    f"  [green]{var_name}[/green] = [yellow]{var_value}[/yellow]"
+                    f"  [green]{clear_var_name}[/green] = [yellow]{var_value}[/yellow]"
                 )
                 )
 
 
             module_instance.display.display_warning(
             module_instance.display.display_warning(
@@ -162,8 +154,6 @@ def config_clear(
 
 
 def config_list(module_instance) -> None:
 def config_list(module_instance) -> None:
     """Display the defaults for this specific module as a table."""
     """Display the defaults for this specific module as a table."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     config = ConfigManager()
 
 
     # Get only the defaults for this module
     # Get only the defaults for this module
@@ -175,16 +165,11 @@ def config_list(module_instance) -> None:
         )
         )
         return
         return
 
 
-    # Display defaults using table primitive
-    from rich.table import Table
-
-    settings = module_instance.display.settings
-
-    table = Table(show_header=True, header_style=settings.STYLE_TABLE_HEADER)
-    table.add_column("Variable", style=settings.STYLE_VAR_COL_NAME, no_wrap=True)
-    table.add_column("Value", style=settings.STYLE_VAR_COL_DEFAULT)
-
-    for var_name, var_value in defaults.items():
-        table.add_row(var_name, str(var_value))
+    # Display defaults using DisplayManager
+    module_instance.display.display_info(
+        f"[bold]Defaults for module '{module_instance.name}':[/bold]\n"
+    )
 
 
-    module_instance.display._print_table(table)
+    # Convert defaults to display format (key: value pairs)
+    items = {f"{var_name}:": str(var_value) for var_name, var_value in defaults.items()}
+    module_instance.display.display_summary_table("", items)

+ 13 - 15
cli/core/module/helpers.py

@@ -4,18 +4,19 @@ from __future__ import annotations
 
 
 import logging
 import logging
 from pathlib import Path
 from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any
 
 
+import click
 import yaml
 import yaml
 from typer import Exit
 from typer import Exit
 
 
 from ..display import DisplayManager
 from ..display import DisplayManager
-from ..prompt import PromptHandler
+from ..input import PromptHandler
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def parse_var_inputs(var_options: List[str], extra_args: List[str]) -> Dict[str, Any]:
+def parse_var_inputs(var_options: list[str], extra_args: list[str]) -> dict[str, Any]:
     """Parse variable inputs from --var options and extra args.
     """Parse variable inputs from --var options and extra args.
 
 
     Supports formats:
     Supports formats:
@@ -36,12 +37,11 @@ def parse_var_inputs(var_options: List[str], extra_args: List[str]) -> Dict[str,
         if "=" in var_option:
         if "=" in var_option:
             key, value = var_option.split("=", 1)
             key, value = var_option.split("=", 1)
             variables[key] = value
             variables[key] = value
+        # --var KEY VALUE format - value should be in extra_args
+        elif extra_args:
+            variables[var_option] = extra_args.pop(0)
         else:
         else:
-            # --var KEY VALUE format - value should be in extra_args
-            if extra_args:
-                variables[var_option] = extra_args.pop(0)
-            else:
-                logger.warning(f"No value provided for variable '{var_option}'")
+            logger.warning(f"No value provided for variable '{var_option}'")
 
 
     return variables
     return variables
 
 
@@ -68,11 +68,11 @@ def load_var_file(var_file_path: str) -> dict:
         raise ValueError(f"Variable file path is not a file: {var_file_path}")
         raise ValueError(f"Variable file path is not a file: {var_file_path}")
 
 
     try:
     try:
-        with open(var_path, "r", encoding="utf-8") as f:
+        with open(var_path, encoding="utf-8") as f:
             content = yaml.safe_load(f)
             content = yaml.safe_load(f)
     except yaml.YAMLError as e:
     except yaml.YAMLError as e:
         raise ValueError(f"Invalid YAML in variable file: {e}") from e
         raise ValueError(f"Invalid YAML in variable file: {e}") from e
-    except (IOError, OSError) as e:
+    except OSError as e:
         raise ValueError(f"Error reading variable file: {e}") from e
         raise ValueError(f"Error reading variable file: {e}") from e
 
 
     if not isinstance(content, dict):
     if not isinstance(content, dict):
@@ -107,7 +107,7 @@ def apply_variable_defaults(template, config_manager, module_name: str) -> None:
 
 
 
 
 def apply_var_file(
 def apply_var_file(
-    template, var_file_path: Optional[str], display: DisplayManager
+    template, var_file_path: str | None, display: DisplayManager
 ) -> None:
 ) -> None:
     """Apply variables from a YAML file to template.
     """Apply variables from a YAML file to template.
 
 
@@ -149,7 +149,7 @@ def apply_var_file(
         raise Exit(code=1) from e
         raise Exit(code=1) from e
 
 
 
 
-def apply_cli_overrides(template, var: Optional[List[str]], ctx=None) -> None:
+def apply_cli_overrides(template, var: list[str] | None, ctx=None) -> None:
     """Apply CLI variable overrides to template.
     """Apply CLI variable overrides to template.
 
 
     Args:
     Args:
@@ -162,8 +162,6 @@ def apply_cli_overrides(template, var: Optional[List[str]], ctx=None) -> None:
 
 
     # Get context if not provided (compatible with all Typer versions)
     # Get context if not provided (compatible with all Typer versions)
     if ctx is None:
     if ctx is None:
-        import click
-
         try:
         try:
             ctx = click.get_current_context()
             ctx = click.get_current_context()
         except RuntimeError:
         except RuntimeError:
@@ -181,7 +179,7 @@ def apply_cli_overrides(template, var: Optional[List[str]], ctx=None) -> None:
             )
             )
 
 
 
 
-def collect_variable_values(template, interactive: bool) -> Dict[str, Any]:
+def collect_variable_values(template, interactive: bool) -> dict[str, Any]:
     """Collect variable values from user prompts and template defaults.
     """Collect variable values from user prompts and template defaults.
 
 
     Args:
     Args:

+ 5 - 6
cli/core/prompt.py

@@ -1,9 +1,10 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from typing import Dict, Any, Callable
 import logging
 import logging
+from typing import Any, Callable
+
 from rich.console import Console
 from rich.console import Console
-from rich.prompt import Prompt, Confirm, IntPrompt
+from rich.prompt import Confirm, IntPrompt, Prompt
 
 
 from .display import DisplayManager
 from .display import DisplayManager
 from .template import Variable, VariableCollection
 from .template import Variable, VariableCollection
@@ -31,7 +32,7 @@ class PromptHandler:
             logger.info("User opted to keep all default values")
             logger.info("User opted to keep all default values")
             return {}
             return {}
 
 
-        collected: Dict[str, Any] = {}
+        collected: dict[str, Any] = {}
         prompted_variables: set[str] = (
         prompted_variables: set[str] = (
             set()
             set()
         )  # Track which variables we've already prompted for
         )  # Track which variables we've already prompted for
@@ -167,9 +168,7 @@ class PromptHandler:
                 self._show_validation_error(str(exc))
                 self._show_validation_error(str(exc))
             except Exception as e:
             except Exception as e:
                 # Unexpected error — log and retry using the stored (unconverted) value
                 # Unexpected error — log and retry using the stored (unconverted) value
-                logger.error(
-                    f"Error prompting for variable '{variable.name}': {str(e)}"
-                )
+                logger.error(f"Error prompting for variable '{variable.name}': {e!s}")
                 default_value = variable.value
                 default_value = variable.value
                 handler = self._get_prompt_handler(variable)
                 handler = self._get_prompt_handler(variable)
 
 

+ 3 - 3
cli/core/registry.py

@@ -3,7 +3,7 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import logging
 import logging
-from typing import Iterator, Type
+from collections.abc import Iterator
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -15,7 +15,7 @@ class ModuleRegistry:
         self._modules = {}
         self._modules = {}
         logger.debug("Initializing module registry")
         logger.debug("Initializing module registry")
 
 
-    def register(self, module_class: Type) -> None:
+    def register(self, module_class: type) -> None:
         """Register a module class."""
         """Register a module class."""
         # Module class defines its own name attribute
         # Module class defines its own name attribute
         logger.debug(f"Attempting to register module class '{module_class.name}'")
         logger.debug(f"Attempting to register module class '{module_class.name}'")
@@ -33,7 +33,7 @@ class ModuleRegistry:
             f"Module '{module_class.name}' details: description='{module_class.description}'"
             f"Module '{module_class.name}' details: description='{module_class.description}'"
         )
         )
 
 
-    def iter_module_classes(self) -> Iterator[tuple[str, Type]]:
+    def iter_module_classes(self) -> Iterator[tuple[str, type]]:
         """Yield registered module classes without instantiating them."""
         """Yield registered module classes without instantiating them."""
         logger.debug(f"Iterating over {len(self._modules)} registered module classes")
         logger.debug(f"Iterating over {len(self._modules)} registered module classes")
         for name in sorted(self._modules.keys()):
         for name in sorted(self._modules.keys()):

+ 110 - 127
cli/core/repo.py

@@ -3,9 +3,9 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 import logging
 import logging
+import shutil
 import subprocess
 import subprocess
 from pathlib import Path
 from pathlib import Path
-from typing import Optional
 
 
 from rich.progress import SpinnerColumn, TextColumn
 from rich.progress import SpinnerColumn, TextColumn
 from rich.table import Table
 from rich.table import Table
@@ -21,9 +21,7 @@ display = DisplayManager()
 app = Typer(help="Manage library repositories")
 app = Typer(help="Manage library repositories")
 
 
 
 
-def _run_git_command(
-    args: list[str], cwd: Optional[Path] = None
-) -> tuple[bool, str, str]:
+def _run_git_command(args: list[str], cwd: Path | None = None) -> tuple[bool, str, str]:
     """Run a git command and return the result.
     """Run a git command and return the result.
 
 
     Args:
     Args:
@@ -35,7 +33,8 @@ def _run_git_command(
     """
     """
     try:
     try:
         result = subprocess.run(
         result = subprocess.run(
-            ["git"] + args,
+            ["git", *args],
+            check=False,
             cwd=cwd,
             cwd=cwd,
             capture_output=True,
             capture_output=True,
             text=True,
             text=True,
@@ -54,8 +53,8 @@ def _clone_or_pull_repo(
     name: str,
     name: str,
     url: str,
     url: str,
     target_path: Path,
     target_path: Path,
-    branch: Optional[str] = None,
-    sparse_dir: Optional[str] = None,
+    branch: str | None = None,
+    sparse_dir: str | None = None,
 ) -> tuple[bool, str]:
 ) -> tuple[bool, str]:
     """Clone or pull a git repository with optional sparse-checkout.
     """Clone or pull a git repository with optional sparse-checkout.
 
 
@@ -70,122 +69,117 @@ def _clone_or_pull_repo(
         Tuple of (success, message)
         Tuple of (success, message)
     """
     """
     if target_path.exists() and (target_path / ".git").exists():
     if target_path.exists() and (target_path / ".git").exists():
-        # Repository exists, pull updates
-        logger.debug(f"Pulling updates for library '{name}' at {target_path}")
+        return _pull_repo_updates(name, target_path, branch)
+    else:
+        return _clone_new_repo(name, url, target_path, branch, sparse_dir)
 
 
-        # Determine which branch to pull
-        pull_branch = branch if branch else "main"
 
 
-        # Pull updates from specific branch
-        success, stdout, stderr = _run_git_command(
-            ["pull", "--ff-only", "origin", pull_branch], cwd=target_path
-        )
+def _pull_repo_updates(name: str, target_path: Path, branch: str | None) -> tuple[bool, str]:
+    """Pull updates for an existing repository."""
+    logger.debug(f"Pulling updates for library '{name}' at {target_path}")
 
 
-        if success:
-            # Check if anything was updated
-            if "Already up to date" in stdout or "Already up-to-date" in stdout:
-                return True, "Already up to date"
-            else:
-                return True, "Updated successfully"
-        else:
-            error_msg = stderr or stdout
-            logger.error(f"Failed to pull library '{name}': {error_msg}")
-            return False, f"Pull failed: {error_msg}"
+    pull_branch = branch if branch else "main"
+    success, stdout, stderr = _run_git_command(
+        ["pull", "--ff-only", "origin", pull_branch], cwd=target_path
+    )
+
+    if not success:
+        error_msg = stderr or stdout
+        logger.error(f"Failed to pull library '{name}': {error_msg}")
+        return False, f"Pull failed: {error_msg}"
+
+    if "Already up to date" in stdout or "Already up-to-date" in stdout:
+        return True, "Already up to date"
     else:
     else:
-        # Repository doesn't exist, clone it
-        logger.debug(f"Cloning library '{name}' from {url} to {target_path}")
-
-        # Ensure parent directory exists
-        target_path.parent.mkdir(parents=True, exist_ok=True)
-
-        # Determine if we should use sparse-checkout
-        use_sparse = sparse_dir and sparse_dir != "."
-
-        if use_sparse:
-            # Use sparse-checkout to clone only specific directory
-            logger.debug(f"Using sparse-checkout for directory: {sparse_dir}")
-
-            # Initialize empty repo
-            success, stdout, stderr = _run_git_command(["init"], cwd=None)
-            if success:
-                # Create target directory
-                target_path.mkdir(parents=True, exist_ok=True)
-
-                # Initialize git repo
-                success, stdout, stderr = _run_git_command(["init"], cwd=target_path)
-                if not success:
-                    return False, f"Failed to initialize repo: {stderr or stdout}"
-
-                # Add remote
-                success, stdout, stderr = _run_git_command(
-                    ["remote", "add", "origin", url], cwd=target_path
-                )
-                if not success:
-                    return False, f"Failed to add remote: {stderr or stdout}"
-
-                # Enable sparse-checkout (non-cone mode to exclude root files)
-                success, stdout, stderr = _run_git_command(
-                    ["sparse-checkout", "init", "--no-cone"], cwd=target_path
-                )
-                if not success:
-                    return (
-                        False,
-                        f"Failed to enable sparse-checkout: {stderr or stdout}",
-                    )
+        return True, "Updated successfully"
 
 
-                # Set sparse-checkout to specific directory (non-cone uses patterns)
-                success, stdout, stderr = _run_git_command(
-                    ["sparse-checkout", "set", f"{sparse_dir}/*"], cwd=target_path
-                )
-                if not success:
-                    return (
-                        False,
-                        f"Failed to set sparse-checkout directory: {stderr or stdout}",
-                    )
 
 
-                # Fetch specific branch (without attempting to update local ref)
-                fetch_args = ["fetch", "--depth", "1", "origin"]
-                if branch:
-                    fetch_args.append(branch)
-                else:
-                    fetch_args.append("main")
-
-                success, stdout, stderr = _run_git_command(fetch_args, cwd=target_path)
-                if not success:
-                    return False, f"Fetch failed: {stderr or stdout}"
-
-                # Checkout the branch
-                checkout_branch = branch if branch else "main"
-                success, stdout, stderr = _run_git_command(
-                    ["checkout", checkout_branch], cwd=target_path
-                )
-                if not success:
-                    return False, f"Checkout failed: {stderr or stdout}"
-
-                # Done! Files are in target_path/sparse_dir/
-                return True, "Cloned successfully (sparse)"
-            else:
-                return False, f"Failed to initialize: {stderr or stdout}"
-        else:
-            # Regular full clone
-            clone_args = ["clone", "--depth", "1"]
-            if branch:
-                clone_args.extend(["--branch", branch])
-            clone_args.extend([url, str(target_path)])
+def _clone_new_repo(
+    name: str, url: str, target_path: Path, branch: str | None, sparse_dir: str | None
+) -> tuple[bool, str]:
+    """Clone a new repository, optionally with sparse-checkout."""
+    logger.debug(f"Cloning library '{name}' from {url} to {target_path}")
+    target_path.parent.mkdir(parents=True, exist_ok=True)
 
 
-            success, stdout, stderr = _run_git_command(clone_args)
+    use_sparse = sparse_dir and sparse_dir != "."
+
+    if use_sparse:
+        return _clone_sparse_repo(url, target_path, branch, sparse_dir)
+    else:
+        return _clone_full_repo(name, url, target_path, branch)
 
 
-            if success:
-                return True, "Cloned successfully"
-            else:
-                error_msg = stderr or stdout
-                logger.error(f"Failed to clone library '{name}': {error_msg}")
-                return False, f"Clone failed: {error_msg}"
+
+def _clone_sparse_repo(
+    url: str, target_path: Path, branch: str | None, sparse_dir: str
+) -> tuple[bool, str]:
+    """Clone repository with sparse-checkout."""
+    logger.debug(f"Using sparse-checkout for directory: {sparse_dir}")
+
+    target_path.mkdir(parents=True, exist_ok=True)
+
+    # Initialize git repo
+    success, stdout, stderr = _run_git_command(["init"], cwd=target_path)
+    if not success:
+        return False, f"Failed to initialize repo: {stderr or stdout}"
+
+    # Add remote
+    success, stdout, stderr = _run_git_command(
+        ["remote", "add", "origin", url], cwd=target_path
+    )
+    if not success:
+        return False, f"Failed to add remote: {stderr or stdout}"
+
+    # Enable sparse-checkout
+    success, stdout, stderr = _run_git_command(
+        ["sparse-checkout", "init", "--no-cone"], cwd=target_path
+    )
+    if not success:
+        return False, f"Failed to enable sparse-checkout: {stderr or stdout}"
+
+    # Set sparse-checkout directory
+    success, stdout, stderr = _run_git_command(
+        ["sparse-checkout", "set", f"{sparse_dir}/*"], cwd=target_path
+    )
+    if not success:
+        return False, f"Failed to set sparse-checkout directory: {stderr or stdout}"
+
+    # Fetch and checkout
+    fetch_branch = branch if branch else "main"
+    success, stdout, stderr = _run_git_command(
+        ["fetch", "--depth", "1", "origin", fetch_branch], cwd=target_path
+    )
+    if not success:
+        return False, f"Fetch failed: {stderr or stdout}"
+
+    success, stdout, stderr = _run_git_command(["checkout", fetch_branch], cwd=target_path)
+    if not success:
+        return False, f"Checkout failed: {stderr or stdout}"
+
+    return True, "Cloned successfully (sparse)"
+
+
+def _clone_full_repo(
+    name: str, url: str, target_path: Path, branch: str | None
+) -> tuple[bool, str]:
+    """Clone full repository."""
+    clone_args = ["clone", "--depth", "1"]
+    if branch:
+        clone_args.extend(["--branch", branch])
+    clone_args.extend([url, str(target_path)])
+
+    success, stdout, stderr = _run_git_command(clone_args)
+
+    if success:
+        return True, "Cloned successfully"
+    else:
+        error_msg = stderr or stdout
+        logger.error(f"Failed to clone library '{name}': {error_msg}")
+        return False, f"Clone failed: {error_msg}"
 
 
 
 
 @app.command()
 @app.command()
 def update(
 def update(
-    library_name: Optional[str] = Argument(
+    library_name: str | None = Argument(
         None, help="Name of specific library to update (updates all if not specified)"
         None, help="Name of specific library to update (updates all if not specified)"
     ),
     ),
     verbose: bool = Option(False, "--verbose", "-v", help="Show detailed output"),
     verbose: bool = Option(False, "--verbose", "-v", help="Show detailed output"),
@@ -337,8 +331,6 @@ def list() -> None:
             directory = "-"
             directory = "-"
 
 
             # Check if static path exists
             # Check if static path exists
-            from pathlib import Path
-
             library_path = Path(url_or_path).expanduser()
             library_path = Path(url_or_path).expanduser()
             if not library_path.is_absolute():
             if not library_path.is_absolute():
                 library_path = (config.config_path.parent / library_path).resolve()
                 library_path = (config.config_path.parent / library_path).resolve()
@@ -371,19 +363,11 @@ def list() -> None:
 @app.command()
 @app.command()
 def add(
 def add(
     name: str = Argument(..., help="Unique name for the library"),
     name: str = Argument(..., help="Unique name for the library"),
-    library_type: str = Option(
-        "git", "--type", "-t", help="Library type (git or static)"
-    ),
-    url: Optional[str] = Option(
-        None, "--url", "-u", help="Git repository URL (for git type)"
-    ),
-    branch: str = Option("main", "--branch", "-b", help="Git branch (for git type)"),
-    directory: str = Option(
-        "library", "--directory", "-d", help="Directory in repo (for git type)"
-    ),
-    path: Optional[str] = Option(
-        None, "--path", "-p", help="Local path (for static type)"
-    ),
+    library_type: str | None = None,
+    url: str | None = None,
+    branch: str = "main",
+    directory: str = "library",
+    path: str | None = None,
     enabled: bool = Option(
     enabled: bool = Option(
         True, "--enabled/--disabled", help="Enable or disable the library"
         True, "--enabled/--disabled", help="Enable or disable the library"
     ),
     ),
@@ -456,7 +440,6 @@ def remove(
             library_path = libraries_path / name
             library_path = libraries_path / name
 
 
             if library_path.exists():
             if library_path.exists():
-                import shutil
 
 
                 shutil.rmtree(library_path)
                 shutil.rmtree(library_path)
                 display.display_success(f"Deleted local files at {library_path}")
                 display.display_success(f"Deleted local files at {library_path}")

+ 5 - 5
cli/core/template/__init__.py

@@ -4,17 +4,17 @@ This package provides Template, VariableCollection, VariableSection, and Variabl
 classes for managing templates and their variables.
 classes for managing templates and their variables.
 """
 """
 
 
-from .template import Template, TemplateMetadata, TemplateFile, TemplateErrorHandler
+from .template import Template, TemplateErrorHandler, TemplateFile, TemplateMetadata
+from .variable import Variable
 from .variable_collection import VariableCollection
 from .variable_collection import VariableCollection
 from .variable_section import VariableSection
 from .variable_section import VariableSection
-from .variable import Variable
 
 
 __all__ = [
 __all__ = [
     "Template",
     "Template",
-    "TemplateMetadata",
-    "TemplateFile",
     "TemplateErrorHandler",
     "TemplateErrorHandler",
+    "TemplateFile",
+    "TemplateMetadata",
+    "Variable",
     "VariableCollection",
     "VariableCollection",
     "VariableSection",
     "VariableSection",
-    "Variable",
 ]
 ]

+ 64 - 57
cli/core/template/template.py

@@ -1,30 +1,42 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from .variable_collection import VariableCollection
-from ..exceptions import (
-    TemplateLoadError,
-    TemplateSyntaxError,
-    TemplateValidationError,
-    TemplateRenderError,
-    YAMLParseError,
-    IncompatibleSchemaVersionError,
-)
-from ..version import is_compatible
-from pathlib import Path
-from typing import Any, Dict, List, Set, Optional, Literal
-from dataclasses import dataclass, field
-from functools import lru_cache
+import importlib
 import logging
 import logging
 import os
 import os
+import re
+import secrets
+import string
+from dataclasses import dataclass, field
+from functools import lru_cache
+from pathlib import Path
+from typing import Any, Literal
+
 import yaml
 import yaml
 from jinja2 import Environment, FileSystemLoader, meta
 from jinja2 import Environment, FileSystemLoader, meta
-from jinja2.sandbox import SandboxedEnvironment
 from jinja2.exceptions import (
 from jinja2.exceptions import (
-    TemplateSyntaxError as Jinja2TemplateSyntaxError,
-    UndefinedError,
     TemplateError as Jinja2TemplateError,
     TemplateError as Jinja2TemplateError,
+)
+from jinja2.exceptions import (
     TemplateNotFound as Jinja2TemplateNotFound,
     TemplateNotFound as Jinja2TemplateNotFound,
 )
 )
+from jinja2.exceptions import (
+    TemplateSyntaxError as Jinja2TemplateSyntaxError,
+)
+from jinja2.exceptions import (
+    UndefinedError,
+)
+from jinja2.sandbox import SandboxedEnvironment
+
+from ..exceptions import (
+    IncompatibleSchemaVersionError,
+    TemplateLoadError,
+    TemplateRenderError,
+    TemplateSyntaxError,
+    TemplateValidationError,
+    YAMLParseError,
+)
+from ..version import is_compatible
+from .variable_collection import VariableCollection
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -40,8 +52,8 @@ class TemplateErrorHandler:
 
 
     @staticmethod
     @staticmethod
     def extract_error_context(
     def extract_error_context(
-        file_path: Path, line_number: Optional[int], context_size: int = 3
-    ) -> List[str]:
+        file_path: Path, line_number: int | None, context_size: int = 3
+    ) -> list[str]:
         """Extract lines of context around an error location.
         """Extract lines of context around an error location.
 
 
         Args:
         Args:
@@ -56,7 +68,7 @@ class TemplateErrorHandler:
             return []
             return []
 
 
         try:
         try:
-            with open(file_path, "r", encoding="utf-8") as f:
+            with open(file_path, encoding="utf-8") as f:
                 lines = f.readlines()
                 lines = f.readlines()
 
 
             start_line = max(0, line_number - context_size - 1)
             start_line = max(0, line_number - context_size - 1)
@@ -69,11 +81,11 @@ class TemplateErrorHandler:
                 context.append(f"{marker} {line_num:4d} | {lines[i].rstrip()}")
                 context.append(f"{marker} {line_num:4d} | {lines[i].rstrip()}")
 
 
             return context
             return context
-        except (IOError, OSError):
+        except OSError:
             return []
             return []
 
 
     @staticmethod
     @staticmethod
-    def get_common_jinja_suggestions(error_msg: str, available_vars: set) -> List[str]:
+    def get_common_jinja_suggestions(error_msg: str, available_vars: set) -> list[str]:
         """Generate helpful suggestions based on common Jinja2 errors.
         """Generate helpful suggestions based on common Jinja2 errors.
 
 
         Args:
         Args:
@@ -89,8 +101,6 @@ class TemplateErrorHandler:
         # Undefined variable errors
         # Undefined variable errors
         if "undefined" in error_lower or "is not defined" in error_lower:
         if "undefined" in error_lower or "is not defined" in error_lower:
             # Try to extract variable name from error message
             # Try to extract variable name from error message
-            import re
-
             var_match = re.search(r"'([^']+)'.*is undefined", error_msg)
             var_match = re.search(r"'([^']+)'.*is undefined", error_msg)
             if not var_match:
             if not var_match:
                 var_match = re.search(r"'([^']+)'.*is not defined", error_msg)
                 var_match = re.search(r"'([^']+)'.*is not defined", error_msg)
@@ -175,10 +185,10 @@ class TemplateErrorHandler:
     def parse_jinja_error(
     def parse_jinja_error(
         cls,
         cls,
         error: Exception,
         error: Exception,
-        template_file: "TemplateFile",
+        template_file: TemplateFile,
         template_dir: Path,
         template_dir: Path,
         available_vars: set,
         available_vars: set,
-    ) -> tuple[str, Optional[int], Optional[int], List[str], List[str]]:
+    ) -> tuple[str, int | None, int | None, list[str], list[str]]:
         """Parse a Jinja2 exception to extract detailed error information.
         """Parse a Jinja2 exception to extract detailed error information.
 
 
         Args:
         Args:
@@ -241,7 +251,7 @@ class TemplateMetadata:
     date: str
     date: str
     version: str
     version: str
     module: str = ""
     module: str = ""
-    tags: List[str] = field(default_factory=list)
+    tags: list[str] = field(default_factory=list)
     library: str = "unknown"
     library: str = "unknown"
     library_type: str = "git"  # Type of library ("git" or "static")
     library_type: str = "git"  # Type of library ("git" or "static")
     next_steps: str = ""
     next_steps: str = ""
@@ -339,17 +349,17 @@ class Template:
         self.library_type = library_type
         self.library_type = library_type
 
 
         # Initialize caches for lazy loading
         # Initialize caches for lazy loading
-        self.__module_specs: Optional[dict] = None
-        self.__merged_specs: Optional[dict] = None
-        self.__jinja_env: Optional[Environment] = None
-        self.__used_variables: Optional[Set[str]] = None
-        self.__variables: Optional[VariableCollection] = None
-        self.__template_files: Optional[List[TemplateFile]] = None  # New attribute
+        self.__module_specs: dict | None = None
+        self.__merged_specs: dict | None = None
+        self.__jinja_env: Environment | None = None
+        self.__used_variables: set[str] | None = None
+        self.__variables: VariableCollection | None = None
+        self.__template_files: list[TemplateFile] | None = None  # New attribute
 
 
         try:
         try:
             # Find and parse the main template file (template.yaml or template.yml)
             # Find and parse the main template file (template.yaml or template.yml)
             main_template_path = self._find_main_template_file()
             main_template_path = self._find_main_template_file()
-            with open(main_template_path, "r", encoding="utf-8") as f:
+            with open(main_template_path, encoding="utf-8") as f:
                 # Load all YAML documents (handles templates with empty lines before ---)
                 # Load all YAML documents (handles templates with empty lines before ---)
                 documents = list(yaml.safe_load_all(f))
                 documents = list(yaml.safe_load_all(f))
 
 
@@ -392,15 +402,17 @@ class Template:
 
 
         except (ValueError, FileNotFoundError) as e:
         except (ValueError, FileNotFoundError) as e:
             logger.error(f"Error loading template from {template_dir}: {e}")
             logger.error(f"Error loading template from {template_dir}: {e}")
-            raise TemplateLoadError(f"Error loading template from {template_dir}: {e}")
+            raise TemplateLoadError(
+                f"Error loading template from {template_dir}: {e}"
+            ) from e
         except yaml.YAMLError as e:
         except yaml.YAMLError as e:
             logger.error(f"YAML parsing error in template {template_dir}: {e}")
             logger.error(f"YAML parsing error in template {template_dir}: {e}")
-            raise YAMLParseError(str(template_dir / "template.y*ml"), e)
-        except (IOError, OSError) as e:
+            raise YAMLParseError(str(template_dir / "template.y*ml"), e) from e
+        except OSError as e:
             logger.error(f"File I/O error loading template {template_dir}: {e}")
             logger.error(f"File I/O error loading template {template_dir}: {e}")
             raise TemplateLoadError(
             raise TemplateLoadError(
                 f"File I/O error loading template from {template_dir}: {e}"
                 f"File I/O error loading template from {template_dir}: {e}"
-            )
+            ) from e
 
 
     def set_qualified_id(self, library_name: str | None = None) -> None:
     def set_qualified_id(self, library_name: str | None = None) -> None:
         """Set a qualified ID for this template (used when duplicates exist across libraries).
         """Set a qualified ID for this template (used when duplicates exist across libraries).
@@ -444,8 +456,6 @@ class Template:
         if not kind:
         if not kind:
             return {}
             return {}
         try:
         try:
-            import importlib
-
             module = importlib.import_module(f"cli.modules.{kind}")
             module = importlib.import_module(f"cli.modules.{kind}")
 
 
             # Check if module has schema-specific specs (multi-schema support)
             # Check if module has schema-specific specs (multi-schema support)
@@ -469,7 +479,7 @@ class Template:
         except Exception as e:
         except Exception as e:
             raise ValueError(
             raise ValueError(
                 f"Error loading module specifications for kind '{kind}': {e}"
                 f"Error loading module specifications for kind '{kind}': {e}"
-            )
+            ) from e
 
 
     def _merge_specs(self, module_specs: dict, template_specs: dict) -> dict:
     def _merge_specs(self, module_specs: dict, template_specs: dict) -> dict:
         """Deep merge template specs with module specs using VariableCollection.
         """Deep merge template specs with module specs using VariableCollection.
@@ -505,7 +515,7 @@ class Template:
 
 
     def _collect_template_files(self) -> None:
     def _collect_template_files(self) -> None:
         """Collects all TemplateFile objects in the template directory."""
         """Collects all TemplateFile objects in the template directory."""
-        template_files: List[TemplateFile] = []
+        template_files: list[TemplateFile] = []
 
 
         for root, _, files in os.walk(self.template_dir):
         for root, _, files in os.walk(self.template_dir):
             for filename in files:
             for filename in files:
@@ -533,24 +543,24 @@ class Template:
 
 
         self.__template_files = template_files
         self.__template_files = template_files
 
 
-    def _extract_all_used_variables(self) -> Set[str]:
+    def _extract_all_used_variables(self) -> set[str]:
         """Extract all undeclared variables from all .j2 files in the template directory.
         """Extract all undeclared variables from all .j2 files in the template directory.
 
 
         Raises:
         Raises:
             ValueError: If any Jinja2 template has syntax errors
             ValueError: If any Jinja2 template has syntax errors
         """
         """
-        used_variables: Set[str] = set()
+        used_variables: set[str] = set()
         syntax_errors = []
         syntax_errors = []
 
 
         for template_file in self.template_files:  # Iterate over TemplateFile objects
         for template_file in self.template_files:  # Iterate over TemplateFile objects
             if template_file.file_type == "j2":
             if template_file.file_type == "j2":
                 file_path = self.template_dir / template_file.relative_path
                 file_path = self.template_dir / template_file.relative_path
                 try:
                 try:
-                    with open(file_path, "r", encoding="utf-8") as f:
+                    with open(file_path, encoding="utf-8") as f:
                         content = f.read()
                         content = f.read()
                         ast = self.jinja_env.parse(content)  # Use lazy-loaded jinja_env
                         ast = self.jinja_env.parse(content)  # Use lazy-loaded jinja_env
                         used_variables.update(meta.find_undeclared_variables(ast))
                         used_variables.update(meta.find_undeclared_variables(ast))
-                except (IOError, OSError) as e:
+                except OSError as e:
                     relative_path = file_path.relative_to(self.template_dir)
                     relative_path = file_path.relative_to(self.template_dir)
                     syntax_errors.append(f"  - {relative_path}: File I/O error: {e}")
                     syntax_errors.append(f"  - {relative_path}: File I/O error: {e}")
                 except Exception as e:
                 except Exception as e:
@@ -698,7 +708,7 @@ class Template:
 
 
     def render(
     def render(
         self, variables: VariableCollection, debug: bool = False
         self, variables: VariableCollection, debug: bool = False
-    ) -> tuple[Dict[str, str], Dict[str, Any]]:
+    ) -> tuple[dict[str, str], dict[str, Any]]:
         """Render all .j2 files in the template directory.
         """Render all .j2 files in the template directory.
 
 
         Args:
         Args:
@@ -712,9 +722,6 @@ class Template:
         variable_values = variables.get_satisfied_values()
         variable_values = variables.get_satisfied_values()
 
 
         # Auto-generate values for autogenerated variables that are empty
         # Auto-generate values for autogenerated variables that are empty
-        import secrets
-        import string
-
         for section in variables.get_sections().values():
         for section in variables.get_sections().values():
             for var_name, variable in section.variables.items():
             for var_name, variable in section.variables.items():
                 if variable.autogenerated and (
                 if variable.autogenerated and (
@@ -793,7 +800,7 @@ class Template:
                         else {},
                         else {},
                         suggestions=suggestions,
                         suggestions=suggestions,
                         original_error=e,
                         original_error=e,
-                    )
+                    ) from e
 
 
                 except Exception as e:
                 except Exception as e:
                     # Catch any other unexpected errors
                     # Catch any other unexpected errors
@@ -807,7 +814,7 @@ class Template:
                             "This is an unexpected error. Please check the template for issues."
                             "This is an unexpected error. Please check the template for issues."
                         ],
                         ],
                         original_error=e,
                         original_error=e,
-                    )
+                    ) from e
 
 
             elif template_file.file_type == "static":
             elif template_file.file_type == "static":
                 # For static files, just read their content and add to rendered_files
                 # For static files, just read their content and add to rendered_files
@@ -819,10 +826,10 @@ class Template:
                             f"Copying static file: {template_file.relative_path}"
                             f"Copying static file: {template_file.relative_path}"
                         )
                         )
 
 
-                    with open(file_path, "r", encoding="utf-8") as f:
+                    with open(file_path, encoding="utf-8") as f:
                         content = f.read()
                         content = f.read()
                         rendered_files[str(template_file.output_path)] = content
                         rendered_files[str(template_file.output_path)] = content
-                except (IOError, OSError) as e:
+                except OSError as e:
                     logger.error(f"Error reading static file {file_path}: {e}")
                     logger.error(f"Error reading static file {file_path}: {e}")
                     raise TemplateRenderError(
                     raise TemplateRenderError(
                         message=f"Error reading static file: {e}",
                         message=f"Error reading static file: {e}",
@@ -831,7 +838,7 @@ class Template:
                             "Check that the file exists and has read permissions"
                             "Check that the file exists and has read permissions"
                         ],
                         ],
                         original_error=e,
                         original_error=e,
-                    )
+                    ) from e
 
 
         return rendered_files, variable_values
         return rendered_files, variable_values
 
 
@@ -855,7 +862,7 @@ class Template:
         return "\n".join(sanitized).lstrip("\n").rstrip("\n") + "\n"
         return "\n".join(sanitized).lstrip("\n").rstrip("\n") + "\n"
 
 
     @property
     @property
-    def template_files(self) -> List[TemplateFile]:
+    def template_files(self) -> list[TemplateFile]:
         if self.__template_files is None:
         if self.__template_files is None:
             self._collect_template_files()  # Populate self.__template_files
             self._collect_template_files()  # Populate self.__template_files
         return self.__template_files
         return self.__template_files
@@ -890,7 +897,7 @@ class Template:
         return self.__jinja_env
         return self.__jinja_env
 
 
     @property
     @property
-    def used_variables(self) -> Set[str]:
+    def used_variables(self) -> set[str]:
         if self.__used_variables is None:
         if self.__used_variables is None:
             self.__used_variables = self._extract_all_used_variables()
             self.__used_variables = self._extract_all_used_variables()
         return self.__used_variables
         return self.__used_variables

+ 31 - 35
cli/core/template/variable.py

@@ -1,11 +1,11 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
-from urllib.parse import urlparse
 import logging
 import logging
 import re
 import re
+from typing import TYPE_CHECKING, Any
+from urllib.parse import urlparse
 
 
-from ..exceptions import VariableValidationError, VariableError
+from ..exceptions import VariableError, VariableValidationError
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .variable_section import VariableSection
     from .variable_section import VariableSection
@@ -38,28 +38,28 @@ class Variable:
             raise VariableError("Variable data must contain 'name' key")
             raise VariableError("Variable data must contain 'name' key")
 
 
         # Track which fields were explicitly provided in source data
         # Track which fields were explicitly provided in source data
-        self._explicit_fields: Set[str] = set(data.keys())
+        self._explicit_fields: set[str] = set(data.keys())
 
 
         # Initialize fields
         # Initialize fields
         self.name: str = data["name"]
         self.name: str = data["name"]
         # Reference to parent section (set by VariableCollection)
         # Reference to parent section (set by VariableCollection)
-        self.parent_section: Optional["VariableSection"] = data.get("parent_section")
-        self.description: Optional[str] = data.get("description") or data.get(
+        self.parent_section: VariableSection | None = data.get("parent_section")
+        self.description: str | None = data.get("description") or data.get(
             "display", ""
             "display", ""
         )
         )
         self.type: str = data.get("type", "str")
         self.type: str = data.get("type", "str")
-        self.options: Optional[List[Any]] = data.get("options", [])
-        self.prompt: Optional[str] = data.get("prompt")
+        self.options: list[Any] | None = data.get("options", [])
+        self.prompt: str | None = data.get("prompt")
         if "value" in data:
         if "value" in data:
             self.value: Any = data.get("value")
             self.value: Any = data.get("value")
         elif "default" in data:
         elif "default" in data:
             self.value: Any = data.get("default")
             self.value: Any = data.get("default")
         else:
         else:
             self.value: Any = None
             self.value: Any = None
-        self.origin: Optional[str] = data.get("origin")
+        self.origin: str | None = data.get("origin")
         self.sensitive: bool = data.get("sensitive", False)
         self.sensitive: bool = data.get("sensitive", False)
         # Optional extra explanation used by interactive prompts
         # Optional extra explanation used by interactive prompts
-        self.extra: Optional[str] = data.get("extra")
+        self.extra: str | None = data.get("extra")
         # Flag indicating this variable should be auto-generated when empty
         # Flag indicating this variable should be auto-generated when empty
         self.autogenerated: bool = data.get("autogenerated", False)
         self.autogenerated: bool = data.get("autogenerated", False)
         # Flag indicating this variable is required even when section is disabled
         # Flag indicating this variable is required even when section is disabled
@@ -67,7 +67,7 @@ class Variable:
         # Flag indicating this variable can be empty/optional
         # Flag indicating this variable can be empty/optional
         self.optional: bool = data.get("optional", False)
         self.optional: bool = data.get("optional", False)
         # Original value before config override (used for display)
         # Original value before config override (used for display)
-        self.original_value: Optional[Any] = data.get("original_value")
+        self.original_value: Any | None = data.get("original_value")
         # Variable dependencies - can be string or list of strings in format "var_name=value"
         # Variable dependencies - can be string or list of strings in format "var_name=value"
         # Supports semicolon-separated multiple conditions: "var1=value1;var2=value2,value3"
         # Supports semicolon-separated multiple conditions: "var1=value1;var2=value2,value3"
         needs_value = data.get("needs")
         needs_value = data.get("needs")
@@ -75,17 +75,17 @@ class Variable:
             if isinstance(needs_value, str):
             if isinstance(needs_value, str):
                 # Split by semicolon to support multiple AND conditions in a single string
                 # Split by semicolon to support multiple AND conditions in a single string
                 # Example: "traefik_enabled=true;network_mode=bridge,macvlan"
                 # Example: "traefik_enabled=true;network_mode=bridge,macvlan"
-                self.needs: List[str] = [
+                self.needs: list[str] = [
                     need.strip() for need in needs_value.split(";") if need.strip()
                     need.strip() for need in needs_value.split(";") if need.strip()
                 ]
                 ]
             elif isinstance(needs_value, list):
             elif isinstance(needs_value, list):
-                self.needs: List[str] = needs_value
+                self.needs: list[str] = needs_value
             else:
             else:
                 raise VariableError(
                 raise VariableError(
                     f"Variable '{self.name}' has invalid 'needs' value: must be string or list"
                     f"Variable '{self.name}' has invalid 'needs' value: must be string or list"
                 )
                 )
         else:
         else:
-            self.needs: List[str] = []
+            self.needs: list[str] = []
 
 
         # Validate and convert the default/initial value if present
         # Validate and convert the default/initial value if present
         if self.value is not None:
         if self.value is not None:
@@ -94,7 +94,7 @@ class Variable:
             except ValueError as exc:
             except ValueError as exc:
                 raise VariableValidationError(
                 raise VariableValidationError(
                     self.name, f"Invalid default value: {exc}"
                     self.name, f"Invalid default value: {exc}"
-                )
+                ) from exc
 
 
     def convert(self, value: Any) -> Any:
     def convert(self, value: Any) -> Any:
         """Validate and convert a raw value based on the variable type.
         """Validate and convert a raw value based on the variable type.
@@ -160,9 +160,7 @@ class Variable:
         # Allow empty values as they will be auto-generated later
         # Allow empty values as they will be auto-generated later
         if self.autogenerated and (
         if self.autogenerated and (
             converted is None
             converted is None
-            or (
-                isinstance(converted, str) and (converted == "" or converted == "*auto")
-            )
+            or (isinstance(converted, str) and converted in ("", "*auto"))
         ):
         ):
             return None  # Signal that auto-generation should happen
             return None  # Signal that auto-generation should happen
 
 
@@ -173,9 +171,12 @@ class Variable:
             return None
             return None
 
 
         # Check if this is a required field and the value is empty
         # Check if this is a required field and the value is empty
-        if check_required and self.is_required():
-            if converted is None or (isinstance(converted, str) and converted == ""):
-                raise ValueError("This field is required and cannot be empty")
+        if (
+            check_required
+            and self.is_required()
+            and (converted is None or (isinstance(converted, str) and converted == ""))
+        ):
+            raise ValueError("This field is required and cannot be empty")
 
 
         return converted
         return converted
 
 
@@ -191,7 +192,7 @@ class Variable:
                 return False
                 return False
         raise ValueError("value must be a boolean (true/false)")
         raise ValueError("value must be a boolean (true/false)")
 
 
-    def _convert_int(self, value: Any) -> Optional[int]:
+    def _convert_int(self, value: Any) -> int | None:
         """Convert value to integer."""
         """Convert value to integer."""
         if isinstance(value, int):
         if isinstance(value, int):
             return value
             return value
@@ -202,7 +203,7 @@ class Variable:
         except (TypeError, ValueError) as exc:
         except (TypeError, ValueError) as exc:
             raise ValueError("value must be an integer") from exc
             raise ValueError("value must be an integer") from exc
 
 
-    def _convert_float(self, value: Any) -> Optional[float]:
+    def _convert_float(self, value: Any) -> float | None:
         """Convert value to float."""
         """Convert value to float."""
         if isinstance(value, float):
         if isinstance(value, float):
             return value
             return value
@@ -213,7 +214,7 @@ class Variable:
         except (TypeError, ValueError) as exc:
         except (TypeError, ValueError) as exc:
             raise ValueError("value must be a float") from exc
             raise ValueError("value must be a float") from exc
 
 
-    def _convert_enum(self, value: Any) -> Optional[str]:
+    def _convert_enum(self, value: Any) -> str | None:
         if value == "":
         if value == "":
             return None
             return None
         val = str(value)
         val = str(value)
@@ -238,7 +239,7 @@ class Variable:
             raise ValueError("value must be a valid email address")
             raise ValueError("value must be a valid email address")
         return val
         return val
 
 
-    def to_dict(self) -> Dict[str, Any]:
+    def to_dict(self) -> dict[str, Any]:
         """Serialize Variable to a dictionary for storage."""
         """Serialize Variable to a dictionary for storage."""
         result = {}
         result = {}
 
 
@@ -356,7 +357,7 @@ class Variable:
 
 
         return prompt_text
         return prompt_text
 
 
-    def get_validation_hint(self) -> Optional[str]:
+    def get_validation_hint(self) -> str | None:
         """Get validation hint for prompts (e.g., enum options).
         """Get validation hint for prompts (e.g., enum options).
 
 
         Returns:
         Returns:
@@ -394,9 +395,7 @@ class Variable:
         # Explicit required flag takes highest precedence
         # Explicit required flag takes highest precedence
         if self.required:
         if self.required:
             # But autogenerated variables can still be empty (will be generated later)
             # But autogenerated variables can still be empty (will be generated later)
-            if self.autogenerated:
-                return False
-            return True
+            return not self.autogenerated
 
 
         # Autogenerated variables can be empty (will be generated later)
         # Autogenerated variables can be empty (will be generated later)
         if self.autogenerated:
         if self.autogenerated:
@@ -407,13 +406,10 @@ class Variable:
             return False
             return False
 
 
         # Variables with a default value are not required
         # Variables with a default value are not required
-        if self.value is not None:
-            return False
-
         # No default value and not autogenerated = required
         # No default value and not autogenerated = required
-        return True
+        return self.value is None
 
 
-    def get_parent(self) -> Optional["VariableSection"]:
+    def get_parent(self) -> VariableSection | None:
         """Get the parent VariableSection that contains this variable.
         """Get the parent VariableSection that contains this variable.
 
 
         Returns:
         Returns:
@@ -421,7 +417,7 @@ class Variable:
         """
         """
         return self.parent_section
         return self.parent_section
 
 
-    def clone(self, update: Optional[Dict[str, Any]] = None) -> "Variable":
+    def clone(self, update: dict[str, Any] | None = None) -> Variable:
         """Create a deep copy of the variable with optional field updates.
         """Create a deep copy of the variable with optional field updates.
 
 
         This is more efficient than converting to dict and back when copying variables.
         This is more efficient than converting to dict and back when copying variables.

+ 63 - 62
cli/core/template/variable_collection.py

@@ -1,12 +1,12 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from collections import defaultdict
-from typing import Any, Dict, List, Optional, Set, Union
 import logging
 import logging
+from collections import defaultdict
+from typing import Any
 
 
+from ..exceptions import VariableError, VariableValidationError
 from .variable import Variable
 from .variable import Variable
 from .variable_section import VariableSection
 from .variable_section import VariableSection
-from ..exceptions import VariableValidationError, VariableError
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -40,11 +40,11 @@ class VariableCollection:
         if not isinstance(spec, dict):
         if not isinstance(spec, dict):
             raise ValueError("Spec must be a dictionary")
             raise ValueError("Spec must be a dictionary")
 
 
-        self._sections: Dict[str, VariableSection] = {}
+        self._sections: dict[str, VariableSection] = {}
         # NOTE: The _variable_map provides a flat, O(1) lookup for any variable by its name,
         # NOTE: The _variable_map provides a flat, O(1) lookup for any variable by its name,
         # avoiding the need to iterate through sections. It stores references to the same
         # avoiding the need to iterate through sections. It stores references to the same
         # Variable objects contained in the _set structure.
         # Variable objects contained in the _set structure.
-        self._variable_map: Dict[str, Variable] = {}
+        self._variable_map: dict[str, Variable] = {}
         self._initialize_sections(spec)
         self._initialize_sections(spec)
         # Validate dependencies after all sections are loaded
         # Validate dependencies after all sections are loaded
         self._validate_dependencies()
         self._validate_dependencies()
@@ -112,7 +112,7 @@ class VariableCollection:
 
 
     def _validate_unique_variable_names(self) -> None:
     def _validate_unique_variable_names(self) -> None:
         """Validate that all variable names are unique across all sections."""
         """Validate that all variable names are unique across all sections."""
-        var_to_sections: Dict[str, List[str]] = defaultdict(list)
+        var_to_sections: dict[str, list[str]] = defaultdict(list)
 
 
         # Build mapping of variable names to sections
         # Build mapping of variable names to sections
         for section_key, section in self._sections.items():
         for section_key, section in self._sections.items():
@@ -168,7 +168,7 @@ class VariableCollection:
             )
             )
 
 
     @staticmethod
     @staticmethod
-    def _parse_need(need_str: str) -> tuple[str, Optional[Any]]:
+    def _parse_need(need_str: str) -> tuple[str, Any | None]:
         """Parse a need string into variable name and expected value(s).
         """Parse a need string into variable name and expected value(s).
 
 
         Supports three formats:
         Supports three formats:
@@ -245,12 +245,11 @@ class VariableCollection:
                         if variable.type == "bool":
                         if variable.type == "bool":
                             if bool(actual_value) == bool(expected_converted):
                             if bool(actual_value) == bool(expected_converted):
                                 return True
                                 return True
-                        else:
-                            # String comparison for other types
-                            if actual_value is not None and str(actual_value) == str(
-                                expected_converted
-                            ):
-                                return True
+                        # String comparison for other types
+                        elif actual_value is not None and str(actual_value) == str(
+                            expected_converted
+                        ):
+                            return True
                     return False  # None of the expected values matched
                     return False  # None of the expected values matched
                 else:
                 else:
                     # Single expected value (original behavior)
                     # Single expected value (original behavior)
@@ -287,30 +286,32 @@ class VariableCollection:
                         raise VariableError(
                         raise VariableError(
                             f"Section '{section_key}' depends on '{var_or_section}', but '{var_or_section}' does not exist"
                             f"Section '{section_key}' depends on '{var_or_section}', but '{var_or_section}' does not exist"
                         )
                         )
-                else:
-                    # New format: validate variable exists
-                    # NOTE: We only warn here, not raise an error, because the variable might be
-                    # added later during merge with module spec. The actual runtime check in
-                    # _is_need_satisfied() will handle missing variables gracefully.
-                    if var_or_section not in self._variable_map:
-                        logger.debug(
-                            f"Section '{section_key}' has need '{dep}', but variable '{var_or_section}' "
-                            f"not found (might be added during merge)"
-                        )
+                # New format: validate variable exists
+                # NOTE: We only warn here, not raise an error, because the variable might be
+                # added later during merge with module spec. The actual runtime check in
+                # _is_need_satisfied() will handle missing variables gracefully.
+                elif (
+                    expected_value is not None
+                    and var_or_section not in self._variable_map
+                ):
+                    logger.debug(
+                        f"Section '{section_key}' has need '{dep}', but variable '{var_or_section}' "
+                        f"not found (might be added during merge)"
+                    )
 
 
         # Check for missing dependencies in variables
         # Check for missing dependencies in variables
         for var_name, variable in self._variable_map.items():
         for var_name, variable in self._variable_map.items():
             for dep in variable.needs:
             for dep in variable.needs:
                 dep_var, expected_value = self._parse_need(dep)
                 dep_var, expected_value = self._parse_need(dep)
-                if expected_value is not None:  # Only validate new format
-                    if dep_var not in self._variable_map:
-                        # NOTE: We only warn here, not raise an error, because the variable might be
-                        # added later during merge with module spec. The actual runtime check in
-                        # _is_need_satisfied() will handle missing variables gracefully.
-                        logger.debug(
-                            f"Variable '{var_name}' has need '{dep}', but variable '{dep_var}' "
-                            f"not found (might be added during merge)"
-                        )
+                # Only validate new format
+                if expected_value is not None and dep_var not in self._variable_map:
+                    # NOTE: We only warn here, not raise an error, because the variable might be
+                    # added later during merge with module spec. The actual runtime check in
+                    # _is_need_satisfied() will handle missing variables gracefully.
+                    logger.debug(
+                        f"Variable '{var_name}' has need '{dep}', but variable '{dep_var}' "
+                        f"not found (might be added during merge)"
+                    )
 
 
         # Check for circular dependencies using depth-first search
         # Check for circular dependencies using depth-first search
         # Note: Only checks section-level dependencies in old format (section names)
         # Note: Only checks section-level dependencies in old format (section names)
@@ -433,24 +434,23 @@ class VariableCollection:
                 var_satisfied = self.is_variable_satisfied(var_name)
                 var_satisfied = self.is_variable_satisfied(var_name)
 
 
                 # If section is disabled OR variable dependencies aren't met, reset to False
                 # If section is disabled OR variable dependencies aren't met, reset to False
-                if not section_satisfied or not is_enabled or not var_satisfied:
-                    # Only reset if current value is not already False
-                    if variable.value is not False:
-                        # Don't reset CLI-provided variables - they'll be validated later
-                        if variable.origin == "cli":
-                            continue
-
-                        # Store original value if not already stored (for display purposes)
-                        if not hasattr(variable, "_original_disabled"):
-                            variable._original_disabled = variable.value
-
-                        variable.value = False
-                        reset_vars.append(var_name)
-                        logger.debug(
-                            f"Reset disabled bool variable '{var_name}' to False "
-                            f"(section satisfied: {section_satisfied}, enabled: {is_enabled}, "
-                            f"var satisfied: {var_satisfied})"
-                        )
+                # Only reset if current value is not already False and not CLI-provided
+                if (
+                    (not section_satisfied or not is_enabled or not var_satisfied)
+                    and variable.value is not False
+                    and variable.origin != "cli"
+                ):
+                    # Store original value if not already stored (for display purposes)
+                    if not hasattr(variable, "_original_disabled"):
+                        variable._original_disabled = variable.value
+
+                    variable.value = False
+                    reset_vars.append(var_name)
+                    logger.debug(
+                        f"Reset disabled bool variable '{var_name}' to False "
+                        f"(section satisfied: {section_satisfied}, enabled: {is_enabled}, "
+                        f"var satisfied: {var_satisfied})"
+                    )
 
 
         return reset_vars
         return reset_vars
 
 
@@ -503,7 +503,7 @@ class VariableCollection:
         for section in self._sections.values():
         for section in self._sections.values():
             section.sort_variables(self._is_need_satisfied)
             section.sort_variables(self._is_need_satisfied)
 
 
-    def _topological_sort(self) -> List[str]:
+    def _topological_sort(self) -> list[str]:
         """Perform topological sort on sections based on dependencies using Kahn's algorithm."""
         """Perform topological sort on sections based on dependencies using Kahn's algorithm."""
         in_degree = {key: len(section.needs) for key, section in self._sections.items()}
         in_degree = {key: len(section.needs) for key, section in self._sections.items()}
         queue = [key for key, degree in in_degree.items() if degree == 0]
         queue = [key for key, degree in in_degree.items() if degree == 0]
@@ -571,11 +571,11 @@ class VariableCollection:
 
 
             yield section_key, section
             yield section_key, section
 
 
-    def get_sections(self) -> Dict[str, VariableSection]:
+    def get_sections(self) -> dict[str, VariableSection]:
         """Get all sections in the collection."""
         """Get all sections in the collection."""
         return self._sections.copy()
         return self._sections.copy()
 
 
-    def get_section(self, key: str) -> Optional[VariableSection]:
+    def get_section(self, key: str) -> VariableSection | None:
         """Get a specific section by its key."""
         """Get a specific section by its key."""
         return self._sections.get(key)
         return self._sections.get(key)
 
 
@@ -632,7 +632,7 @@ class VariableCollection:
 
 
         return satisfied_values
         return satisfied_values
 
 
-    def get_sensitive_variables(self) -> Dict[str, Any]:
+    def get_sensitive_variables(self) -> dict[str, Any]:
         """Get only the sensitive variables with their values."""
         """Get only the sensitive variables with their values."""
         return {
         return {
             name: var.value
             name: var.value
@@ -828,9 +828,9 @@ class VariableCollection:
 
 
     def merge(
     def merge(
         self,
         self,
-        other_spec: Union[Dict[str, Any], "VariableCollection"],
+        other_spec: dict[str, Any] | VariableCollection,
         origin: str = "override",
         origin: str = "override",
-    ) -> "VariableCollection":
+    ) -> VariableCollection:
         """Merge another spec or VariableCollection into this one with precedence tracking.
         """Merge another spec or VariableCollection into this one with precedence tracking.
 
 
         OPTIMIZED: Works directly on objects without dict conversions for better performance.
         OPTIMIZED: Works directly on objects without dict conversions for better performance.
@@ -950,9 +950,10 @@ class VariableCollection:
                     update["needs"] = other_var.needs.copy() if other_var.needs else []
                     update["needs"] = other_var.needs.copy() if other_var.needs else []
 
 
                 # Special handling for value/default (allow explicit null to clear)
                 # Special handling for value/default (allow explicit null to clear)
-                if "value" in other_var._explicit_fields:
-                    update["value"] = other_var.value
-                elif "default" in other_var._explicit_fields:
+                if (
+                    "value" in other_var._explicit_fields
+                    or "default" in other_var._explicit_fields
+                ):
                     update["value"] = other_var.value
                     update["value"] = other_var.value
 
 
                 merged_section.variables[var_name] = self_var.clone(update=update)
                 merged_section.variables[var_name] = self_var.clone(update=update)
@@ -965,8 +966,8 @@ class VariableCollection:
         return merged_section
         return merged_section
 
 
     def filter_to_used(
     def filter_to_used(
-        self, used_variables: Set[str], keep_sensitive: bool = True
-    ) -> "VariableCollection":
+        self, used_variables: set[str], keep_sensitive: bool = True
+    ) -> VariableCollection:
         """Filter collection to only variables that are used (or sensitive).
         """Filter collection to only variables that are used (or sensitive).
 
 
         OPTIMIZED: Works directly on objects without dict conversions for better performance.
         OPTIMIZED: Works directly on objects without dict conversions for better performance.
@@ -1024,7 +1025,7 @@ class VariableCollection:
 
 
         return filtered
         return filtered
 
 
-    def get_all_variable_names(self) -> Set[str]:
+    def get_all_variable_names(self) -> set[str]:
         """Get set of all variable names across all sections.
         """Get set of all variable names across all sections.
 
 
         Returns:
         Returns:

+ 9 - 9
cli/core/template/variable_section.py

@@ -1,10 +1,10 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 from collections import OrderedDict
 from collections import OrderedDict
-from typing import Any, Dict, List, Optional
+from typing import Any
 
 
-from .variable import Variable
 from ..exceptions import VariableError
 from ..exceptions import VariableError
+from .variable import Variable
 
 
 
 
 class VariableSection:
 class VariableSection:
@@ -28,8 +28,8 @@ class VariableSection:
         self.key: str = data["key"]
         self.key: str = data["key"]
         self.title: str = data["title"]
         self.title: str = data["title"]
         self.variables: OrderedDict[str, Variable] = OrderedDict()
         self.variables: OrderedDict[str, Variable] = OrderedDict()
-        self.description: Optional[str] = data.get("description")
-        self.toggle: Optional[str] = data.get("toggle")
+        self.description: str | None = data.get("description")
+        self.toggle: str | None = data.get("toggle")
         # Track which fields were explicitly provided (to support explicit clears)
         # Track which fields were explicitly provided (to support explicit clears)
         self._explicit_fields: set[str] = set(data.keys())
         self._explicit_fields: set[str] = set(data.keys())
         # Default "general" section to required=True, all others to required=False
         # Default "general" section to required=True, all others to required=False
@@ -41,19 +41,19 @@ class VariableSection:
             if isinstance(needs_value, str):
             if isinstance(needs_value, str):
                 # Split by semicolon to support multiple AND conditions in a single string
                 # Split by semicolon to support multiple AND conditions in a single string
                 # Example: "traefik_enabled=true;network_mode=bridge,macvlan"
                 # Example: "traefik_enabled=true;network_mode=bridge,macvlan"
-                self.needs: List[str] = [
+                self.needs: list[str] = [
                     need.strip() for need in needs_value.split(";") if need.strip()
                     need.strip() for need in needs_value.split(";") if need.strip()
                 ]
                 ]
             elif isinstance(needs_value, list):
             elif isinstance(needs_value, list):
-                self.needs: List[str] = needs_value
+                self.needs: list[str] = needs_value
             else:
             else:
                 raise VariableError(
                 raise VariableError(
                     f"Section '{self.key}' has invalid 'needs' value: must be string or list"
                     f"Section '{self.key}' has invalid 'needs' value: must be string or list"
                 )
                 )
         else:
         else:
-            self.needs: List[str] = []
+            self.needs: list[str] = []
 
 
-    def to_dict(self) -> Dict[str, Any]:
+    def to_dict(self) -> dict[str, Any]:
         """Serialize VariableSection to a dictionary for storage."""
         """Serialize VariableSection to a dictionary for storage."""
         section_dict = {
         section_dict = {
             "required": self.required,
             "required": self.required,
@@ -95,7 +95,7 @@ class VariableSection:
         except Exception:
         except Exception:
             return False
             return False
 
 
-    def clone(self, origin_update: Optional[str] = None) -> "VariableSection":
+    def clone(self, origin_update: str | None = None) -> VariableSection:
         """Create a deep copy of the section with all variables.
         """Create a deep copy of the section with all variables.
 
 
         This is more efficient than converting to dict and back when copying sections.
         This is more efficient than converting to dict and back when copying sections.

+ 13 - 10
cli/core/validators.py

@@ -9,10 +9,15 @@ from __future__ import annotations
 import logging
 import logging
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from pathlib import Path
 from pathlib import Path
-from typing import Any, List, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
+
+if TYPE_CHECKING:
+    pass
 
 
 import yaml
 import yaml
 
 
+from .display import DisplayManager
+
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
@@ -20,9 +25,9 @@ class ValidationResult:
     """Represents the result of a validation operation."""
     """Represents the result of a validation operation."""
 
 
     def __init__(self):
     def __init__(self):
-        self.errors: List[str] = []
-        self.warnings: List[str] = []
-        self.info: List[str] = []
+        self.errors: list[str] = []
+        self.warnings: list[str] = []
+        self.info: list[str] = []
 
 
     def add_error(self, message: str) -> None:
     def add_error(self, message: str) -> None:
         """Add an error message."""
         """Add an error message."""
@@ -51,8 +56,6 @@ class ValidationResult:
 
 
     def display(self, context: str = "Validation") -> None:
     def display(self, context: str = "Validation") -> None:
         """Display validation results using DisplayManager."""
         """Display validation results using DisplayManager."""
-        from ..display import DisplayManager
-
         display = DisplayManager()
         display = DisplayManager()
 
 
         if self.errors:
         if self.errors:
@@ -66,7 +69,7 @@ class ValidationResult:
                 display.warning(f"  • {warning}")
                 display.warning(f"  • {warning}")
 
 
         if self.info:
         if self.info:
-            display.text(f"\n[blue] {context} Info:[/blue]")
+            display.text(f"\n[blue]i {context} Info:[/blue]")
             for info_msg in self.info:
             for info_msg in self.info:
                 display.text(f"  [blue]• {info_msg}[/blue]")
                 display.text(f"  [blue]• {info_msg}[/blue]")
 
 
@@ -106,7 +109,7 @@ class ContentValidator(ABC):
 class DockerComposeValidator(ContentValidator):
 class DockerComposeValidator(ContentValidator):
     """Validator for Docker Compose files."""
     """Validator for Docker Compose files."""
 
 
-    COMPOSE_FILENAMES = {
+    COMPOSE_FILENAMES: ClassVar[set[str]] = {
         "docker-compose.yml",
         "docker-compose.yml",
         "docker-compose.yaml",
         "docker-compose.yaml",
         "compose.yml",
         "compose.yml",
@@ -240,7 +243,7 @@ class ValidatorRegistry:
     """Registry for content validators."""
     """Registry for content validators."""
 
 
     def __init__(self):
     def __init__(self):
-        self.validators: List[ContentValidator] = []
+        self.validators: list[ContentValidator] = []
         self._register_default_validators()
         self._register_default_validators()
 
 
     def _register_default_validators(self) -> None:
     def _register_default_validators(self) -> None:
@@ -257,7 +260,7 @@ class ValidatorRegistry:
         self.validators.append(validator)
         self.validators.append(validator)
         logger.debug(f"Registered validator: {validator.__class__.__name__}")
         logger.debug(f"Registered validator: {validator.__class__.__name__}")
 
 
-    def get_validator(self, file_path: str) -> Optional[ContentValidator]:
+    def get_validator(self, file_path: str) -> ContentValidator | None:
         """Get the most appropriate validator for a file.
         """Get the most appropriate validator for a file.
 
 
         Args:
         Args:

+ 2 - 3
cli/core/version.py

@@ -6,14 +6,13 @@ Supports version strings in the format: major.minor (e.g., "1.0", "1.2")
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-import re
-from typing import Tuple
 import logging
 import logging
+import re
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 
 
-def parse_version(version_str: str) -> Tuple[int, int]:
+def parse_version(version_str: str) -> tuple[int, int]:
     """Parse a semantic version string into a tuple of integers.
     """Parse a semantic version string into a tuple of integers.
 
 
     Args:
     Args: