Files
dss/dss-mvp1/dss/project/sync.py
Digital Production Factory 276ed71f31 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
2025-12-09 18:45:48 -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()