Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
161
dss-claude-plugin/core/config.py
Normal file
161
dss-claude-plugin/core/config.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
DSS Configuration Module
|
||||
========================
|
||||
|
||||
Handles configuration management for the Design System Swarm (DSS) Claude Plugin.
|
||||
Supports local/remote mode detection, persistent configuration storage, and
|
||||
environment variable overrides.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, Any
|
||||
|
||||
import aiohttp
|
||||
from pydantic import BaseModel, Field, HttpUrl, ValidationError
|
||||
|
||||
# Configure module-level logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_DIR = Path.home() / ".dss"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
DEFAULT_REMOTE_URL = "https://dss.overbits.luz.uy"
|
||||
DEFAULT_LOCAL_URL = "http://localhost:6006"
|
||||
|
||||
|
||||
class DSSMode(str, Enum):
|
||||
"""Operation modes for the DSS plugin."""
|
||||
LOCAL = "local"
|
||||
REMOTE = "remote"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
class DSSConfig(BaseModel):
|
||||
"""
|
||||
Configuration model for DSS Plugin.
|
||||
|
||||
Attributes:
|
||||
mode (DSSMode): The configured operation mode (default: AUTO).
|
||||
remote_url (str): URL for the remote DSS API.
|
||||
local_url (str): URL for the local DSS API (usually localhost).
|
||||
session_id (str): Unique identifier for this client instance.
|
||||
"""
|
||||
mode: DSSMode = Field(default=DSSMode.AUTO, description="Operation mode preference")
|
||||
remote_url: str = Field(default=DEFAULT_REMOTE_URL, description="Remote API endpoint")
|
||||
local_url: str = Field(default=DEFAULT_LOCAL_URL, description="Local API endpoint")
|
||||
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Persistent session ID")
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
extra = "ignore" # Allow forward compatibility with new config keys
|
||||
|
||||
@classmethod
|
||||
def load(cls) -> "DSSConfig":
|
||||
"""
|
||||
Load configuration from ~/.dss/config.json.
|
||||
Returns a default instance if the file does not exist or is invalid.
|
||||
"""
|
||||
if not CONFIG_FILE.exists():
|
||||
logger.debug(f"No config found at {CONFIG_FILE}, using defaults.")
|
||||
return cls()
|
||||
|
||||
try:
|
||||
content = CONFIG_FILE.read_text(encoding="utf-8")
|
||||
data = json.loads(content)
|
||||
# Ensure complex types are handled by Pydantic validation
|
||||
return cls.model_validate(data)
|
||||
except (json.JSONDecodeError, ValidationError) as e:
|
||||
logger.warning(f"Failed to load config from {CONFIG_FILE}: {e}. Using defaults.")
|
||||
return cls()
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error loading config: {e}")
|
||||
return cls()
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save the current configuration to ~/.dss/config.json.
|
||||
Creates the directory if it does not exist.
|
||||
"""
|
||||
try:
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Export using mode='json' to handle enums and urls correctly
|
||||
json_data = self.model_dump_json(indent=2)
|
||||
CONFIG_FILE.write_text(json_data, encoding="utf-8")
|
||||
logger.debug(f"Configuration saved to {CONFIG_FILE}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save config to {CONFIG_FILE}: {e}")
|
||||
raise
|
||||
|
||||
async def get_active_mode(self) -> DSSMode:
|
||||
"""
|
||||
Determine the actual runtime mode based on priority rules.
|
||||
|
||||
Priority:
|
||||
1. DSS_MODE environment variable
|
||||
2. Configured 'mode' (if not AUTO)
|
||||
3. Auto-detection (ping local health endpoint)
|
||||
4. Fallback to REMOTE
|
||||
|
||||
Returns:
|
||||
DSSMode: The resolved active mode (LOCAL or REMOTE).
|
||||
"""
|
||||
# 1. Check Environment Variable
|
||||
env_mode = os.getenv("DSS_MODE")
|
||||
if env_mode:
|
||||
try:
|
||||
# Normalize string to enum
|
||||
return DSSMode(env_mode.lower())
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid DSS_MODE env var '{env_mode}', ignoring.")
|
||||
|
||||
# 2. Check Configuration (if explicit)
|
||||
if self.mode != DSSMode.AUTO:
|
||||
return self.mode
|
||||
|
||||
# 3. Auto-detect
|
||||
logger.info("Auto-detecting DSS mode...")
|
||||
is_local_healthy = await self._check_local_health()
|
||||
|
||||
if is_local_healthy:
|
||||
logger.info(f"Local server detected at {self.local_url}. Switching to LOCAL mode.")
|
||||
return DSSMode.LOCAL
|
||||
else:
|
||||
logger.info("Local server unreachable. Fallback to REMOTE mode.")
|
||||
# 4. Fallback
|
||||
return DSSMode.REMOTE
|
||||
|
||||
async def _check_local_health(self) -> bool:
|
||||
"""
|
||||
Ping the local server health endpoint to check availability.
|
||||
|
||||
Returns:
|
||||
bool: True if server responds with 200 OK, False otherwise.
|
||||
"""
|
||||
health_url = f"{self.local_url.rstrip('/')}/health"
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=2.0) # Short timeout for responsiveness
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(health_url) as response:
|
||||
if response.status == 200:
|
||||
return True
|
||||
logger.debug(f"Local health check returned status {response.status}")
|
||||
except aiohttp.ClientError as e:
|
||||
logger.debug(f"Local health check connection failed: {e}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Unexpected error during health check: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def get_api_url(self, active_mode: DSSMode) -> str:
|
||||
"""
|
||||
Helper to get the correct API URL for the determined mode.
|
||||
"""
|
||||
if active_mode == DSSMode.LOCAL:
|
||||
return self.local_url
|
||||
return self.remote_url
|
||||
Reference in New Issue
Block a user