Browse Source

fixed some ruff errors

xcad 4 tháng trước cách đây
mục cha
commit
da1142b684

+ 17 - 18
cli/__main__.py

@@ -11,13 +11,16 @@ import logging
 import pkgutil
 import sys
 from pathlib import Path
-from typing import Optional
-from typer import Typer, Option
+
+import click
 from rich.console import Console
+from typer import Option, Typer
+
 import cli.modules
-from cli.core.registry import registry
-from cli.core import repo
 from cli import __version__
+from cli.core import repo
+from cli.core.registry import registry
+
 # Using standard Python exceptions instead of custom ones
 
 app = Typer(
@@ -54,12 +57,12 @@ def setup_logging(log_level: str = "WARNING") -> None:
         logger = logging.getLogger(__name__)
         logger.setLevel(numeric_level)
     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)
 def main(
-    version: Optional[bool] = Option(
+    version: bool | None = Option(
         None,
         "--version",
         "-v",
@@ -71,7 +74,7 @@ def main(
         else None,
         is_eager=True,
     ),
-    log_level: Optional[str] = Option(
+    log_level: str | None = Option(
         None,
         "--log-level",
         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)
 
     # Get context without type annotation (compatible with all Typer versions)
-    import click
-
     ctx = click.get_current_context()
 
     # 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
         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)
             if not name.startswith("_") and name != "base":
                 try:
@@ -127,11 +128,11 @@ def init_app() -> None:
                     )
                     importlib.import_module(f"cli.modules.{name}")
                 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)
                     logger.warning(error_info)
                 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)
                     logger.error(error_info)
 
@@ -140,7 +141,7 @@ def init_app() -> None:
             logger.debug("Registering repo command")
             repo.register_cli(app)
         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)
             logger.warning(error_info)
 
@@ -148,14 +149,12 @@ def init_app() -> None:
         module_classes = list(registry.iter_module_classes())
         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:
                 logger.debug(f"Registering module class: {module_cls.__name__}")
                 module_cls.register_cli(app)
             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)
                 # Log warning but don't raise exception for individual module failures
                 logger.warning(error_info)
@@ -189,7 +188,7 @@ def init_app() -> None:
             )
 
         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:

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

@@ -1,11 +1,10 @@
 from __future__ import annotations
 
 import logging
-import re
 import shutil
 import tempfile
 from pathlib import Path
-from typing import Any, Dict, Optional, Union
+from typing import Any
 
 import yaml
 
@@ -13,22 +12,12 @@ from ..exceptions import ConfigError, ConfigValidationError, YAMLParseError
 
 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:
     """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.
 
         Args:
@@ -121,75 +110,8 @@ class ConfigManager:
         except Exception as 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.
 
         Returns:
@@ -201,7 +123,7 @@ class ConfigManager:
             ConfigError: If reading fails for other reasons.
         """
         try:
-            with open(self.config_path, "r") as f:
+            with open(self.config_path) as f:
                 config = yaml.safe_load(f) or {}
 
             # Validate config structure
@@ -210,17 +132,17 @@ class ConfigManager:
             return config
         except yaml.YAMLError as 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:
             # Re-raise validation errors as-is
             raise
