""" ConfigService - Project Configuration Management Handles loading, saving, and validating project-specific .dss/config.json files. Uses Pydantic for schema validation with sensible defaults. """ import json import os from pathlib import Path from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field import logging logger = logging.getLogger(__name__) # === Configuration Schema === class FigmaConfig(BaseModel): """Figma integration settings.""" file_id: Optional[str] = None team_id: Optional[str] = None class TokensConfig(BaseModel): """Design token export settings.""" output_path: str = "./tokens" format: str = "css" # css | scss | json | js class AIConfig(BaseModel): """AI assistant behavior settings.""" allowed_operations: List[str] = Field(default_factory=lambda: ["read", "write"]) context_files: List[str] = Field(default_factory=lambda: ["./README.md"]) max_file_size_kb: int = 500 class DSSConfig(BaseModel): """ Complete DSS project configuration schema. Stored in: [project_root]/.dss/config.json """ schema_version: str = "1.0" figma: FigmaConfig = Field(default_factory=FigmaConfig) tokens: TokensConfig = Field(default_factory=TokensConfig) ai: AIConfig = Field(default_factory=AIConfig) class Config: # Allow extra fields for forward compatibility extra = "allow" # === Config Service === class ConfigService: """ Service for managing project configuration files. Loads .dss/config.json from project roots, validates against schema, and provides defaults when config is missing. """ CONFIG_FILENAME = "config.json" DSS_FOLDER = ".dss" def __init__(self): """Initialize config service.""" logger.info("ConfigService initialized") def get_config_path(self, project_root: str) -> Path: """Get path to config file for a project.""" return Path(project_root) / self.DSS_FOLDER / self.CONFIG_FILENAME def get_config(self, project_root: str) -> DSSConfig: """ Load configuration for a project. Args: project_root: Absolute path to project root directory Returns: DSSConfig object (defaults if config file missing) """ config_path = self.get_config_path(project_root) if config_path.exists(): try: with open(config_path) as f: data = json.load(f) config = DSSConfig(**data) logger.debug(f"Loaded config from {config_path}") return config except (json.JSONDecodeError, Exception) as e: logger.warning(f"Failed to parse config at {config_path}: {e}") # Fall through to return defaults logger.debug(f"Using default config for {project_root}") return DSSConfig() def save_config(self, project_root: str, config: DSSConfig) -> None: """ Save configuration for a project. Args: project_root: Absolute path to project root directory config: DSSConfig object to save """ config_path = self.get_config_path(project_root) # Ensure .dss directory exists config_path.parent.mkdir(parents=True, exist_ok=True) with open(config_path, 'w') as f: json.dump(config.dict(), f, indent=2) logger.info(f"Saved config to {config_path}") def update_config(self, project_root: str, updates: Dict[str, Any]) -> DSSConfig: """ Update specific fields in project config. Args: project_root: Absolute path to project root directory updates: Dictionary of fields to update Returns: Updated DSSConfig object """ config = self.get_config(project_root) # Deep merge updates config_dict = config.dict() for key, value in updates.items(): if isinstance(value, dict) and isinstance(config_dict.get(key), dict): config_dict[key].update(value) else: config_dict[key] = value new_config = DSSConfig(**config_dict) self.save_config(project_root, new_config) return new_config def init_config(self, project_root: str) -> DSSConfig: """ Initialize config file for a new project. Creates .dss/ folder and config.json with defaults if not exists. Args: project_root: Absolute path to project root directory Returns: DSSConfig object (new or existing) """ config_path = self.get_config_path(project_root) if config_path.exists(): logger.debug(f"Config already exists at {config_path}") return self.get_config(project_root) config = DSSConfig() self.save_config(project_root, config) logger.info(f"Initialized new config at {config_path}") return config def config_exists(self, project_root: str) -> bool: """Check if config file exists for a project.""" return self.get_config_path(project_root).exists()