fix: Address high-severity bandit issues
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
"""
|
||||
AI Provider abstraction for Claude and Gemini
|
||||
AI Provider abstraction for Claude and Gemini.
|
||||
|
||||
Handles model-specific API calls and tool execution
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
import json
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class AIProvider(ABC):
|
||||
"""Abstract base class for AI providers"""
|
||||
"""Abstract base class for AI providers."""
|
||||
|
||||
@abstractmethod
|
||||
async def chat(
|
||||
@@ -20,7 +21,7 @@ class AIProvider(ABC):
|
||||
system_prompt: str,
|
||||
history: List[Dict[str, Any]],
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
temperature: float = 0.7
|
||||
temperature: float = 0.7,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a chat message and get response
|
||||
@@ -36,16 +37,17 @@ class AIProvider(ABC):
|
||||
|
||||
|
||||
class ClaudeProvider(AIProvider):
|
||||
"""Anthropic Claude provider"""
|
||||
"""Anthropic Claude provider."""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
self.default_model = "claude-sonnet-4-5-20250929"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Claude is available"""
|
||||
"""Check if Claude is available."""
|
||||
try:
|
||||
from anthropic import Anthropic
|
||||
|
||||
return bool(self.api_key)
|
||||
except ImportError:
|
||||
return False
|
||||
@@ -58,9 +60,9 @@ class ClaudeProvider(AIProvider):
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
temperature: float = 0.7,
|
||||
mcp_handler=None,
|
||||
mcp_context=None
|
||||
mcp_context=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Chat with Claude"""
|
||||
"""Chat with Claude."""
|
||||
|
||||
if not self.is_available():
|
||||
return {
|
||||
@@ -68,7 +70,7 @@ class ClaudeProvider(AIProvider):
|
||||
"response": "Claude not available. Install anthropic SDK or set ANTHROPIC_API_KEY.",
|
||||
"model": "error",
|
||||
"tools_used": [],
|
||||
"stop_reason": "error"
|
||||
"stop_reason": "error",
|
||||
}
|
||||
|
||||
from anthropic import Anthropic
|
||||
@@ -91,17 +93,14 @@ class ClaudeProvider(AIProvider):
|
||||
"max_tokens": 4096,
|
||||
"temperature": temperature,
|
||||
"system": system_prompt,
|
||||
"messages": messages
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
if tools:
|
||||
api_params["tools"] = tools
|
||||
|
||||
# Initial call
|
||||
response = await asyncio.to_thread(
|
||||
client.messages.create,
|
||||
**api_params
|
||||
)
|
||||
response = await asyncio.to_thread(client.messages.create, **api_params)
|
||||
|
||||
# Handle tool use loop
|
||||
tools_used = []
|
||||
@@ -120,16 +119,16 @@ class ClaudeProvider(AIProvider):
|
||||
|
||||
# Execute tool via MCP handler
|
||||
result = await mcp_handler.execute_tool(
|
||||
tool_name=tool_name,
|
||||
arguments=tool_input,
|
||||
context=mcp_context
|
||||
tool_name=tool_name, arguments=tool_input, context=mcp_context
|
||||
)
|
||||
|
||||
tools_used.append({
|
||||
"tool": tool_name,
|
||||
"success": result.success,
|
||||
"duration_ms": result.duration_ms
|
||||
})
|
||||
tools_used.append(
|
||||
{
|
||||
"tool": tool_name,
|
||||
"success": result.success,
|
||||
"duration_ms": result.duration_ms,
|
||||
}
|
||||
)
|
||||
|
||||
# Format result
|
||||
if result.success:
|
||||
@@ -137,19 +136,20 @@ class ClaudeProvider(AIProvider):
|
||||
else:
|
||||
tool_result_content = json.dumps({"error": result.error})
|
||||
|
||||
tool_results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_use_id,
|
||||
"content": tool_result_content
|
||||
})
|
||||
tool_results.append(
|
||||
{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tool_use_id,
|
||||
"content": tool_result_content,
|
||||
}
|
||||
)
|
||||
|
||||
# Continue conversation with tool results
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
messages.append({"role": "user", "content": tool_results})
|
||||
|
||||
response = await asyncio.to_thread(
|
||||
client.messages.create,
|
||||
**{**api_params, "messages": messages}
|
||||
client.messages.create, **{**api_params, "messages": messages}
|
||||
)
|
||||
|
||||
# Extract final response
|
||||
@@ -163,27 +163,30 @@ class ClaudeProvider(AIProvider):
|
||||
"response": response_text,
|
||||
"model": response.model,
|
||||
"tools_used": tools_used,
|
||||
"stop_reason": response.stop_reason
|
||||
"stop_reason": response.stop_reason,
|
||||
}
|
||||
|
||||
|
||||
class GeminiProvider(AIProvider):
|
||||
"""Google Gemini provider"""
|
||||
"""Google Gemini provider."""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
||||
self.default_model = "gemini-2.0-flash-exp"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Gemini is available"""
|
||||
"""Check if Gemini is available."""
|
||||
try:
|
||||
import google.generativeai as genai
|
||||
|
||||
return bool(self.api_key)
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def _convert_tools_to_gemini_format(self, claude_tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Convert Claude tool format to Gemini function declarations"""
|
||||
def _convert_tools_to_gemini_format(
|
||||
self, claude_tools: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Convert Claude tool format to Gemini function declarations."""
|
||||
gemini_tools = []
|
||||
|
||||
for tool in claude_tools:
|
||||
@@ -191,11 +194,7 @@ class GeminiProvider(AIProvider):
|
||||
function_declaration = {
|
||||
"name": tool.get("name"),
|
||||
"description": tool.get("description", ""),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||
}
|
||||
|
||||
# Convert input schema
|
||||
@@ -218,9 +217,9 @@ class GeminiProvider(AIProvider):
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
temperature: float = 0.7,
|
||||
mcp_handler=None,
|
||||
mcp_context=None
|
||||
mcp_context=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Chat with Gemini"""
|
||||
"""Chat with Gemini."""
|
||||
|
||||
if not self.is_available():
|
||||
return {
|
||||
@@ -228,7 +227,7 @@ class GeminiProvider(AIProvider):
|
||||
"response": "Gemini not available. Install google-generativeai SDK or set GOOGLE_API_KEY/GEMINI_API_KEY.",
|
||||
"model": "error",
|
||||
"tools_used": [],
|
||||
"stop_reason": "error"
|
||||
"stop_reason": "error",
|
||||
}
|
||||
|
||||
import google.generativeai as genai
|
||||
@@ -241,10 +240,9 @@ class GeminiProvider(AIProvider):
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
if content and role in ["user", "assistant"]:
|
||||
gemini_history.append({
|
||||
"role": "user" if role == "user" else "model",
|
||||
"parts": [content]
|
||||
})
|
||||
gemini_history.append(
|
||||
{"role": "user" if role == "user" else "model", "parts": [content]}
|
||||
)
|
||||
|
||||
# Create model with tools if available
|
||||
model_kwargs = {
|
||||
@@ -253,7 +251,7 @@ class GeminiProvider(AIProvider):
|
||||
"temperature": temperature,
|
||||
"max_output_tokens": 4096,
|
||||
},
|
||||
"system_instruction": system_prompt
|
||||
"system_instruction": system_prompt,
|
||||
}
|
||||
|
||||
# Convert and add tools if available
|
||||
@@ -282,7 +280,7 @@ class GeminiProvider(AIProvider):
|
||||
has_function_call = False
|
||||
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'function_call') and part.function_call:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
has_function_call = True
|
||||
func_call = part.function_call
|
||||
tool_name = func_call.name
|
||||
@@ -290,31 +288,34 @@ class GeminiProvider(AIProvider):
|
||||
|
||||
# Execute tool
|
||||
result = await mcp_handler.execute_tool(
|
||||
tool_name=tool_name,
|
||||
arguments=tool_args,
|
||||
context=mcp_context
|
||||
tool_name=tool_name, arguments=tool_args, context=mcp_context
|
||||
)
|
||||
|
||||
tools_used.append({
|
||||
"tool": tool_name,
|
||||
"success": result.success,
|
||||
"duration_ms": result.duration_ms
|
||||
})
|
||||
tools_used.append(
|
||||
{
|
||||
"tool": tool_name,
|
||||
"success": result.success,
|
||||
"duration_ms": result.duration_ms,
|
||||
}
|
||||
)
|
||||
|
||||
# Format result for Gemini
|
||||
function_response = {
|
||||
"name": tool_name,
|
||||
"response": result.result if result.success else {"error": result.error}
|
||||
"response": result.result
|
||||
if result.success
|
||||
else {"error": result.error},
|
||||
}
|
||||
|
||||
# Send function response back
|
||||
current_message = genai.protos.Content(
|
||||
parts=[genai.protos.Part(
|
||||
function_response=genai.protos.FunctionResponse(
|
||||
name=tool_name,
|
||||
response=function_response
|
||||
parts=[
|
||||
genai.protos.Part(
|
||||
function_response=genai.protos.FunctionResponse(
|
||||
name=tool_name, response=function_response
|
||||
)
|
||||
)
|
||||
)]
|
||||
]
|
||||
)
|
||||
break
|
||||
|
||||
@@ -328,7 +329,7 @@ class GeminiProvider(AIProvider):
|
||||
response_text = ""
|
||||
if response.candidates and response.candidates[0].content.parts:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, 'text'):
|
||||
if hasattr(part, "text"):
|
||||
response_text += part.text
|
||||
|
||||
return {
|
||||
@@ -336,13 +337,13 @@ class GeminiProvider(AIProvider):
|
||||
"response": response_text,
|
||||
"model": self.default_model,
|
||||
"tools_used": tools_used,
|
||||
"stop_reason": "stop" if response.candidates else "error"
|
||||
"stop_reason": "stop" if response.candidates else "error",
|
||||
}
|
||||
|
||||
|
||||
# Factory function
|
||||
def get_ai_provider(model_name: str) -> AIProvider:
|
||||
"""Get AI provider by name"""
|
||||
"""Get AI provider by name."""
|
||||
if model_name.lower() in ["gemini", "google"]:
|
||||
return GeminiProvider()
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Any, Optional
|
||||
|
||||
# --- Configuration ---
|
||||
# Use project-local logs directory to avoid permission issues
|
||||
@@ -21,30 +22,29 @@ browser_logger = logging.getLogger("browser_logger")
|
||||
browser_logger.setLevel(logging.INFO)
|
||||
|
||||
# Rotating file handler: 10MB max size, keep last 5 backups
|
||||
handler = RotatingFileHandler(LOG_FILE, maxBytes=10*1024*1024, backupCount=5)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s [%(levelname)s] [BROWSER] %(message)s'
|
||||
)
|
||||
handler = RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5)
|
||||
formatter = logging.Formatter("%(asctime)s [%(levelname)s] [BROWSER] %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
browser_logger.addHandler(handler)
|
||||
|
||||
# --- API Router ---
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class LogEntry(BaseModel):
|
||||
level: str
|
||||
timestamp: str
|
||||
message: str
|
||||
data: Optional[List[Any]] = None
|
||||
|
||||
|
||||
class LogBatch(BaseModel):
|
||||
logs: List[LogEntry]
|
||||
|
||||
|
||||
@router.post("/api/logs/browser")
|
||||
async def receive_browser_logs(batch: LogBatch):
|
||||
"""
|
||||
Receives a batch of logs from the browser and writes them to the log file.
|
||||
"""
|
||||
"""Receives a batch of logs from the browser and writes them to the log file."""
|
||||
try:
|
||||
for log in batch.logs:
|
||||
# Map browser levels to python logging levels
|
||||
@@ -52,11 +52,11 @@ async def receive_browser_logs(batch: LogBatch):
|
||||
|
||||
log_message = f"[{log.timestamp}] {log.message}"
|
||||
|
||||
if level == 'error':
|
||||
if level == "error":
|
||||
browser_logger.error(log_message)
|
||||
elif level == 'warn':
|
||||
elif level == "warn":
|
||||
browser_logger.warning(log_message)
|
||||
elif level == 'debug':
|
||||
elif level == "debug":
|
||||
browser_logger.debug(log_message)
|
||||
else:
|
||||
browser_logger.info(log_message)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Design System Server (DSS) - FastAPI Server
|
||||
Design System Server (DSS) - FastAPI Server.
|
||||
|
||||
Portable API server providing:
|
||||
- Project management (CRUD)
|
||||
@@ -16,37 +16,43 @@ Modes:
|
||||
Uses SQLite for persistence, integrates with Figma tools.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from config import config
|
||||
from storage.json_store import (
|
||||
Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats
|
||||
)
|
||||
from fastapi import BackgroundTasks, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from figma.figma_tools import FigmaToolSuite
|
||||
from pydantic import BaseModel
|
||||
from storage.json_store import (
|
||||
ActivityLog,
|
||||
Cache,
|
||||
Components,
|
||||
Projects,
|
||||
SyncHistory,
|
||||
Teams,
|
||||
get_stats,
|
||||
)
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
# === Runtime Configuration ===
|
||||
|
||||
|
||||
class RuntimeConfig:
|
||||
"""
|
||||
Runtime configuration that can be modified from the dashboard.
|
||||
|
||||
Persists to .dss/runtime-config.json for portability.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config_path = Path(__file__).parent.parent.parent / ".dss" / "runtime-config.json"
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -71,7 +77,7 @@ class RuntimeConfig:
|
||||
"token_sync": True,
|
||||
"code_gen": True,
|
||||
"ai_advisor": False,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def _save(self):
|
||||
@@ -114,6 +120,7 @@ runtime_config = RuntimeConfig()
|
||||
|
||||
# === Service Discovery ===
|
||||
|
||||
|
||||
class ServiceDiscovery:
|
||||
"""Discovers and manages companion services."""
|
||||
|
||||
@@ -136,13 +143,13 @@ class ServiceDiscovery:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.5)
|
||||
result = sock.connect_ex(('127.0.0.1', port))
|
||||
result = sock.connect_ex(("127.0.0.1", port))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
discovered[service] = {
|
||||
"running": True,
|
||||
"port": port,
|
||||
"url": f"http://localhost:{port}"
|
||||
"url": f"http://localhost:{port}",
|
||||
}
|
||||
break
|
||||
except:
|
||||
@@ -164,11 +171,7 @@ class ServiceDiscovery:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=2.0) as client:
|
||||
resp = await client.get(url)
|
||||
return {
|
||||
"running": resp.status_code == 200,
|
||||
"url": url,
|
||||
"port": port
|
||||
}
|
||||
return {"running": resp.status_code == 200, "url": url, "port": port}
|
||||
except:
|
||||
return {"running": False, "url": url, "port": port}
|
||||
|
||||
@@ -178,7 +181,7 @@ class ServiceDiscovery:
|
||||
app = FastAPI(
|
||||
title="Design System Server (DSS)",
|
||||
description="API for design system management and Figma integration",
|
||||
version="1.0.0"
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@@ -195,31 +198,38 @@ if UI_DIR.exists():
|
||||
app.mount("/admin-ui", StaticFiles(directory=str(UI_DIR), html=True), name="admin-ui")
|
||||
|
||||
# Initialize Figma tools
|
||||
figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output"))
|
||||
figma_suite = FigmaToolSuite(
|
||||
output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")
|
||||
)
|
||||
|
||||
|
||||
# === Request/Response Models ===
|
||||
|
||||
|
||||
class ProjectCreate(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
figma_file_key: str = ""
|
||||
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
figma_file_key: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class FigmaExtractRequest(BaseModel):
|
||||
file_key: str
|
||||
format: str = "css"
|
||||
|
||||
|
||||
class FigmaSyncRequest(BaseModel):
|
||||
file_key: str
|
||||
target_path: str
|
||||
format: str = "css"
|
||||
|
||||
|
||||
class TeamCreate(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
@@ -227,10 +237,12 @@ class TeamCreate(BaseModel):
|
||||
|
||||
# === Root & Health ===
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Redirect to Admin UI dashboard."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
return RedirectResponse(url="/admin-ui/index.html")
|
||||
|
||||
|
||||
@@ -243,30 +255,30 @@ async def health():
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
"figma_mode": figma_suite.mode,
|
||||
"config": config.summary()
|
||||
"config": config.summary(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/stats")
|
||||
async def get_statistics():
|
||||
"""Get database and system statistics."""
|
||||
db_stats = get_stats()
|
||||
return {
|
||||
"database": db_stats,
|
||||
"figma": {
|
||||
"mode": figma_suite.mode,
|
||||
"configured": config.figma.is_configured
|
||||
}
|
||||
"figma": {"mode": figma_suite.mode, "configured": config.figma.is_configured},
|
||||
}
|
||||
|
||||
|
||||
# === Projects ===
|
||||
|
||||
|
||||
@app.get("/api/projects")
|
||||
async def list_projects(status: Optional[str] = None):
|
||||
"""List all projects."""
|
||||
projects = Projects.list(status=status)
|
||||
return projects
|
||||
|
||||
|
||||
@app.get("/api/projects/{project_id}")
|
||||
async def get_project(project_id: str):
|
||||
"""Get a specific project."""
|
||||
@@ -275,6 +287,7 @@ async def get_project(project_id: str):
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@app.post("/api/projects")
|
||||
async def create_project(project: ProjectCreate):
|
||||
"""Create a new project."""
|
||||
@@ -283,17 +296,18 @@ async def create_project(project: ProjectCreate):
|
||||
id=project_id,
|
||||
name=project.name,
|
||||
description=project.description,
|
||||
figma_file_key=project.figma_file_key
|
||||
figma_file_key=project.figma_file_key,
|
||||
)
|
||||
ActivityLog.log(
|
||||
action="project_created",
|
||||
entity_type="project",
|
||||
entity_id=project_id,
|
||||
project_id=project_id,
|
||||
details={"name": project.name}
|
||||
details={"name": project.name},
|
||||
)
|
||||
return created
|
||||
|
||||
|
||||
@app.put("/api/projects/{project_id}")
|
||||
async def update_project(project_id: str, update: ProjectUpdate):
|
||||
"""Update a project."""
|
||||
@@ -311,25 +325,23 @@ async def update_project(project_id: str, update: ProjectUpdate):
|
||||
entity_type="project",
|
||||
entity_id=project_id,
|
||||
project_id=project_id,
|
||||
details=update_data
|
||||
details=update_data,
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
@app.delete("/api/projects/{project_id}")
|
||||
async def delete_project(project_id: str):
|
||||
"""Delete a project."""
|
||||
if not Projects.delete(project_id):
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
ActivityLog.log(
|
||||
action="project_deleted",
|
||||
entity_type="project",
|
||||
entity_id=project_id
|
||||
)
|
||||
ActivityLog.log(action="project_deleted", entity_type="project", entity_id=project_id)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# === Components ===
|
||||
|
||||
|
||||
@app.get("/api/projects/{project_id}/components")
|
||||
async def list_components(project_id: str):
|
||||
"""List components for a project."""
|
||||
@@ -340,6 +352,7 @@ async def list_components(project_id: str):
|
||||
|
||||
# === Figma Integration ===
|
||||
|
||||
|
||||
@app.post("/api/figma/extract-variables")
|
||||
async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks):
|
||||
"""Extract design tokens from Figma file."""
|
||||
@@ -348,12 +361,17 @@ async def extract_variables(request: FigmaExtractRequest, background_tasks: Back
|
||||
ActivityLog.log(
|
||||
action="figma_extract_variables",
|
||||
entity_type="figma",
|
||||
details={"file_key": request.file_key, "format": request.format, "count": result.get("tokens_count")}
|
||||
details={
|
||||
"file_key": request.file_key,
|
||||
"format": request.format,
|
||||
"count": result.get("tokens_count"),
|
||||
},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/figma/extract-components")
|
||||
async def extract_components(request: FigmaExtractRequest):
|
||||
"""Extract components from Figma file."""
|
||||
@@ -362,12 +380,13 @@ async def extract_components(request: FigmaExtractRequest):
|
||||
ActivityLog.log(
|
||||
action="figma_extract_components",
|
||||
entity_type="figma",
|
||||
details={"file_key": request.file_key, "count": result.get("components_count")}
|
||||
details={"file_key": request.file_key, "count": result.get("components_count")},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/figma/extract-styles")
|
||||
async def extract_styles(request: FigmaExtractRequest):
|
||||
"""Extract styles from Figma file."""
|
||||
@@ -377,20 +396,28 @@ async def extract_styles(request: FigmaExtractRequest):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/figma/sync-tokens")
|
||||
async def sync_tokens(request: FigmaSyncRequest):
|
||||
"""Sync tokens from Figma to target path."""
|
||||
try:
|
||||
result = await figma_suite.sync_tokens(request.file_key, request.target_path, request.format)
|
||||
result = await figma_suite.sync_tokens(
|
||||
request.file_key, request.target_path, request.format
|
||||
)
|
||||
ActivityLog.log(
|
||||
action="figma_sync_tokens",
|
||||
entity_type="figma",
|
||||
details={"file_key": request.file_key, "target": request.target_path, "synced": result.get("tokens_synced")}
|
||||
details={
|
||||
"file_key": request.file_key,
|
||||
"target": request.target_path,
|
||||
"synced": result.get("tokens_synced"),
|
||||
},
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/figma/validate")
|
||||
async def validate_components(request: FigmaExtractRequest):
|
||||
"""Validate components against design system rules."""
|
||||
@@ -400,6 +427,7 @@ async def validate_components(request: FigmaExtractRequest):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/figma/generate-code")
|
||||
async def generate_code(file_key: str, component_name: str, framework: str = "webcomponent"):
|
||||
"""Generate component code from Figma."""
|
||||
@@ -412,6 +440,7 @@ async def generate_code(file_key: str, component_name: str, framework: str = "we
|
||||
|
||||
# === Discovery ===
|
||||
|
||||
|
||||
@app.get("/api/discovery")
|
||||
async def run_discovery(path: str = "."):
|
||||
"""Run project discovery."""
|
||||
@@ -419,10 +448,7 @@ async def run_discovery(path: str = "."):
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(script_path), path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
[str(script_path), path], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
@@ -433,22 +459,19 @@ async def run_discovery(path: str = "."):
|
||||
except json.JSONDecodeError:
|
||||
return {"raw_output": result.stdout}
|
||||
|
||||
|
||||
@app.get("/api/discovery/ports")
|
||||
async def discover_ports():
|
||||
"""Discover listening ports and services."""
|
||||
script_path = Path(__file__).parent.parent / "discovery" / "discover-ports.sh"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(script_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
result = subprocess.run([str(script_path)], capture_output=True, text=True, timeout=10)
|
||||
return json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/discovery/env")
|
||||
async def discover_env(path: str = "."):
|
||||
"""Analyze environment configuration."""
|
||||
@@ -456,10 +479,7 @@ async def discover_env(path: str = "."):
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(script_path), path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
[str(script_path), path], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
@@ -468,24 +488,30 @@ async def discover_env(path: str = "."):
|
||||
|
||||
# === Activity & Sync History ===
|
||||
|
||||
|
||||
@app.get("/api/activity")
|
||||
async def get_activity(limit: int = Query(default=50, le=100)):
|
||||
"""Get recent activity log."""
|
||||
return ActivityLog.recent(limit=limit)
|
||||
|
||||
|
||||
@app.get("/api/sync-history")
|
||||
async def get_sync_history(project_id: Optional[str] = None, limit: int = Query(default=20, le=100)):
|
||||
async def get_sync_history(
|
||||
project_id: Optional[str] = None, limit: int = Query(default=20, le=100)
|
||||
):
|
||||
"""Get sync history."""
|
||||
return SyncHistory.recent(project_id=project_id, limit=limit)
|
||||
|
||||
|
||||
# === Teams ===
|
||||
|
||||
|
||||
@app.get("/api/teams")
|
||||
async def list_teams():
|
||||
"""List all teams."""
|
||||
return Teams.list()
|
||||
|
||||
|
||||
@app.post("/api/teams")
|
||||
async def create_team(team: TeamCreate):
|
||||
"""Create a new team."""
|
||||
@@ -493,6 +519,7 @@ async def create_team(team: TeamCreate):
|
||||
created = Teams.create(team_id, team.name, team.description)
|
||||
return created
|
||||
|
||||
|
||||
@app.get("/api/teams/{team_id}")
|
||||
async def get_team(team_id: str):
|
||||
"""Get a specific team."""
|
||||
@@ -504,12 +531,14 @@ async def get_team(team_id: str):
|
||||
|
||||
# === Cache Management ===
|
||||
|
||||
|
||||
@app.post("/api/cache/clear")
|
||||
async def clear_cache():
|
||||
"""Clear expired cache entries."""
|
||||
count = Cache.clear_expired()
|
||||
return {"cleared": count}
|
||||
|
||||
|
||||
@app.delete("/api/cache")
|
||||
async def purge_cache():
|
||||
"""Purge all cache entries."""
|
||||
@@ -519,6 +548,7 @@ async def purge_cache():
|
||||
|
||||
# === Configuration Management ===
|
||||
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
mode: Optional[str] = None
|
||||
figma_token: Optional[str] = None
|
||||
@@ -532,7 +562,7 @@ async def get_config():
|
||||
return {
|
||||
"config": runtime_config.get(),
|
||||
"env": config.summary(),
|
||||
"mode": runtime_config.get("mode")
|
||||
"mode": runtime_config.get("mode"),
|
||||
}
|
||||
|
||||
|
||||
@@ -548,11 +578,13 @@ async def update_config(update: ConfigUpdate):
|
||||
runtime_config.set_figma_token(update.figma_token)
|
||||
# Reinitialize Figma tools with new token
|
||||
global figma_suite
|
||||
figma_suite = FigmaToolSuite(output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output"))
|
||||
figma_suite = FigmaToolSuite(
|
||||
output_dir=str(Path(__file__).parent.parent.parent / ".dss" / "output")
|
||||
)
|
||||
ActivityLog.log(
|
||||
action="figma_token_updated",
|
||||
entity_type="config",
|
||||
details={"configured": bool(update.figma_token)}
|
||||
details={"configured": bool(update.figma_token)},
|
||||
)
|
||||
|
||||
if update.services:
|
||||
@@ -564,9 +596,7 @@ async def update_config(update: ConfigUpdate):
|
||||
if updates:
|
||||
runtime_config.update(updates)
|
||||
ActivityLog.log(
|
||||
action="config_updated",
|
||||
entity_type="config",
|
||||
details={"keys": list(updates.keys())}
|
||||
action="config_updated", entity_type="config", details={"keys": list(updates.keys())}
|
||||
)
|
||||
|
||||
return runtime_config.get()
|
||||
@@ -586,7 +616,7 @@ async def get_figma_config():
|
||||
"sync_tokens": True,
|
||||
"validate": True,
|
||||
"generate_code": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -600,18 +630,16 @@ async def test_figma_connection():
|
||||
|
||||
# Test with a minimal API call
|
||||
import httpx
|
||||
|
||||
token = runtime_config._data["figma"]["token"]
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
"https://api.figma.com/v1/me",
|
||||
headers={"X-Figma-Token": token}
|
||||
)
|
||||
resp = await client.get("https://api.figma.com/v1/me", headers={"X-Figma-Token": token})
|
||||
if resp.status_code == 200:
|
||||
user = resp.json()
|
||||
return {
|
||||
"success": True,
|
||||
"user": user.get("email", "connected"),
|
||||
"handle": user.get("handle")
|
||||
"handle": user.get("handle"),
|
||||
}
|
||||
else:
|
||||
return {"success": False, "error": f"API returned {resp.status_code}"}
|
||||
@@ -621,6 +649,7 @@ async def test_figma_connection():
|
||||
|
||||
# === Service Discovery ===
|
||||
|
||||
|
||||
@app.get("/api/services")
|
||||
async def list_services():
|
||||
"""List configured and discovered services."""
|
||||
@@ -630,7 +659,7 @@ async def list_services():
|
||||
return {
|
||||
"configured": configured,
|
||||
"discovered": discovered,
|
||||
"storybook": await ServiceDiscovery.check_storybook()
|
||||
"storybook": await ServiceDiscovery.check_storybook(),
|
||||
}
|
||||
|
||||
|
||||
@@ -645,7 +674,7 @@ async def configure_service(service_name: str, config_data: Dict[str, Any]):
|
||||
action="service_configured",
|
||||
entity_type="service",
|
||||
entity_id=service_name,
|
||||
details={"keys": list(config_data.keys())}
|
||||
details={"keys": list(config_data.keys())},
|
||||
)
|
||||
|
||||
return services[service_name]
|
||||
@@ -659,6 +688,7 @@ async def get_storybook_status():
|
||||
|
||||
# === DSS Mode ===
|
||||
|
||||
|
||||
@app.get("/api/mode")
|
||||
async def get_mode():
|
||||
"""Get current DSS mode."""
|
||||
@@ -666,7 +696,7 @@ async def get_mode():
|
||||
return {
|
||||
"mode": mode,
|
||||
"description": "Local dev companion" if mode == "local" else "Remote design system server",
|
||||
"features": runtime_config.get("features")
|
||||
"features": runtime_config.get("features"),
|
||||
}
|
||||
|
||||
|
||||
@@ -677,11 +707,7 @@ async def set_mode(mode: str):
|
||||
raise HTTPException(status_code=400, detail="Mode must be 'local' or 'server'")
|
||||
|
||||
runtime_config.set("mode", mode)
|
||||
ActivityLog.log(
|
||||
action="mode_changed",
|
||||
entity_type="config",
|
||||
details={"mode": mode}
|
||||
)
|
||||
ActivityLog.log(action="mode_changed", entity_type="config", details={"mode": mode})
|
||||
|
||||
return {"mode": mode, "success": True}
|
||||
|
||||
@@ -704,7 +730,8 @@ if __name__ == "__main__":
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
|
||||
url = f"http://{host}:{port}"
|
||||
print(f"""
|
||||
print(
|
||||
f"""
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ Design System Server (DSS) - Portable Server ║
|
||||
╠═══════════════════════════════════════════════════════════════╣
|
||||
@@ -714,11 +741,7 @@ if __name__ == "__main__":
|
||||
║ Environment: {config.server.env:^47}║
|
||||
║ Figma Mode: {figma_suite.mode:^47}║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
uvicorn.run(
|
||||
"server:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=config.server.env == "development"
|
||||
"""
|
||||
)
|
||||
|
||||
uvicorn.run("server:app", host=host, port=port, reload=config.server.env == "development")
|
||||
|
||||
Reference in New Issue
Block a user