fix: Address high-severity bandit issues

This commit is contained in:
DSS
2025-12-11 07:13:06 -03:00
parent bcb4475744
commit 5b2a328dd1
167 changed files with 7051 additions and 7168 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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")