config.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. # Using standard Python exceptions
  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 ValueError(f"Invalid YAML format in config.yaml: {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 OSError(f"Failed to save config.yaml: {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 ValueError(f"Library with name '{library.name}' already exists")
  129. self.libraries.append(library)
  130. # Sort by priority (highest first)
  131. self.libraries.sort(key=lambda l: l.priority, reverse=True)
  132. def remove_library(self, name):
  133. """Remove a library configuration by name.
  134. Args:
  135. name: Name of the library to remove
  136. Returns:
  137. True if library was removed, False if not found
  138. """
  139. original_count = len(self.libraries)
  140. self.libraries = [lib for lib in self.libraries if lib.name != name]
  141. return len(self.libraries) < original_count
  142. def get_library(self, name):
  143. """Get a library configuration by name.
  144. Args:
  145. name: Name of the library
  146. Returns:
  147. LibraryConfig if found, None otherwise
  148. """
  149. for lib in self.libraries:
  150. if lib.name == name:
  151. return lib
  152. return None
  153. # Global configuration instance
  154. _config = None
  155. def get_config():
  156. """Get the global configuration instance.
  157. Returns:
  158. The global Config instance, loading it if necessary
  159. """
  160. global _config
  161. if _config is None:
  162. _config = Config.load()
  163. return _config
  164. def set_config(config):
  165. """Set the global configuration instance.
  166. Args:
  167. config: Config instance to use globally
  168. """
  169. global _config
  170. _config = config