-        except (IOError, OSError) as e:
+        except OSError as e:
             logger.error(f"Failed to read configuration file: {e}")
             raise ConfigError(
                 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.
 
         This prevents config file corruption if write operation fails partway through.
@@ -260,20 +182,20 @@ class ConfigManager:
             if tmp_path:
                 Path(tmp_path).unlink(missing_ok=True)
             raise
-        except (IOError, OSError, yaml.YAMLError) as e:
+        except (OSError, yaml.YAMLError) as e:
             # Clean up temp file if it exists
             if tmp_path:
                 try:
                     Path(tmp_path).unlink(missing_ok=True)
-                except (IOError, OSError):
+                except OSError:
                     logger.warning(f"Failed to clean up temporary file: {tmp_path}")
             logger.error(f"Failed to write configuration file: {e}")
             raise ConfigError(
                 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:
             config: Configuration dictionary to validate.
@@ -284,197 +206,58 @@ class ConfigManager:
         if not isinstance(config, dict):
             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
-        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:
         """Get the path to the configuration file being used.
@@ -492,7 +275,7 @@ class ConfigManager:
         """
         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.
 
         Returns defaults in a flat format:
@@ -511,7 +294,7 @@ class ConfigManager:
         defaults = config.get("defaults", {})
         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.
 
         Args:
@@ -522,47 +305,13 @@ class ConfigManager:
         Raises:
             ConfigValidationError: If module name or variable names are invalid.
         """
-        # Validate module name
+        # Basic validation
         if not isinstance(module_name, str) or not module_name:
             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):
             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()
 
         if "defaults" not in config:
@@ -583,42 +332,19 @@ class ConfigManager:
         Raises:
             ConfigValidationError: If module name or variable name is invalid.
         """
-        # Validate inputs
+        # Basic validation
         if not isinstance(module_name, str) or not module_name:
             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[var_name] = value
         self.set_defaults(module_name, defaults)
         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.
 
         Args:
@@ -644,7 +370,7 @@ class ConfigManager:
             self._write_config(config)
             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.
 
         Args:
@@ -667,46 +393,10 @@ class ConfigManager:
         Raises:
             ConfigValidationError: If key or value is invalid for known preference types.
         """
-        # Validate key
+        # Basic validation
         if not isinstance(key, str) or not key:
             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()
 
         if "preferences" not in config:
@@ -716,7 +406,7 @@ class ConfigManager:
         self._write_config(config)
         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.
 
         Returns:
@@ -725,7 +415,7 @@ class ConfigManager:
         config = self._read_config()
         return config.get("preferences", {})
 
-    def get_libraries(self) -> list[Dict[str, Any]]:
+    def get_libraries(self) -> list[dict[str, Any]]:
         """Get all configured libraries.
 
         Returns:
@@ -734,7 +424,7 @@ class ConfigManager:
         config = self._read_config()
         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.
 
         Args:
@@ -753,10 +443,10 @@ class ConfigManager:
         self,
         name: str,
         library_type: str = "git",
-        url: Optional[str] = None,
-        directory: Optional[str] = None,
+        url: str | None = None,
+        directory: str | None = None,
         branch: str = "main",
-        path: Optional[str] = None,
+        path: str | None = None,
         enabled: bool = True,
     ) -> None:
         """Add a new library to the configuration.
@@ -773,45 +463,22 @@ class ConfigManager:
         Raises:
             ConfigValidationError: If library with the same name already exists or validation fails
         """
-        # Validate name
+        # Basic validation
         if not isinstance(name, str) or not name:
             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"):
             raise ConfigValidationError(
                 f"Library type must be 'git' or 'static', got '{library_type}'"
             )
 
-        # Check if library already exists
         if self.get_library_by_name(name):
             raise ConfigValidationError(f"Library '{name}' already exists")
 
-        # Type-specific validation and config building
+        # Type-specific validation
         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 = {
                 "name": name,
@@ -826,11 +493,6 @@ class ConfigManager:
             if not path:
                 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,
             # add dummy values for git-specific fields
             library_config = {
@@ -897,41 +559,16 @@ class ConfigManager:
 
                 # Update allowed fields
                 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:
-                    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:
-                    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:
-                    enabled = kwargs["enabled"]
-                    if not isinstance(enabled, bool):
-                        raise ConfigValidationError("Library enabled must be a boolean")
-                    library["enabled"] = enabled
+                    library["enabled"] = kwargs["enabled"]
 
                 break
 

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

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

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

@@ -18,17 +18,17 @@ class IconManager:
     """
 
     # 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_SUCCESS = "\uf00c"  #  (check)
@@ -38,9 +38,9 @@ class IconManager:
     STATUS_SKIPPED = "\uf05e"  #  (ban/circle-slash)
 
     # 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_BULLET = "\uf111"  #  (circle)
     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.syntax import Syntax
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..exceptions import TemplateRenderError
+    from . import DisplayManager
 
 logger = logging.getLogger(__name__)
 console_err = Console(stderr=True)  # Keep for error output
@@ -24,7 +26,7 @@ class StatusDisplayManager:
     and informational messages with consistent formatting.
     """
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize StatusDisplayManager.
 
         Args:
@@ -42,8 +44,6 @@ class StatusDisplayManager:
             message: The message to display
             context: Optional context information
         """
-        from . import IconManager
-
         # Errors and warnings always go to stderr, even in quiet mode
         # Success and info respect quiet mode and go to stdout
         use_stderr = level in ("error", "warning")
@@ -66,13 +66,13 @@ class StatusDisplayManager:
         if context:
             text = (
                 f"{level.capitalize()} in {context}: {message}"
-                if level == "error" or level == "warning"
+                if level in {"error", "warning"}
                 else f"{context}: {message}"
             )
         else:
             text = (
                 f"{level.capitalize()}: {message}"
-                if level == "error" or level == "warning"
+                if level in {"error", "warning"}
                 else message
             )
 
@@ -146,8 +146,6 @@ class StatusDisplayManager:
             required_version: Minimum CLI version required by template
             current_version: Current CLI version
         """
-        from . import IconManager
-
         console_err.print()
         console_err.print(
             f"[bold red]{IconManager.STATUS_ERROR} Version Incompatibility[/bold red]"
@@ -179,8 +177,6 @@ class StatusDisplayManager:
             message: The main message to display
             reason: Optional reason why it was skipped
         """
-        from . import IconManager
-
         icon = IconManager.get_status_icon("skipped")
         if reason:
             self.parent.text(f"\n{icon} {message} (skipped - {reason})", style="dim")
@@ -200,8 +196,6 @@ class StatusDisplayManager:
         Returns:
             True if user confirms, False otherwise
         """
-        from . import IconManager
-
         icon = IconManager.get_status_icon("warning")
         self.parent.text(f"\n{icon} {message}", style="yellow")
 
@@ -212,7 +206,7 @@ class StatusDisplayManager:
         return Confirm.ask("Continue?", default=default)
 
     def display_template_render_error(
-        self, error: "TemplateRenderError", context: str | None = None
+        self, error: TemplateRenderError, context: str | None = None
     ) -> None:
         """Display a detailed template rendering error with context and suggestions.
 
@@ -220,8 +214,6 @@ class StatusDisplayManager:
             error: TemplateRenderError exception with detailed error information
             context: Optional context information (e.g., template ID)
         """
-        from . import IconManager
-
         # Always display errors to stderr
         icon = IconManager.get_status_icon("error")
         if context:
@@ -287,7 +279,7 @@ class StatusDisplayManager:
         # Display suggestions if available
         if error.suggestions:
             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
                 console_err.print(f"  [yellow]{bullet}[/yellow] {suggestion}")
             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.tree import Tree
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
     from . import DisplayManager
 
@@ -19,7 +21,7 @@ class TableDisplayManager:
     including templates lists, status tables, and summaries.
     """
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize TableDisplayManager.
 
         Args:
@@ -89,8 +91,6 @@ class TableDisplayManager:
             rows: List of tuples (name, message, success_bool)
             columns: Column headers (name_header, status_header)
         """
-        from . import IconManager
-
         table = Table(show_header=True)
         table.add_column(columns[0], style="cyan", no_wrap=True)
         table.add_column(columns[1])
@@ -159,8 +159,6 @@ class TableDisplayManager:
             module_name: Name of the module
             show_all: If True, show all details including descriptions
         """
-        from . import IconManager
-
         if not spec:
             self.parent.text(
                 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 typing import TYPE_CHECKING
 
+from .icon_manager import IconManager
+
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..template import Template
+    from . import DisplayManager
 
 
 class TemplateDisplayManager:
@@ -15,7 +17,7 @@ class TemplateDisplayManager:
     file trees, and metadata.
     """
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize TemplateDisplayManager.
 
         Args:
@@ -23,7 +25,7 @@ class TemplateDisplayManager:
         """
         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.
 
         Args:
@@ -34,7 +36,7 @@ class TemplateDisplayManager:
         self.render_file_tree(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.
 
         Args:
@@ -67,14 +69,12 @@ class TemplateDisplayManager:
         )
         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.
 
         Args:
             template: Template instance
         """
-        from . import IconManager
-
         self.parent.text("")
         self.parent.heading("Template File Structure")
 
@@ -108,8 +108,6 @@ class TemplateDisplayManager:
             files: Dictionary of file paths to content
             existing_files: List of existing files that will be overwritten
         """
-        from . import IconManager
-
         self.parent.text("")
         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 .icon_manager import IconManager
+
 if TYPE_CHECKING:
-    from . import DisplayManager
     from ..template import Template
+    from . import DisplayManager
 
 
 class VariableDisplayManager:
@@ -16,7 +18,7 @@ class VariableDisplayManager:
     and their values with appropriate formatting based on context.
     """
 
-    def __init__(self, parent: "DisplayManager"):
+    def __init__(self, parent: DisplayManager):
         """Initialize VariableDisplayManager.
 
         Args:
@@ -42,8 +44,6 @@ class VariableDisplayManager:
         Returns:
             Formatted string representation of the variable value
         """
-        from . import IconManager
-
         # Handle disabled bool variables
         if (is_dimmed or not var_satisfied) and variable.type == "bool":
             if (
@@ -171,8 +171,6 @@ class VariableDisplayManager:
         Returns:
             Tuple of (var_display, type, default_val, description, row_style)
         """
-        from . import IconManager
-
         settings = self.parent.settings
 
         # Build row style
@@ -200,7 +198,7 @@ class VariableDisplayManager:
             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.
 
         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.
 """
 
-from typing import Optional, List, Dict
-
 
 class BoilerplatesError(Exception):
     """Base exception for all boilerplates CLI errors."""
@@ -34,7 +32,7 @@ class TemplateError(BoilerplatesError):
 class TemplateNotFoundError(TemplateError):
     """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.module_name = module_name
         msg = f"Template '{template_id}' not found"
@@ -64,7 +62,7 @@ class TemplateLoadError(TemplateError):
 class TemplateSyntaxError(TemplateError):
     """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.errors = errors
         msg = f"Jinja2 syntax errors in template '{template_id}':\n" + "\n".join(errors)
@@ -107,13 +105,13 @@ class TemplateRenderError(TemplateError):
     def __init__(
         self,
         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.line_number = line_number

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

@@ -1,9 +1,10 @@
 from __future__ import annotations
 
-from typing import Dict, Any, Callable
 import logging
+from typing import Any, Callable
+
 from rich.console import Console
-from rich.prompt import Prompt, Confirm, IntPrompt
+from rich.prompt import Confirm, IntPrompt, Prompt
 
 from .display import DisplayManager
 from .template import Variable, VariableCollection
@@ -31,7 +32,7 @@ class PromptHandler:
             logger.info("User opted to keep all default values")
             return {}
 
-        collected: Dict[str, Any] = {}
+        collected: dict[str, Any] = {}
         prompted_variables: set[str] = (
             set()
         )  # Track which variables we've already prompted for
@@ -167,9 +168,7 @@ class PromptHandler:
                 self._show_validation_error(str(exc))
             except Exception as e:
                 # 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
                 handler = self._get_prompt_handler(variable)
 

+ 12 - 12
cli/core/library.py

@@ -1,14 +1,18 @@
 from __future__ import annotations
 
-from pathlib import Path
 import logging
-from typing import Optional
+from pathlib import Path
+
 import yaml
 
-from .exceptions import LibraryError, TemplateNotFoundError, DuplicateTemplateError
+from .config import ConfigManager
+from .exceptions import DuplicateTemplateError, LibraryError, TemplateNotFoundError
 
 logger = logging.getLogger(__name__)
 
+# Qualified ID format: "template_id.library_name"
+QUALIFIED_ID_PARTS = 2
+
 
 class Library:
     """Represents a single library with a specific path."""
@@ -45,12 +49,12 @@ class Library:
             return False
 
         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]
                 return (
                     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}")
             return False
 
@@ -137,7 +141,7 @@ class Library:
         except PermissionError as e:
             raise LibraryError(
                 f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}"
-            )
+            ) from e
 
         # Sort if requested
         if sort_results:
@@ -152,8 +156,6 @@ class LibraryManager:
 
     def __init__(self) -> None:
         """Initialize LibraryManager with git-based libraries from config."""
-        from .config import ConfigManager
-
         self.config = ConfigManager()
         self.libraries = self._load_libraries_from_config()
 
@@ -246,9 +248,7 @@ class LibraryManager:
 
         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.
 
         Supports both simple IDs and qualified IDs (template.library format).
@@ -267,7 +267,7 @@ class LibraryManager:
         # Check if this is a qualified ID (contains '.')
         if "." in template_id:
             parts = template_id.rsplit(".", 1)
-            if len(parts) == 2:
+            if len(parts) == QUALIFIED_ID_PARTS:
                 base_id, requested_lib = parts
                 logger.debug(
                     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 os
 from pathlib import Path
-from typing import Dict, List, Optional
 
 from rich.prompt import Confirm
 from typer import Exit
 
+from ..config import ConfigManager
 from ..display import DisplayManager
 from ..exceptions import (
     TemplateRenderError,
     TemplateSyntaxError,
     TemplateValidationError,
 )
+from ..template import Template
+from ..validators import get_validator_registry
 from .helpers import (
-    apply_variable_defaults,
-    apply_var_file,
     apply_cli_overrides,
+    apply_var_file,
+    apply_variable_defaults,
     collect_variable_values,
 )
 
 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:
     """List all templates."""
@@ -105,8 +111,6 @@ def show_template(module_instance, id: str) -> None:
     # Apply config defaults (same as in generate)
     # This ensures the display shows the actual defaults that will be used
     if template.variables:
-        from ..config import ConfigManager
-
         config = ConfigManager()
         config_defaults = config.get_defaults(module_instance.name)
 
@@ -130,10 +134,10 @@ def show_template(module_instance, id: str) -> None:
 
 def check_output_directory(
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     interactive: bool,
     display: DisplayManager,
-) -> Optional[List[Path]]:
+) -> list[Path] | None:
     """Check output directory for conflicts and get user confirmation if needed."""
     dir_exists = output_dir.exists()
     dir_not_empty = dir_exists and any(output_dir.iterdir())
@@ -141,7 +145,7 @@ def check_output_directory(
     # Check which files already exist
     existing_files = []
     if dir_exists:
-        for file_path in rendered_files.keys():
+        for file_path in rendered_files:
             full_path = output_dir / file_path
             if full_path.exists():
                 existing_files.append(full_path)
@@ -171,8 +175,8 @@ def check_output_directory(
 
 def get_generation_confirmation(
     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,
     dry_run: bool,
     interactive: bool,
@@ -187,10 +191,13 @@ def get_generation_confirmation(
     )
 
     # 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
 
@@ -198,7 +205,7 @@ def get_generation_confirmation(
 def execute_dry_run(
     id: str,
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     show_files: bool,
     display: DisplayManager,
 ) -> None:
@@ -233,7 +240,7 @@ def execute_dry_run(
 
     # Collect unique subdirectories that would be created
     subdirs = set()
-    for file_path in rendered_files.keys():
+    for file_path in rendered_files:
         parts = Path(file_path).parts
         for i in range(1, len(parts)):
             subdirs.add(Path(*parts[:i]))
@@ -274,12 +281,12 @@ def execute_dry_run(
     display.display_info("")
 
     # Summary statistics
-    if total_size < 1024:
+    if total_size < BYTES_PER_KB:
         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:
-        size_str = f"{total_size / (1024 * 1024):.1f}MB"
+        size_str = f"{total_size / BYTES_PER_MB:.1f}MB"
 
     summary_items = {
         "Total files:": str(len(rendered_files)),
@@ -312,7 +319,7 @@ def execute_dry_run(
 
 def write_generated_files(
     output_dir: Path,
-    rendered_files: Dict[str, str],
+    rendered_files: dict[str, str],
     quiet: bool,
     display: DisplayManager,
 ) -> None:
@@ -335,10 +342,10 @@ def write_generated_files(
 def generate_template(
     module_instance,
     id: str,
-    directory: Optional[str],
+    directory: str | None,
     interactive: bool,
-    var: Optional[list[str]],
-    var_file: Optional[str],
+    var: list[str] | None,
+    var_file: str | None,
     dry_run: bool,
     show_files: bool,
     quiet: bool,
@@ -354,8 +361,6 @@ def generate_template(
     template = module_instance._load_template_by_id(id)
 
     # Apply defaults and overrides (in precedence order)
-    from ..config import ConfigManager
-
     config = ConfigManager()
     apply_variable_defaults(template, config, module_instance.name)
     apply_var_file(template, var_file, display)
@@ -450,192 +455,179 @@ def generate_template(
     except TemplateRenderError as e:
         # Display enhanced error information for template rendering errors (always show errors)
         display.display_template_render_error(e, context=f"template '{id}'")
-        raise Exit(code=1)
+        raise Exit(code=1) from None
     except Exception as e:
         display.display_error(str(e), context=f"generating template '{id}'")
-        raise Exit(code=1)
+        raise Exit(code=1) from None
 
 
 def validate_templates(
     module_instance,
     template_id: str,
-    path: Optional[str],
+    path: str | None,
     verbose: bool,
     semantic: bool,
 ) -> None:
     """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:
             module_instance.display.display_error(
                 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:
             template = module_instance._load_template_by_id(template_id)
             module_instance.display.display_info(
                 f"[bold]Validating template:[/bold] [cyan]{template_id}[/cyan]"
             )
+            return template
         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:
-                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:
-            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
 from abc import ABC
-from typing import Optional
+from typing import Annotated
 
 from typer import Argument, Option, Typer
 
 from ..display import DisplayManager
 from ..library import LibraryManager
+from ..template import Template
 from .base_commands import (
+    generate_template,
     list_templates,
     search_templates,
     show_template,
-    generate_template,
     validate_templates,
 )
 from .config_commands import (
-    config_get,
-    config_set,
-    config_remove,
     config_clear,
+    config_get,
     config_list,
+    config_remove,
+    config_set,
 )
 
 logger = logging.getLogger(__name__)
 
+# Expected length of library entry tuple: (path, library_name, needs_qualification)
+LIBRARY_ENTRY_MIN_LENGTH = 2
+
 
 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: str = "1.0"
 
     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}'")
@@ -49,8 +63,6 @@ class Module(ABC):
 
     def _load_all_templates(self, filter_fn=None) -> list:
         """Load all templates for this module with optional filtering."""
-        from ..template import Template
-
         templates = []
         entries = self.libraries.find(self.name, sort_results=True)
 
@@ -58,7 +70,9 @@ class Module(ABC):
             # Unpack entry - returns (path, library_name, needs_qualification)
             template_dir = entry[0]
             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:
                 # Get library object to determine type
@@ -95,8 +109,6 @@ class Module(ABC):
 
     def _load_template_by_id(self, id: str):
         """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}'")
 
         # find_by_id now handles both simple and qualified IDs
@@ -136,15 +148,16 @@ class Module(ABC):
 
     def list(
         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 all templates."""
         return list_templates(self, raw)
 
     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:
         """Search for templates by ID containing the search string."""
         return search_templates(self, query)
@@ -155,39 +168,47 @@ class Module(ABC):
 
     def generate(
         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:
         """Generate from template.
 
@@ -204,59 +225,48 @@ class Module(ABC):
 
     def validate(
         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:
         """Validate templates for Jinja2 syntax, undefined variables, and semantic correctness."""
         return validate_templates(self, template_id, path, verbose, semantic)
 
     def config_get(
         self,
-        var_name: Optional[str] = Argument(
-            None, help="Variable name to get (omit to show all defaults)"
-        ),
+        var_name: str | None = None,
     ) -> None:
         """Get default value(s) for this module."""
         return config_get(self, var_name)
 
     def config_set(
         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:
         """Set a default value for a variable."""
         return config_set(self, var_name, value)
 
     def config_remove(
         self,
-        var_name: str = Argument(..., help="Variable name to remove"),
+        var_name: Annotated[str, Argument(help="Variable name to remove")],
     ) -> None:
         """Remove a specific default variable value."""
         return config_remove(self, var_name)
 
     def config_clear(
         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:
         """Clear default value(s) for this module."""
         return config_clear(self, var_name, force)

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

@@ -3,18 +3,16 @@
 from __future__ import annotations
 
 import logging
-from typing import Optional
 
 from rich.prompt import Confirm
 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."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
 
     if var_name:
@@ -36,9 +34,9 @@ def config_get(module_instance, var_name: Optional[str] = None) -> None:
             module_instance.display.display_info(
                 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(
-                    f"  [green]{var_name}[/green] = [yellow]{var_value}[/yellow]"
+                    f"  [green]{config_var_name}[/green] = [yellow]{var_value}[/yellow]"
                 )
         else:
             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."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
 
     # 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:
     """Remove a specific default variable value."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     defaults = config.get_defaults(module_instance.name)
 
@@ -105,11 +99,9 @@ def config_remove(module_instance, var_name: str) -> None:
 
 
 def config_clear(
-    module_instance, var_name: Optional[str] = None, force: bool = False
+    module_instance, var_name: str | None = None, force: bool = False
 ) -> None:
     """Clear default value(s) for this module."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
     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}':",
                 "",
             ]
-            for var_name, var_value in defaults.items():
+            for clear_var_name, var_value in defaults.items():
                 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(
@@ -162,8 +154,6 @@ def config_clear(
 
 def config_list(module_instance) -> None:
     """Display the defaults for this specific module as a table."""
-    from ..config import ConfigManager
-
     config = ConfigManager()
 
     # Get only the defaults for this module
@@ -175,16 +165,11 @@ def config_list(module_instance) -> None:
         )
         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
 from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any
 
+import click
 import yaml
 from typer import Exit
 
 from ..display import DisplayManager
-from ..prompt import PromptHandler
+from ..input import PromptHandler
 
 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.
 
     Supports formats:
@@ -36,12 +37,11 @@ def parse_var_inputs(var_options: List[str], extra_args: List[str]) -> Dict[str,
         if "=" in var_option:
             key, value = var_option.split("=", 1)
             variables[key] = value
+        # --var KEY VALUE format - value should be in extra_args
+        elif extra_args:
+            variables[var_option] = extra_args.pop(0)
         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
 
@@ -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}")
 
     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)
     except yaml.YAMLError as 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
 
     if not isinstance(content, dict):
@@ -107,7 +107,7 @@ def apply_variable_defaults(template, config_manager, module_name: str) -> None:
 
 
 def apply_var_file(
-    template, var_file_path: Optional[str], display: DisplayManager
+    template, var_file_path: str | None, display: DisplayManager
 ) -> None:
     """Apply variables from a YAML file to template.
 
@@ -149,7 +149,7 @@ def apply_var_file(
         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.
 
     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)
     if ctx is None:
-        import click
-
         try:
             ctx = click.get_current_context()
         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.
 
     Args:

+ 5 - 6
cli/core/prompt.py

@@ -1,9 +1,10 @@
 from __future__ import annotations
 
-from typing import Dict, Any, Callable
 import logging
+from typing import Any, Callable
+
 from rich.console import Console
-from rich.prompt import Prompt, Confirm, IntPrompt
+from rich.prompt import Confirm, IntPrompt, Prompt
 
 from .display import DisplayManager
 from .template import Variable, VariableCollection
@@ -31,7 +32,7 @@ class PromptHandler:
             logger.info("User opted to keep all default values")
             return {}
 
-        collected: Dict[str, Any] = {}
+        collected: dict[str, Any] = {}
         prompted_variables: set[str] = (
             set()
         )  # Track which variables we've already prompted for
@@ -167,9 +168,7 @@ class PromptHandler:
                 self._show_validation_error(str(exc))
             except Exception as e:
                 # 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
                 handler = self._get_prompt_handler(variable)
 

+ 3 - 3
cli/core/registry.py

@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import logging
-from typing import Iterator, Type
+from collections.abc import Iterator
 
 logger = logging.getLogger(__name__)
 
@@ -15,7 +15,7 @@ class ModuleRegistry:
         self._modules = {}
         logger.debug("Initializing module registry")
 
-    def register(self, module_class: Type) -> None:
+    def register(self, module_class: type) -> None:
         """Register a module class."""
         # Module class defines its own name attribute
         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}'"
         )
 
-    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."""
         logger.debug(f"Iterating over {len(self._modules)} registered module classes")
         for name in sorted(self._modules.keys()):

+ 110 - 127
cli/core/repo.py

@@ -3,9 +3,9 @@
 from __future__ import annotations
 
 import logging
+import shutil
 import subprocess
 from pathlib import Path
-from typing import Optional
 
 from rich.progress import SpinnerColumn, TextColumn
 from rich.table import Table
@@ -21,9 +21,7 @@ display = DisplayManager()
 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.
 
     Args:
@@ -35,7 +33,8 @@ def _run_git_command(
     """
     try:
         result = subprocess.run(
-            ["git"] + args,
+            ["git", *args],
+            check=False,
             cwd=cwd,
             capture_output=True,
             text=True,
@@ -54,8 +53,8 @@ def _clone_or_pull_repo(
     name: str,
     url: str,
     target_path: Path,
-    branch: Optional[str] = None,
-    sparse_dir: Optional[str] = None,
+    branch: str | None = None,
+    sparse_dir: str | None = None,
 ) -> tuple[bool, str]:
     """Clone or pull a git repository with optional sparse-checkout.
 
@@ -70,122 +69,117 @@ def _clone_or_pull_repo(
         Tuple of (success, message)
     """
     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:
-        # 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()
 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)"
     ),
     verbose: bool = Option(False, "--verbose", "-v", help="Show detailed output"),
@@ -337,8 +331,6 @@ def list() -> None:
             directory = "-"
 
             # Check if static path exists
-            from pathlib import Path
-
             library_path = Path(url_or_path).expanduser()
             if not library_path.is_absolute():
                 library_path = (config.config_path.parent / library_path).resolve()
@@ -371,19 +363,11 @@ def list() -> None:
 @app.command()
 def add(
     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(
         True, "--enabled/--disabled", help="Enable or disable the library"
     ),
@@ -456,7 +440,6 @@ def remove(
             library_path = libraries_path / name
 
             if library_path.exists():
-                import shutil
 
                 shutil.rmtree(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.
 """
 
-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_section import VariableSection
-from .variable import Variable
 
 __all__ = [
     "Template",
-    "TemplateMetadata",
-    "TemplateFile",
     "TemplateErrorHandler",
+    "TemplateFile",
+    "TemplateMetadata",
+    "Variable",
     "VariableCollection",
     "VariableSection",
-    "Variable",
 ]

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

@@ -1,30 +1,42 @@
 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 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
 from jinja2 import Environment, FileSystemLoader, meta
-from jinja2.sandbox import SandboxedEnvironment
 from jinja2.exceptions import (
-    TemplateSyntaxError as Jinja2TemplateSyntaxError,
-    UndefinedError,
     TemplateError as Jinja2TemplateError,
+)
+from jinja2.exceptions import (
     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__)
 
@@ -40,8 +52,8 @@ class TemplateErrorHandler:
 
     @staticmethod
     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.
 
         Args:
@@ -56,7 +68,7 @@ class TemplateErrorHandler:
             return []
 
         try:
-            with open(file_path, "r", encoding="utf-8") as f:
+            with open(file_path, encoding="utf-8") as f:
                 lines = f.readlines()
 
             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()}")
 
             return context
-        except (IOError, OSError):
+        except OSError:
             return []
 
     @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.
 
         Args:
@@ -89,8 +101,6 @@ class TemplateErrorHandler:
         # Undefined variable errors
         if "undefined" in error_lower or "is not defined" in error_lower:
             # Try to extract variable name from error message
-            import re
-
             var_match = re.search(r"'([^']+)'.*is undefined", error_msg)
             if not var_match:
                 var_match = re.search(r"'([^']+)'.*is not defined", error_msg)
@@ -175,10 +185,10 @@ class TemplateErrorHandler:
     def parse_jinja_error(
         cls,
         error: Exception,
-        template_file: "TemplateFile",
+        template_file: TemplateFile,
         template_dir: Path,
         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.
 
         Args:
@@ -241,7 +251,7 @@ class TemplateMetadata:
     date: str
     version: str
     module: str = ""
-    tags: List[str] = field(default_factory=list)
+    tags: list[str] = field(default_factory=list)
     library: str = "unknown"
     library_type: str = "git"  # Type of library ("git" or "static")
     next_steps: str = ""
@@ -339,17 +349,17 @@ class Template:
         self.library_type = library_type
 
         # 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:
             # Find and parse the main template file (template.yaml or template.yml)
             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 ---)
                 documents = list(yaml.safe_load_all(f))
 
@@ -392,15 +402,17 @@ class Template:
 
         except (ValueError, FileNotFoundError) as 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:
             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}")
             raise TemplateLoadError(
                 f"File I/O error loading template from {template_dir}: {e}"
-            )
+            ) from e
 
     def set_qualified_id(self, library_name: str | None = None) -> None:
         """Set a qualified ID for this template (used when duplicates exist across libraries).
@@ -444,8 +456,6 @@ class Template:
         if not kind:
             return {}
         try:
-            import importlib
-
             module = importlib.import_module(f"cli.modules.{kind}")
 
             # Check if module has schema-specific specs (multi-schema support)
@@ -469,7 +479,7 @@ class Template:
         except Exception as e:
             raise ValueError(
                 f"Error loading module specifications for kind '{kind}': {e}"
-            )
+            ) from e
 
     def _merge_specs(self, module_specs: dict, template_specs: dict) -> dict:
         """Deep merge template specs with module specs using VariableCollection.
@@ -505,7 +515,7 @@ class Template:
 
     def _collect_template_files(self) -> None:
         """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 filename in files:
@@ -533,24 +543,24 @@ class Template:
 
         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.
 
         Raises:
             ValueError: If any Jinja2 template has syntax errors
         """
-        used_variables: Set[str] = set()
+        used_variables: set[str] = set()
         syntax_errors = []
 
         for template_file in self.template_files:  # Iterate over TemplateFile objects
             if template_file.file_type == "j2":
                 file_path = self.template_dir / template_file.relative_path
                 try:
-                    with open(file_path, "r", encoding="utf-8") as f:
+                    with open(file_path, encoding="utf-8") as f:
                         content = f.read()
                         ast = self.jinja_env.parse(content)  # Use lazy-loaded jinja_env
                         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)
                     syntax_errors.append(f"  - {relative_path}: File I/O error: {e}")
                 except Exception as e:
@@ -698,7 +708,7 @@ class Template:
 
     def render(
         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.
 
         Args:
@@ -712,9 +722,6 @@ class Template:
         variable_values = variables.get_satisfied_values()
 
         # Auto-generate values for autogenerated variables that are empty
-        import secrets
-        import string
-
         for section in variables.get_sections().values():
             for var_name, variable in section.variables.items():
                 if variable.autogenerated and (
@@ -793,7 +800,7 @@ class Template:
                         else {},
                         suggestions=suggestions,
                         original_error=e,
-                    )
+                    ) from e
 
                 except Exception as e:
                     # Catch any other unexpected errors
@@ -807,7 +814,7 @@ class Template:
                             "This is an unexpected error. Please check the template for issues."
                         ],
                         original_error=e,
-                    )
+                    ) from e
 
             elif template_file.file_type == "static":
                 # 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}"
                         )
 
-                    with open(file_path, "r", encoding="utf-8") as f:
+                    with open(file_path, encoding="utf-8") as f:
                         content = f.read()
                         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}")
                     raise TemplateRenderError(
                         message=f"Error reading static file: {e}",
@@ -831,7 +838,7 @@ class Template:
                             "Check that the file exists and has read permissions"
                         ],
                         original_error=e,
-                    )
+                    ) from e
 
         return rendered_files, variable_values
 
@@ -855,7 +862,7 @@ class Template:
         return "\n".join(sanitized).lstrip("\n").rstrip("\n") + "\n"
 
     @property
-    def template_files(self) -> List[TemplateFile]:
+    def template_files(self) -> list[TemplateFile]:
         if self.__template_files is None:
             self._collect_template_files()  # Populate self.__template_files
         return self.__template_files
@@ -890,7 +897,7 @@ class Template:
         return self.__jinja_env
 
     @property
-    def used_variables(self) -> Set[str]:
+    def used_variables(self) -> set[str]:
         if self.__used_variables is None:
             self.__used_variables = self._extract_all_used_variables()
         return self.__used_variables

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

@@ -1,11 +1,11 @@
 from __future__ import annotations
 
-from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
-from urllib.parse import urlparse
 import logging
 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:
     from .variable_section import VariableSection
@@ -38,28 +38,28 @@ class Variable:
             raise VariableError("Variable data must contain 'name' key")
 
         # 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
         self.name: str = data["name"]
         # 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", ""
         )
         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:
             self.value: Any = data.get("value")
         elif "default" in data:
             self.value: Any = data.get("default")
         else:
             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)
         # 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
         self.autogenerated: bool = data.get("autogenerated", False)
         # 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
         self.optional: bool = data.get("optional", False)
         # 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"
         # Supports semicolon-separated multiple conditions: "var1=value1;var2=value2,value3"
         needs_value = data.get("needs")
@@ -75,17 +75,17 @@ class Variable:
             if isinstance(needs_value, str):
                 # Split by semicolon to support multiple AND conditions in a single string
                 # 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()
                 ]
             elif isinstance(needs_value, list):
-                self.needs: List[str] = needs_value
+                self.needs: list[str] = needs_value
             else:
                 raise VariableError(
                     f"Variable '{self.name}' has invalid 'needs' value: must be string or list"
                 )
         else:
-            self.needs: List[str] = []
+            self.needs: list[str] = []
 
         # Validate and convert the default/initial value if present
         if self.value is not None:
@@ -94,7 +94,7 @@ class Variable:
             except ValueError as exc:
                 raise VariableValidationError(
                     self.name, f"Invalid default value: {exc}"
-                )
+                ) from exc
 
     def convert(self, value: Any) -> Any:
         """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
         if self.autogenerated and (
             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
 
@@ -173,9 +171,12 @@ class Variable:
             return None
 
         # 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
 
@@ -191,7 +192,7 @@ class Variable:
                 return 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."""
         if isinstance(value, int):
             return value
@@ -202,7 +203,7 @@ class Variable:
         except (TypeError, ValueError) as 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."""
         if isinstance(value, float):
             return value
@@ -213,7 +214,7 @@ class Variable:
         except (TypeError, ValueError) as 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 == "":
             return None
         val = str(value)
@@ -238,7 +239,7 @@ class Variable:
             raise ValueError("value must be a valid email address")
         return val
 
-    def to_dict(self) -> Dict[str, Any]:
+    def to_dict(self) -> dict[str, Any]:
         """Serialize Variable to a dictionary for storage."""
         result = {}
 
@@ -356,7 +357,7 @@ class Variable:
 
         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).
 
         Returns:
@@ -394,9 +395,7 @@ class Variable:
         # Explicit required flag takes highest precedence
         if self.required:
             # 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)
         if self.autogenerated:
@@ -407,13 +406,10 @@ class Variable:
             return False
 
         # Variables with a default value are not required
-        if self.value is not None:
-            return False
-
         # 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.
 
         Returns:
@@ -421,7 +417,7 @@ class Variable:
         """
         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.
 
         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 collections import defaultdict
-from typing import Any, Dict, List, Optional, Set, Union
 import logging
+from collections import defaultdict
+from typing import Any
 
+from ..exceptions import VariableError, VariableValidationError
 from .variable import Variable
 from .variable_section import VariableSection
-from ..exceptions import VariableValidationError, VariableError
 
 logger = logging.getLogger(__name__)
 
@@ -40,11 +40,11 @@ class VariableCollection:
         if not isinstance(spec, dict):
             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,
         # avoiding the need to iterate through sections. It stores references to the same
         # Variable objects contained in the _set structure.
-        self._variable_map: Dict[str, Variable] = {}
+        self._variable_map: dict[str, Variable] = {}
         self._initialize_sections(spec)
         # Validate dependencies after all sections are loaded
         self._validate_dependencies()
@@ -112,7 +112,7 @@ class VariableCollection:
 
     def _validate_unique_variable_names(self) -> None:
         """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
         for section_key, section in self._sections.items():
@@ -168,7 +168,7 @@ class VariableCollection:
             )
 
     @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).
 
         Supports three formats:
@@ -245,12 +245,11 @@ class VariableCollection:
                         if variable.type == "bool":
                             if bool(actual_value) == bool(expected_converted):
                                 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
                 else:
                     # Single expected value (original behavior)
@@ -287,30 +286,32 @@ class VariableCollection:
                         raise VariableError(
                             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
         for var_name, variable in self._variable_map.items():
             for dep in variable.needs:
                 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
         # 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)
 
                 # 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
 
@@ -503,7 +503,7 @@ class VariableCollection:
         for section in self._sections.values():
             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."""
         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]
@@ -571,11 +571,11 @@ class VariableCollection:
 
             yield section_key, section
 
-    def get_sections(self) -> Dict[str, VariableSection]:
+    def get_sections(self) -> dict[str, VariableSection]:
         """Get all sections in the collection."""
         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."""
         return self._sections.get(key)
 
@@ -632,7 +632,7 @@ class VariableCollection:
 
         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."""
         return {
             name: var.value
@@ -828,9 +828,9 @@ class VariableCollection:
 
     def merge(
         self,
-        other_spec: Union[Dict[str, Any], "VariableCollection"],
+        other_spec: dict[str, Any] | VariableCollection,
         origin: str = "override",
-    ) -> "VariableCollection":
+    ) -> VariableCollection:
         """Merge another spec or VariableCollection into this one with precedence tracking.
 
         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 []
 
                 # 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
 
                 merged_section.variables[var_name] = self_var.clone(update=update)
@@ -965,8 +966,8 @@ class VariableCollection:
         return merged_section
 
     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).
 
         OPTIMIZED: Works directly on objects without dict conversions for better performance.
@@ -1024,7 +1025,7 @@ class VariableCollection:
 
         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.
 
         Returns:

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

@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 from collections import OrderedDict
-from typing import Any, Dict, List, Optional
+from typing import Any
 
-from .variable import Variable
 from ..exceptions import VariableError
+from .variable import Variable
 
 
 class VariableSection:
@@ -28,8 +28,8 @@ class VariableSection:
         self.key: str = data["key"]
         self.title: str = data["title"]
         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)
         self._explicit_fields: set[str] = set(data.keys())
         # Default "general" section to required=True, all others to required=False
@@ -41,19 +41,19 @@ class VariableSection:
             if isinstance(needs_value, str):
                 # Split by semicolon to support multiple AND conditions in a single string
                 # 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()
                 ]
             elif isinstance(needs_value, list):
-                self.needs: List[str] = needs_value
+                self.needs: list[str] = needs_value
             else:
                 raise VariableError(
                     f"Section '{self.key}' has invalid 'needs' value: must be string or list"
                 )
         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."""
         section_dict = {
             "required": self.required,
@@ -95,7 +95,7 @@ class VariableSection:
         except Exception:
             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.
 
         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
 from abc import ABC, abstractmethod
 from pathlib import Path
-from typing import Any, List, Optional
+from typing import TYPE_CHECKING, Any, ClassVar
+
+if TYPE_CHECKING:
+    pass
 
 import yaml
 
+from .display import DisplayManager
+
 logger = logging.getLogger(__name__)
 
 
@@ -20,9 +25,9 @@ class ValidationResult:
     """Represents the result of a validation operation."""
 
     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:
         """Add an error message."""
@@ -51,8 +56,6 @@ class ValidationResult:
 
     def display(self, context: str = "Validation") -> None:
         """Display validation results using DisplayManager."""
-        from ..display import DisplayManager
-
         display = DisplayManager()
 
         if self.errors:
@@ -66,7 +69,7 @@ class ValidationResult:
                 display.warning(f"  • {warning}")
 
         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:
                 display.text(f"  [blue]• {info_msg}[/blue]")
 
@@ -106,7 +109,7 @@ class ContentValidator(ABC):
 class DockerComposeValidator(ContentValidator):
     """Validator for Docker Compose files."""
 
-    COMPOSE_FILENAMES = {
+    COMPOSE_FILENAMES: ClassVar[set[str]] = {
         "docker-compose.yml",
         "docker-compose.yaml",
         "compose.yml",
@@ -240,7 +243,7 @@ class ValidatorRegistry:
     """Registry for content validators."""
 
     def __init__(self):
-        self.validators: List[ContentValidator] = []
+        self.validators: list[ContentValidator] = []
         self._register_default_validators()
 
     def _register_default_validators(self) -> None:
@@ -257,7 +260,7 @@ class ValidatorRegistry:
         self.validators.append(validator)
         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.
 
         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
 
-import re
-from typing import Tuple
 import logging
+import re
 
 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.
 
     Args: