config.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """Global configuration management for the boilerplate CLI."""
  2. from dataclasses import dataclass, field
  3. from pathlib import Path
  4. from typing import Any, Dict, List, Optional
  5. import logging
  6. import yaml
  7. from .exceptions import ConfigurationError
  8. logger = logging.getLogger('boilerplates')
  9. @dataclass
  10. class LibraryConfig:
  11. """Configuration for a single library."""
  12. name: str
  13. type: str # 'local' or 'git'
  14. path: Optional[str] = None # For local libraries
  15. repo: Optional[str] = None # For git libraries
  16. branch: str = "main" # For git libraries
  17. priority: int = 0 # Higher priority = checked first
  18. @dataclass
  19. class Config:
  20. """Global configuration management."""
  21. # Paths
  22. config_dir: Path = field(default_factory=lambda: Path.home() / ".boilerplates")
  23. cache_dir: Path = field(default_factory=lambda: Path.home() / ".boilerplates" / "cache")
  24. # Libraries
  25. libraries: List[LibraryConfig] = field(default_factory=list)
  26. # Application settings
  27. log_level: str = "INFO"
  28. default_editor: str = "vim"
  29. auto_update_remotes: bool = False
  30. template_validation: bool = True
  31. # UI settings
  32. use_rich_output: bool = True
  33. confirm_generation: bool = True
  34. show_summary: bool = True
  35. def __post_init__(self):
  36. """Ensure directories exist."""
  37. self.config_dir.mkdir(parents=True, exist_ok=True)
  38. self.cache_dir.mkdir(parents=True, exist_ok=True)
  39. @classmethod
  40. def load(cls, config_path=None):
  41. """Load configuration from file or use defaults.
  42. Args:
  43. config_path: Optional path to config file. If not provided,
  44. uses ~/.boilerplates/config.yaml
  45. Returns:
  46. Config instance with loaded or default values
  47. """
  48. if config_path is None:
  49. config_path = Path.home() / ".boilerplates" / "config.yaml"
  50. if config_path.exists():
  51. try:
  52. with open(config_path, 'r') as f:
  53. data = yaml.safe_load(f) or {}
  54. # Parse libraries if present
  55. libraries = []
  56. for lib_data in data.get('libraries', []):
  57. try:
  58. libraries.append(LibraryConfig(**lib_data))
  59. except TypeError as e:
  60. logger.warning(f"Invalid library configuration: {lib_data}, error: {e}")
  61. # Remove libraries from data to avoid duplicate in Config init
  62. if 'libraries' in data:
  63. del data['libraries']
  64. # Convert path strings to Path objects
  65. if 'config_dir' in data:
  66. data['config_dir'] = Path(data['config_dir'])
  67. if 'cache_dir' in data:
  68. data['cache_dir'] = Path(data['cache_dir'])
  69. config = cls(**data, libraries=libraries)
  70. logger.debug(f"Loaded configuration from {config_path}")
  71. return config
  72. except yaml.YAMLError as e:
  73. raise ConfigurationError("config.yaml", f"Invalid YAML format: {e}")
  74. except Exception as e:
  75. logger.warning(f"Failed to load config from {config_path}: {e}, using defaults")
  76. return cls()
  77. else:
  78. logger.debug(f"No config file found at {config_path}, using defaults")
  79. return cls()
  80. def save(self, config_path=None):
  81. """Save configuration to file.
  82. Args:
  83. config_path: Optional path to save config. If not provided,
  84. uses ~/.boilerplates/config.yaml
  85. """
  86. if config_path is None:
  87. config_path = self.config_dir / "config.yaml"
  88. data = {
  89. 'config_dir': str(self.config_dir),
  90. 'cache_dir': str(self.cache_dir),
  91. 'log_level': self.log_level,
  92. 'default_editor': self.default_editor,
  93. 'auto_update_remotes': self.auto_update_remotes,
  94. 'template_validation': self.template_validation,
  95. 'use_rich_output': self.use_rich_output,
  96. 'confirm_generation': self.confirm_generation,
  97. 'show_summary': self.show_summary,
  98. 'libraries': [
  99. {
  100. 'name': lib.name,
  101. 'type': lib.type,
  102. 'path': lib.path,
  103. 'repo': lib.repo,
  104. 'branch': lib.branch,
  105. 'priority': lib.priority
  106. }
  107. for lib in self.libraries
  108. ]
  109. }
  110. # Remove None values from library configs
  111. for lib in data['libraries']:
  112. lib = {k: v for k, v in lib.items() if v is not None}
  113. try:
  114. config_path.parent.mkdir(parents=True, exist_ok=True)
  115. with open(config_path, 'w') as f:
  116. yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
  117. logger.debug(f"Saved configuration to {config_path}")
  118. except Exception as e:
  119. raise ConfigurationError("config.yaml", f"Failed to save: {e}")
  120. def add_library(self, library):
  121. """Add a library configuration.
  122. Args:
  123. library: LibraryConfig instance to add
  124. """
  125. # Check for duplicate names
  126. existing_names = {lib.name for lib in self.libraries}
  127. if library.name in existing_names:
  128. raise ConfigurationError(
  129. f"library:{library.name}",
  130. f"Library with name '{library.name}' already exists"
  131. )
  132. self.libraries.append(library)
  133. # Sort by priority (highest first)
  134. self.libraries.sort(key=lambda l: l.priority, reverse=True)
  135. def remove_library(self, name):
  136. """Remove a library configuration by name.
  137. Args:
  138. name: Name of the library to remove
  139. Returns:
  140. True if library was removed, False if not found
  141. """
  142. original_count = len(self.libraries)
  143. self.libraries = [lib for lib in self.libraries if lib.name != name]
  144. return len(self.libraries) < original_count
  145. def get_library(self, name):
  146. """Get a library configuration by name.
  147. Args:
  148. name: Name of the library
  149. Returns:
  150. LibraryConfig if found, None otherwise
  151. """
  152. for lib in self.libraries:
  153. if lib.name == name:
  154. return lib
  155. return None
  156. # Global configuration instance
  157. _config = None
  158. def get_config():
  159. """Get the global configuration instance.
  160. Returns:
  161. The global Config instance, loading it if necessary
  162. """
  163. global _config
  164. if _config is None:
  165. _config = Config.load()
  166. return _config
  167. def set_config(config):
  168. """Set the global configuration instance.
  169. Args:
  170. config: Config instance to use globally
  171. """
  172. global _config
  173. _config = config