Files
dss/servers/visual-qa/src/index.ts
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

673 lines
20 KiB
TypeScript
Executable File

/**
* Design System Server (DSS) - Worker Node
*
* A fast, deterministic API server for design system management.
*
* Features:
* - Smart port detection (auto-finds available port)
* - Project import/export for git-friendly backups
* - Deterministic responses (no random behavior)
* - Fast startup with minimal blocking
*/
import Fastify from "fastify";
import fastifyStatic from "@fastify/static";
import { Api } from "figma-api";
import { compare } from "odiff-bin";
import fs from "fs-extra";
import path from "path";
import { fileURLToPath } from "url";
import { createServer } from "net";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// === CONFIGURATION ===
const CONFIG = {
name: "dss-worker",
version: "0.1.0",
defaultPort: 3456,
portRange: { min: 3456, max: 3466 },
dataDir: path.resolve(__dirname, "../../../.dss"),
adminUiPath: path.resolve(__dirname, "../../../admin-ui"),
};
// === PORT DETECTION ===
async function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
server.once("error", () => resolve(false));
server.once("listening", () => {
server.close();
resolve(true);
});
server.listen(port, "0.0.0.0");
});
}
async function findAvailablePort(): Promise<number> {
// Check if PORT env is set - use it directly (fail if unavailable)
if (process.env.PORT) {
const envPort = parseInt(process.env.PORT);
if (await isPortAvailable(envPort)) {
return envPort;
}
console.error(`[DSS] Port ${envPort} (from PORT env) is not available`);
process.exit(1);
}
// Auto-detect available port in range
for (let port = CONFIG.portRange.min; port <= CONFIG.portRange.max; port++) {
if (await isPortAvailable(port)) {
return port;
}
}
console.error(`[DSS] No available ports in range ${CONFIG.portRange.min}-${CONFIG.portRange.max}`);
process.exit(1);
}
// === DATA TYPES ===
interface Project {
id: string;
name: string;
figmaFileKey?: string;
status: "active" | "syncing" | "error" | "pending";
lastSync?: string;
tokens: Token[];
components: Component[];
styles: Style[];
createdAt: string;
updatedAt: string;
}
interface Token {
name: string;
value: string;
type: "color" | "spacing" | "typography" | "shadow" | "border" | "other";
category: string;
description?: string;
}
interface Component {
key: string;
name: string;
description?: string;
variants?: string[];
properties?: Record<string, unknown>;
}
interface Style {
key: string;
name: string;
type: "TEXT" | "FILL" | "EFFECT" | "GRID";
properties: Record<string, unknown>;
}
interface Team {
id: string;
name: string;
description?: string;
members: string[];
projects: string[];
createdAt: string;
}
interface Activity {
id: string;
type: string;
message: string;
status: "success" | "warning" | "error" | "info";
timestamp: string;
projectId?: string;
}
interface ExportBundle {
version: string;
exportedAt: string;
project: Project;
teams?: Team[];
}
// === DATA STORE ===
class DataStore {
private projects: Map<string, Project> = new Map();
private teams: Map<string, Team> = new Map();
private activities: Activity[] = [];
private dataFile: string;
constructor() {
this.dataFile = path.join(CONFIG.dataDir, "data.json");
this.loadFromDisk();
}
private loadFromDisk(): void {
try {
if (fs.existsSync(this.dataFile)) {
const data = fs.readJsonSync(this.dataFile);
if (data.projects) {
for (const p of data.projects) {
this.projects.set(p.id, p);
}
}
if (data.teams) {
for (const t of data.teams) {
this.teams.set(t.id, t);
}
}
if (data.activities) {
this.activities = data.activities.slice(0, 100);
}
}
} catch {
// Start fresh if data is corrupted
console.log("[DSS] Starting with empty data store");
}
}
private saveToDisk(): void {
try {
fs.ensureDirSync(CONFIG.dataDir);
fs.writeJsonSync(
this.dataFile,
{
projects: Array.from(this.projects.values()),
teams: Array.from(this.teams.values()),
activities: this.activities.slice(0, 100),
},
{ spaces: 2 }
);
} catch (e) {
console.error("[DSS] Failed to save data:", e);
}
}
// Projects
getProjects(): Project[] {
return Array.from(this.projects.values());
}
getProject(id: string): Project | undefined {
return this.projects.get(id);
}
createProject(data: Partial<Project>): Project {
const id = `proj-${Date.now()}`;
const project: Project = {
id,
name: data.name || "Untitled Project",
figmaFileKey: data.figmaFileKey,
status: "pending",
tokens: data.tokens || [],
components: data.components || [],
styles: data.styles || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.projects.set(id, project);
this.addActivity("create", `Created project: ${project.name}`, "success", id);
this.saveToDisk();
return project;
}
updateProject(id: string, data: Partial<Project>): Project | null {
const project = this.projects.get(id);
if (!project) return null;
const updated = { ...project, ...data, updatedAt: new Date().toISOString() };
this.projects.set(id, updated);
this.saveToDisk();
return updated;
}
deleteProject(id: string): boolean {
const project = this.projects.get(id);
if (!project) return false;
this.projects.delete(id);
this.addActivity("delete", `Deleted project: ${project.name}`, "info");
this.saveToDisk();
return true;
}
// Import/Export
exportProject(id: string): ExportBundle | null {
const project = this.projects.get(id);
if (!project) return null;
return {
version: CONFIG.version,
exportedAt: new Date().toISOString(),
project,
};
}
importProject(bundle: ExportBundle): Project {
const project = {
...bundle.project,
id: `proj-${Date.now()}`, // New ID to avoid conflicts
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.projects.set(project.id, project);
this.addActivity("import", `Imported project: ${project.name}`, "success", project.id);
this.saveToDisk();
return project;
}
// Teams
getTeams(): Team[] {
return Array.from(this.teams.values());
}
getTeam(id: string): Team | undefined {
return this.teams.get(id);
}
createTeam(data: Partial<Team>): Team {
const id = `team-${Date.now()}`;
const team: Team = {
id,
name: data.name || "New Team",
description: data.description,
members: data.members || [],
projects: data.projects || [],
createdAt: new Date().toISOString(),
};
this.teams.set(id, team);
this.saveToDisk();
return team;
}
// Activity
addActivity(type: string, message: string, status: Activity["status"], projectId?: string): void {
this.activities.unshift({
id: `act-${Date.now()}`,
type,
message,
status,
timestamp: new Date().toISOString(),
projectId,
});
if (this.activities.length > 100) {
this.activities = this.activities.slice(0, 100);
}
// Don't save on every activity - batch writes
}
getActivities(limit = 20): Activity[] {
return this.activities.slice(0, limit);
}
}
// === SERVER SETUP ===
const fastify = Fastify({
logger: {
level: process.env.LOG_LEVEL || "info",
},
});
const store = new DataStore();
// Static files
fastify.register(fastifyStatic, {
root: CONFIG.adminUiPath,
prefix: "/admin-ui/",
});
// CORS
fastify.addHook("onRequest", (request, reply, done) => {
reply.header("Access-Control-Allow-Origin", "*");
reply.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
reply.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (request.method === "OPTIONS") {
reply.status(204).send();
return;
}
done();
});
// === ROUTES ===
// Health
fastify.get("/health", async () => ({
status: "ok",
name: CONFIG.name,
version: CONFIG.version,
uptime: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
}));
// Root redirect
fastify.get("/", async (_, reply) => {
reply.redirect("/admin-ui/index.html");
});
fastify.get("/admin", async (_, reply) => {
reply.redirect("/admin-ui/index.html");
});
// === PROJECTS ===
fastify.get("/api/projects", async () => store.getProjects());
fastify.get<{ Params: { id: string } }>("/api/projects/:id", async (request, reply) => {
const project = store.getProject(request.params.id);
if (!project) return reply.status(404).send({ error: "Project not found" });
return project;
});
fastify.post("/api/projects", async (request) => {
return store.createProject(request.body as Partial<Project>);
});
fastify.put<{ Params: { id: string } }>("/api/projects/:id", async (request, reply) => {
const project = store.updateProject(request.params.id, request.body as Partial<Project>);
if (!project) return reply.status(404).send({ error: "Project not found" });
return project;
});
fastify.delete<{ Params: { id: string } }>("/api/projects/:id", async (request, reply) => {
if (!store.deleteProject(request.params.id)) {
return reply.status(404).send({ error: "Project not found" });
}
return { success: true };
});
// === IMPORT/EXPORT ===
fastify.get<{ Params: { id: string } }>("/api/projects/:id/export", async (request, reply) => {
const bundle = store.exportProject(request.params.id);
if (!bundle) return reply.status(404).send({ error: "Project not found" });
reply.header("Content-Type", "application/json");
reply.header("Content-Disposition", `attachment; filename="${bundle.project.name.replace(/\s+/g, "-")}.dss.json"`);
return bundle;
});
fastify.post("/api/projects/import", async (request, reply) => {
const bundle = request.body as ExportBundle;
if (!bundle.version || !bundle.project) {
return reply.status(400).send({ error: "Invalid export bundle" });
}
const project = store.importProject(bundle);
return { success: true, project };
});
// Export all projects (full backup)
fastify.get("/api/export", async (_, reply) => {
const projects = store.getProjects();
const teams = store.getTeams();
const backup = {
version: CONFIG.version,
exportedAt: new Date().toISOString(),
projects,
teams,
};
reply.header("Content-Type", "application/json");
reply.header("Content-Disposition", `attachment; filename="dss-backup-${Date.now()}.json"`);
return backup;
});
// === DISCOVERY ===
fastify.get("/api/discovery", async () => {
const projects = store.getProjects();
const totalTokens = projects.reduce((sum, p) => sum + (p.tokens?.length || 0), 0);
const totalComponents = projects.reduce((sum, p) => sum + (p.components?.length || 0), 0);
return {
project: {
types: ["design-system"],
frameworks: ["web-components", "css"],
},
files: {
total: projects.length,
tokens: totalTokens,
components: totalComponents,
},
health: {
score: projects.length > 0 ? 85 : 50,
grade: projects.length > 0 ? "B+" : "C",
},
};
});
fastify.get("/api/discovery/stats", async () => {
const projects = store.getProjects();
return {
projects: { total: projects.length, active: projects.filter((p) => p.status === "active").length },
tokens: { total: projects.reduce((sum, p) => sum + (p.tokens?.length || 0), 0) },
components: { total: projects.reduce((sum, p) => sum + (p.components?.length || 0), 0) },
};
});
fastify.get("/api/discovery/activity", async () => {
return { items: store.getActivities() };
});
// === TOKENS & COMPONENTS ===
fastify.get("/api/tokens", async () => {
const projects = store.getProjects();
return projects.flatMap((p) => p.tokens || []);
});
fastify.get("/api/components", async () => {
const projects = store.getProjects();
return projects.flatMap((p) => p.components || []);
});
// === TEAMS ===
fastify.get("/api/teams", async () => store.getTeams());
fastify.get<{ Params: { id: string } }>("/api/teams/:id", async (request, reply) => {
const team = store.getTeam(request.params.id);
if (!team) return reply.status(404).send({ error: "Team not found" });
return team;
});
fastify.post("/api/teams", async (request) => {
return store.createTeam(request.body as Partial<Team>);
});
// === FIGMA INTEGRATION ===
fastify.post("/api/figma/extract-variables", async (request) => {
const { fileKey, format = "css" } = request.body as { fileKey: string; format?: string };
const project = store.getProjects().find((p) => p.figmaFileKey === fileKey);
// Return existing tokens or mock data
const tokens: Token[] = project?.tokens?.length
? project.tokens
: [
{ name: "primary", value: "oklch(0.7 0.15 250)", type: "color", category: "colors" },
{ name: "secondary", value: "oklch(0.6 0.05 260)", type: "color", category: "colors" },
{ name: "background", value: "oklch(0.15 0.02 260)", type: "color", category: "colors" },
{ name: "foreground", value: "oklch(0.95 0.01 260)", type: "color", category: "colors" },
];
store.addActivity("extract", `Extracted ${tokens.length} tokens`, "success");
return { success: true, fileKey, format, tokens, count: tokens.length };
});
fastify.post("/api/figma/extract-components", async (request) => {
const { fileKey } = request.body as { fileKey: string };
const project = store.getProjects().find((p) => p.figmaFileKey === fileKey);
const components: Component[] = project?.components?.length
? project.components
: [
{ key: "btn-1", name: "Button", description: "Interactive button", variants: ["primary", "secondary"] },
{ key: "card-1", name: "Card", description: "Content container", variants: ["default", "bordered"] },
];
store.addActivity("extract", `Extracted ${components.length} components`, "success");
return { success: true, fileKey, components, count: components.length };
});
fastify.post("/api/figma/sync-tokens", async (request) => {
const { fileKey, targetPath } = request.body as { fileKey: string; targetPath: string };
const project = store.getProjects().find((p) => p.figmaFileKey === fileKey);
const tokenCount = project?.tokens?.length || 0;
store.addActivity("sync", `Synced ${tokenCount} tokens to ${targetPath}`, "success");
return { success: true, tokens_synced: tokenCount, target_path: targetPath };
});
fastify.post("/api/figma/visual-diff", async (request) => {
const { fileKey } = request.body as { fileKey: string };
// Deterministic response - no random
return {
success: true,
changes_detected: false,
summary: { total_components: 4, unchanged: 4, changed: 0 },
changes: [],
};
});
fastify.post("/api/figma/validate", async () => {
return {
success: true,
valid: true,
summary: { total: 4, valid: 4, errors: 0, warnings: 0 },
issues: [],
};
});
fastify.post("/api/figma/generate-code", async (request) => {
const { componentName, framework = "webcomponent" } = request.body as {
fileKey: string;
componentName: string;
framework?: string;
};
let code = "";
const nameLower = componentName.toLowerCase();
if (framework === "webcomponent") {
code = `class Ds${componentName} extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() { this.render(); }
render() {
this.shadowRoot.innerHTML = \`<style>:host { display: inline-flex; }</style><slot></slot>\`;
}
}
customElements.define('ds-${nameLower}', Ds${componentName});`;
} else if (framework === "react") {
code = `export const ${componentName} = ({ children, variant = 'default', ...props }) => (
<div className={\`ds-${nameLower} ds-${nameLower}--\${variant}\`} {...props}>{children}</div>
);`;
} else if (framework === "vue") {
code = `<template><div :class="['ds-${nameLower}', \`ds-${nameLower}--\${variant}\`]"><slot /></div></template>
<script setup>
defineProps({ variant: { type: String, default: 'default' } });
</script>`;
}
return { success: true, component: componentName, framework, code };
});
// Real Figma API
fastify.post("/ingest/figma", async (request, reply) => {
const { fileKey, token } = request.body as { fileKey: string; token?: string };
if (token) {
try {
const api = new Api({ personalAccessToken: token });
const file = await api.getFile(fileKey);
return { success: true, name: file.name, nodes: file.document.children.length };
} catch (e: unknown) {
const message = e instanceof Error ? e.message : "Unknown error";
return reply.status(500).send({ error: message });
}
}
return { success: true, name: "Mock Figma File", nodes: 0, mode: "mock" };
});
// Visual diff with ODiff
fastify.post("/visual-diff", async (request, reply) => {
const { baselinePath, currentPath, diffPath } = request.body as {
baselinePath: string;
currentPath: string;
diffPath: string;
};
try {
const result = await compare(baselinePath, currentPath, diffPath, {
threshold: 0.1,
failOnLayoutDiff: true,
});
if (result.match) {
return { match: true, reason: "exact-match", diffPercentage: 0 };
}
const reason = "reason" in result ? result.reason : "unknown";
const diffPercentage = "diffPercentage" in result ? result.diffPercentage : 0;
return { match: false, reason, diffPercentage };
} catch (e: unknown) {
const message = e instanceof Error ? e.message : "Unknown error";
return reply.status(500).send({ error: message });
}
});
// Activity
fastify.get("/api/activity", async (request) => {
const query = request.query as { limit?: string };
const limit = parseInt(query.limit || "20");
return store.getActivities(limit);
});
// === START ===
async function start() {
const port = await findAvailablePort();
try {
await fastify.listen({ port, host: "0.0.0.0" });
const pad = (s: string | number, len: number) => String(s).padEnd(len);
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ Design System Server (DSS) v${pad(CONFIG.version, 28)}
╠═══════════════════════════════════════════════════════════════╣
║ Server: http://localhost:${pad(port, 30)}
║ Admin UI: http://localhost:${port}/admin-ui/${pad("", 21)}
╠═══════════════════════════════════════════════════════════════╣
║ Endpoints: ║
║ GET /api/projects List projects ║
║ GET /api/projects/:id/export Export project (backup) ║
║ POST /api/projects/import Import project ║
║ GET /api/export Full backup ║
║ GET /api/tokens List all tokens ║
║ GET /api/components List all components ║
║ POST /api/figma/* Figma operations ║
╚═══════════════════════════════════════════════════════════════╝
`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}
start();