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:
295
tools/api/services/project_manager.py
Normal file
295
tools/api/services/project_manager.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
ProjectManager - Project Registry Service
|
||||
|
||||
Manages the server-side registry of projects, including:
|
||||
- Project registration with path validation
|
||||
- Root path storage and retrieval
|
||||
- Project lifecycle management
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
"""
|
||||
Manages project registry with root path validation.
|
||||
|
||||
Works with the existing Projects database class to add root_path support.
|
||||
Validates paths exist and are accessible before registration.
|
||||
"""
|
||||
|
||||
def __init__(self, projects_db, config_service=None):
|
||||
"""
|
||||
Initialize project manager.
|
||||
|
||||
Args:
|
||||
projects_db: Projects database class (from storage.database)
|
||||
config_service: Optional ConfigService for config initialization
|
||||
"""
|
||||
self.db = projects_db
|
||||
self.config_service = config_service
|
||||
logger.info("ProjectManager initialized")
|
||||
|
||||
def register_project(
|
||||
self,
|
||||
name: str,
|
||||
root_path: str,
|
||||
description: str = "",
|
||||
figma_file_key: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Register a new project with validated root path.
|
||||
|
||||
Args:
|
||||
name: Human-readable project name
|
||||
root_path: Absolute path to project directory
|
||||
description: Optional project description
|
||||
figma_file_key: Optional Figma file key
|
||||
|
||||
Returns:
|
||||
Created project dict
|
||||
|
||||
Raises:
|
||||
ValueError: If path doesn't exist or isn't a directory
|
||||
PermissionError: If no write access to path
|
||||
"""
|
||||
# Resolve and validate path
|
||||
root_path = os.path.abspath(root_path)
|
||||
|
||||
if not os.path.isdir(root_path):
|
||||
raise ValueError(f"Path does not exist or is not a directory: {root_path}")
|
||||
|
||||
if not os.access(root_path, os.W_OK):
|
||||
raise PermissionError(f"No write access to path: {root_path}")
|
||||
|
||||
# Check if path already registered
|
||||
existing = self.get_by_path(root_path)
|
||||
if existing:
|
||||
raise ValueError(f"Path already registered as project: {existing['name']}")
|
||||
|
||||
# Generate project ID
|
||||
import uuid
|
||||
project_id = str(uuid.uuid4())[:8]
|
||||
|
||||
# Create project in database
|
||||
project = self.db.create(
|
||||
id=project_id,
|
||||
name=name,
|
||||
description=description,
|
||||
figma_file_key=figma_file_key
|
||||
)
|
||||
|
||||
# Update with root_path (need to add this column)
|
||||
self._update_root_path(project_id, root_path)
|
||||
project['root_path'] = root_path
|
||||
|
||||
# Initialize .dss folder and config if config_service available
|
||||
if self.config_service:
|
||||
try:
|
||||
self.config_service.init_config(root_path)
|
||||
logger.info(f"Initialized .dss config for project {name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to init config for {name}: {e}")
|
||||
|
||||
logger.info(f"Registered project: {name} at {root_path}")
|
||||
return project
|
||||
|
||||
def get_project(self, project_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get project by ID with path validation.
|
||||
|
||||
Args:
|
||||
project_id: Project UUID
|
||||
|
||||
Returns:
|
||||
Project dict or None if not found
|
||||
|
||||
Raises:
|
||||
ValueError: If project path no longer exists
|
||||
"""
|
||||
project = self.db.get(project_id)
|
||||
if not project:
|
||||
return None
|
||||
|
||||
root_path = project.get('root_path')
|
||||
if root_path and not os.path.isdir(root_path):
|
||||
logger.warning(f"Project path no longer exists: {root_path}")
|
||||
# Don't raise, just mark it
|
||||
project['path_valid'] = False
|
||||
else:
|
||||
project['path_valid'] = True
|
||||
|
||||
return project
|
||||
|
||||
def list_projects(self, status: str = None, valid_only: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
List all projects with optional filtering.
|
||||
|
||||
Args:
|
||||
status: Filter by status (active, archived, etc.)
|
||||
valid_only: Only return projects with valid paths
|
||||
|
||||
Returns:
|
||||
List of project dicts
|
||||
"""
|
||||
projects = self.db.list(status=status)
|
||||
|
||||
# Add path validation status
|
||||
for project in projects:
|
||||
root_path = project.get('root_path')
|
||||
project['path_valid'] = bool(root_path and os.path.isdir(root_path))
|
||||
|
||||
if valid_only:
|
||||
projects = [p for p in projects if p.get('path_valid', False)]
|
||||
|
||||
return projects
|
||||
|
||||
def get_by_path(self, root_path: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Find project by root path.
|
||||
|
||||
Args:
|
||||
root_path: Absolute path to search for
|
||||
|
||||
Returns:
|
||||
Project dict or None if not found
|
||||
"""
|
||||
root_path = os.path.abspath(root_path)
|
||||
projects = self.list_projects()
|
||||
|
||||
for project in projects:
|
||||
if project.get('root_path') == root_path:
|
||||
return project
|
||||
|
||||
return None
|
||||
|
||||
def update_project(
|
||||
self,
|
||||
project_id: str,
|
||||
name: str = None,
|
||||
description: str = None,
|
||||
root_path: str = None,
|
||||
figma_file_key: str = None,
|
||||
status: str = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Update project fields.
|
||||
|
||||
Args:
|
||||
project_id: Project UUID
|
||||
name: Optional new name
|
||||
description: Optional new description
|
||||
root_path: Optional new root path (validated)
|
||||
figma_file_key: Optional new Figma key
|
||||
status: Optional new status
|
||||
|
||||
Returns:
|
||||
Updated project dict or None if not found
|
||||
"""
|
||||
project = self.db.get(project_id)
|
||||
if not project:
|
||||
return None
|
||||
|
||||
# Validate new root_path if provided
|
||||
if root_path:
|
||||
root_path = os.path.abspath(root_path)
|
||||
if not os.path.isdir(root_path):
|
||||
raise ValueError(f"Path does not exist: {root_path}")
|
||||
if not os.access(root_path, os.W_OK):
|
||||
raise PermissionError(f"No write access: {root_path}")
|
||||
self._update_root_path(project_id, root_path)
|
||||
|
||||
# Update other fields via existing update method
|
||||
updates = {}
|
||||
if name is not None:
|
||||
updates['name'] = name
|
||||
if description is not None:
|
||||
updates['description'] = description
|
||||
if figma_file_key is not None:
|
||||
updates['figma_file_key'] = figma_file_key
|
||||
if status is not None:
|
||||
updates['status'] = status
|
||||
|
||||
if updates:
|
||||
self.db.update(project_id, **updates)
|
||||
|
||||
return self.get_project(project_id)
|
||||
|
||||
def delete_project(self, project_id: str, delete_config: bool = False) -> bool:
|
||||
"""
|
||||
Delete a project from registry.
|
||||
|
||||
Args:
|
||||
project_id: Project UUID
|
||||
delete_config: If True, also delete .dss folder
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
project = self.db.get(project_id)
|
||||
if not project:
|
||||
return False
|
||||
|
||||
if delete_config and project.get('root_path'):
|
||||
import shutil
|
||||
dss_path = Path(project['root_path']) / '.dss'
|
||||
if dss_path.exists():
|
||||
shutil.rmtree(dss_path)
|
||||
logger.info(f"Deleted .dss folder at {dss_path}")
|
||||
|
||||
self.db.delete(project_id)
|
||||
logger.info(f"Deleted project: {project_id}")
|
||||
return True
|
||||
|
||||
def _update_root_path(self, project_id: str, root_path: str) -> None:
|
||||
"""
|
||||
Update root_path in database.
|
||||
|
||||
Uses raw SQL since the column may not be in the existing model.
|
||||
"""
|
||||
from storage.database import get_connection
|
||||
|
||||
with get_connection() as conn:
|
||||
# Ensure column exists
|
||||
try:
|
||||
conn.execute("""
|
||||
ALTER TABLE projects ADD COLUMN root_path TEXT DEFAULT ''
|
||||
""")
|
||||
logger.info("Added root_path column to projects table")
|
||||
except Exception:
|
||||
# Column already exists
|
||||
pass
|
||||
|
||||
# Update the value
|
||||
conn.execute(
|
||||
"UPDATE projects SET root_path = ? WHERE id = ?",
|
||||
(root_path, project_id)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ensure_schema():
|
||||
"""
|
||||
Ensure database schema has root_path column.
|
||||
|
||||
Call this on startup to migrate existing databases.
|
||||
"""
|
||||
from storage.database import get_connection
|
||||
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
# Check if column exists
|
||||
cursor.execute("PRAGMA table_info(projects)")
|
||||
columns = [col[1] for col in cursor.fetchall()]
|
||||
|
||||
if 'root_path' not in columns:
|
||||
cursor.execute("""
|
||||
ALTER TABLE projects ADD COLUMN root_path TEXT DEFAULT ''
|
||||
""")
|
||||
logger.info("Migration: Added root_path column to projects table")
|
||||
else:
|
||||
logger.debug("Schema check: root_path column exists")
|
||||
Reference in New Issue
Block a user