Files
dss/dss/project/sync.py
Bruno Sarlo 41fba59bf7 Major refactor: Consolidate DSS into unified package structure
- Create new dss/ Python package at project root
- Move MCP core from tools/dss_mcp/ to dss/mcp/
- Move storage layer from tools/storage/ to dss/storage/
- Move domain logic from dss-mvp1/dss/ to dss/
- Move services from tools/api/services/ to dss/services/
- Move API server to apps/api/
- Move CLI to apps/cli/
- Move Storybook assets to storybook/
- Create unified dss/__init__.py with comprehensive exports
- Merge configuration into dss/settings.py (Pydantic-based)
- Create pyproject.toml for proper package management
- Update startup scripts for new paths
- Remove old tools/ and dss-mvp1/ directories

Architecture changes:
- DSS is now MCP-first with 40+ tools for Claude Code
- Clean imports: from dss import Projects, Components, FigmaToolSuite
- No more sys.path.insert() hacking
- apps/ contains thin application wrappers (API, CLI)
- Single unified Python package for all DSS logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 12:46:43 -03:00

353 lines
12 KiB
Python

"""
DSS Core Sync
Syncs the canonical DSS Figma (shadcn/ui) to the DSS core tokens.
This is the base layer that all skins and projects inherit from.
"""
import json
import logging
import os
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
from dss.project.core import (
DSS_FIGMA_REFERENCE,
DSS_CORE_DIR,
DSS_CACHE_DIR,
DSS_CORE_THEMES,
ensure_dss_directories,
)
from dss.project.figma import FigmaProjectSync, FigmaStyleData
logger = logging.getLogger(__name__)
class DSSCoreSync:
"""
Syncs the DSS core design system from Figma.
The shadcn/ui Figma file is the canonical source for:
- Color tokens (light/dark themes)
- Typography scale
- Spacing scale
- Component definitions
- Effects (shadows, etc.)
"""
def __init__(self, figma_token: Optional[str] = None):
"""
Initialize DSS core sync.
Args:
figma_token: Figma token. Uses FIGMA_TOKEN env var if not provided.
"""
self.figma_token = figma_token or os.environ.get("FIGMA_TOKEN")
self.reference = DSS_FIGMA_REFERENCE
ensure_dss_directories()
@property
def core_manifest_path(self) -> Path:
"""Path to DSS core manifest file."""
return DSS_CORE_DIR / "manifest.json"
@property
def core_tokens_path(self) -> Path:
"""Path to DSS core tokens file."""
return DSS_CORE_DIR / "tokens.json"
@property
def core_themes_path(self) -> Path:
"""Path to DSS core themes file."""
return DSS_CORE_DIR / "themes.json"
@property
def core_components_path(self) -> Path:
"""Path to DSS core components file."""
return DSS_CORE_DIR / "components.json"
def get_sync_status(self) -> Dict[str, Any]:
"""Get current sync status."""
manifest = self._load_manifest()
return {
"synced": manifest is not None,
"last_sync": manifest.get("last_sync") if manifest else None,
"figma_reference": {
"team_id": self.reference.team_id,
"project_id": self.reference.project_id,
"uikit_file_key": self.reference.uikit_file_key,
"uikit_file_name": self.reference.uikit_file_name,
},
"core_dir": str(DSS_CORE_DIR),
"files": {
"manifest": self.core_manifest_path.exists(),
"tokens": self.core_tokens_path.exists(),
"themes": self.core_themes_path.exists(),
"components": self.core_components_path.exists(),
}
}
def sync(self, force: bool = False) -> Dict[str, Any]:
"""
Sync DSS core from Figma.
Args:
force: Force sync even if recently synced
Returns:
Sync result with extracted data summary
"""
if not self.figma_token:
return {
"success": False,
"error": "FIGMA_TOKEN not configured. Set env var or pass token."
}
# Check if sync needed
manifest = self._load_manifest()
if manifest and not force:
last_sync = manifest.get("last_sync")
if last_sync:
# Could add time-based check here
pass
try:
# Initialize Figma sync
figma = FigmaProjectSync(token=self.figma_token)
# Extract styles from UIKit file
logger.info(f"Syncing from Figma: {self.reference.uikit_file_name}")
styles = figma.get_file_styles(self.reference.uikit_file_key)
# Process and save tokens
tokens = self._process_tokens(styles)
self._save_tokens(tokens)
# Save themes (combine Figma + defaults)
themes = self._process_themes(styles)
self._save_themes(themes)
# Save components
components = self._process_components(styles)
self._save_components(components)
# Update manifest
self._save_manifest(styles)
return {
"success": True,
"message": f"Synced DSS core from {self.reference.uikit_file_name}",
"summary": {
"colors": len(styles.colors),
"typography": len(styles.typography),
"effects": len(styles.effects),
"variables": len(styles.variables),
},
"files_written": [
str(self.core_manifest_path),
str(self.core_tokens_path),
str(self.core_themes_path),
str(self.core_components_path),
]
}
except Exception as e:
logger.exception("DSS core sync failed")
return {"success": False, "error": str(e)}
def _process_tokens(self, styles: FigmaStyleData) -> Dict[str, Any]:
"""Process Figma styles into DSS token format."""
tokens = {
"version": "1.0.0",
"source": "figma",
"figma_file": self.reference.uikit_file_key,
"synced_at": datetime.now().isoformat(),
"categories": {}
}
# Colors
tokens["categories"]["color"] = {}
for path, data in styles.colors.items():
tokens["categories"]["color"][path] = {
"value": None, # Value comes from variables or manual mapping
"figma_id": data.get("figma_id"),
"description": data.get("description", ""),
}
# Add variables as color tokens (they have actual values)
for path, data in styles.variables.items():
if data.get("type") == "COLOR":
tokens["categories"]["color"][path] = {
"value": data.get("values", {}),
"figma_id": data.get("figma_id"),
"type": "variable",
}
# Typography
tokens["categories"]["typography"] = {}
for path, data in styles.typography.items():
tokens["categories"]["typography"][path] = {
"value": None,
"figma_id": data.get("figma_id"),
"name": data.get("name"),
}
# Effects (shadows, blurs)
tokens["categories"]["effect"] = {}
for path, data in styles.effects.items():
tokens["categories"]["effect"][path] = {
"value": None,
"figma_id": data.get("figma_id"),
"name": data.get("name"),
}
return tokens
def _process_themes(self, styles: FigmaStyleData) -> Dict[str, Any]:
"""Process themes, merging Figma data with DSS defaults."""
themes = {
"version": "1.0.0",
"source": "dss-core",
"synced_at": datetime.now().isoformat(),
"themes": {}
}
# Start with DSS core defaults
for theme_name, theme_data in DSS_CORE_THEMES.items():
themes["themes"][theme_name] = {
"description": theme_data["description"],
"colors": theme_data["colors"].copy(),
"source": "dss-defaults",
}
# Overlay any Figma variables that map to themes
# (Figma variables can have modes like light/dark)
for path, data in styles.variables.items():
values_by_mode = data.get("values", {})
for mode_id, value in values_by_mode.items():
# Try to map mode to theme
# This is simplified - real implementation would use Figma mode names
pass
return themes
def _process_components(self, styles: FigmaStyleData) -> Dict[str, Any]:
"""Extract component information from Figma."""
from dss.project.core import DSS_CORE_COMPONENTS
components = {
"version": "1.0.0",
"source": "dss-core",
"synced_at": datetime.now().isoformat(),
"components": {}
}
# Start with DSS core component definitions
for name, comp_data in DSS_CORE_COMPONENTS.items():
components["components"][name] = {
"variants": comp_data.get("variants", []),
"source": "dss-core",
}
return components
def _load_manifest(self) -> Optional[Dict[str, Any]]:
"""Load existing manifest if present."""
if self.core_manifest_path.exists():
try:
with open(self.core_manifest_path, "r") as f:
return json.load(f)
except Exception:
return None
return None
def _save_manifest(self, styles: FigmaStyleData):
"""Save sync manifest."""
manifest = {
"version": "1.0.0",
"last_sync": datetime.now().isoformat(),
"figma_reference": {
"team_id": self.reference.team_id,
"team_name": self.reference.team_name,
"project_id": self.reference.project_id,
"project_name": self.reference.project_name,
"uikit_file_key": self.reference.uikit_file_key,
"uikit_file_name": self.reference.uikit_file_name,
},
"stats": {
"colors": len(styles.colors),
"typography": len(styles.typography),
"effects": len(styles.effects),
"variables": len(styles.variables),
}
}
with open(self.core_manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
def _save_tokens(self, tokens: Dict[str, Any]):
"""Save tokens to file."""
with open(self.core_tokens_path, "w") as f:
json.dump(tokens, f, indent=2)
def _save_themes(self, themes: Dict[str, Any]):
"""Save themes to file."""
with open(self.core_themes_path, "w") as f:
json.dump(themes, f, indent=2)
def _save_components(self, components: Dict[str, Any]):
"""Save components to file."""
with open(self.core_components_path, "w") as f:
json.dump(components, f, indent=2)
def get_tokens(self) -> Optional[Dict[str, Any]]:
"""Load synced tokens."""
if self.core_tokens_path.exists():
with open(self.core_tokens_path, "r") as f:
return json.load(f)
return None
def get_themes(self) -> Optional[Dict[str, Any]]:
"""Load synced themes."""
if self.core_themes_path.exists():
with open(self.core_themes_path, "r") as f:
return json.load(f)
return None
def get_components(self) -> Optional[Dict[str, Any]]:
"""Load synced components."""
if self.core_components_path.exists():
with open(self.core_components_path, "r") as f:
return json.load(f)
return None
# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================
def sync_dss_core(figma_token: Optional[str] = None, force: bool = False) -> Dict[str, Any]:
"""Sync DSS core from Figma."""
sync = DSSCoreSync(figma_token=figma_token)
return sync.sync(force=force)
def get_dss_core_status() -> Dict[str, Any]:
"""Get DSS core sync status."""
sync = DSSCoreSync()
return sync.get_sync_status()
def get_dss_core_tokens() -> Optional[Dict[str, Any]]:
"""Get DSS core tokens (must be synced first)."""
sync = DSSCoreSync()
return sync.get_tokens()
def get_dss_core_themes() -> Optional[Dict[str, Any]]:
"""Get DSS core themes."""
sync = DSSCoreSync()
return sync.get_themes()