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:
394
servers/visual-qa/src/cli.ts
Normal file
394
servers/visual-qa/src/cli.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Design System Server (DSS) - CLI Tool
|
||||
*
|
||||
* Commands:
|
||||
* dss export [projectId] [--output file.json] Export project for backup
|
||||
* dss import <file.json> Import project from backup
|
||||
* dss backup [--output dir] Full backup of all data
|
||||
* dss restore <backup.json> Restore from full backup
|
||||
* dss health Check server health
|
||||
*/
|
||||
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const CONFIG = {
|
||||
version: "0.1.0",
|
||||
dataDir: path.resolve(__dirname, "../../../.dss"),
|
||||
defaultBackupDir: path.resolve(__dirname, "../../../.dss/backups"),
|
||||
serverUrl: process.env.DSS_URL || "http://localhost:3456",
|
||||
};
|
||||
|
||||
// === UTILITIES ===
|
||||
|
||||
function timestamp(): string {
|
||||
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
}
|
||||
|
||||
function log(msg: string, type: "info" | "success" | "error" | "warn" = "info"): void {
|
||||
const colors = {
|
||||
info: "\x1b[36m",
|
||||
success: "\x1b[32m",
|
||||
error: "\x1b[31m",
|
||||
warn: "\x1b[33m",
|
||||
};
|
||||
const reset = "\x1b[0m";
|
||||
const prefix = {
|
||||
info: "ℹ",
|
||||
success: "✓",
|
||||
error: "✗",
|
||||
warn: "⚠",
|
||||
};
|
||||
console.log(`${colors[type]}${prefix[type]}${reset} ${msg}`);
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
Design System Server (DSS) CLI v${CONFIG.version}
|
||||
|
||||
Usage: dss <command> [options]
|
||||
|
||||
Commands:
|
||||
export [id] Export a project (or all if no id given)
|
||||
--output, -o <file> Output file path (default: project-name.dss.json)
|
||||
--stdout Output to stdout instead of file
|
||||
|
||||
import <file> Import a project from backup file
|
||||
--force, -f Overwrite if project exists
|
||||
|
||||
backup Create full backup of all projects
|
||||
--output, -o <dir> Output directory (default: .dss/backups/)
|
||||
|
||||
restore <file> Restore from a full backup
|
||||
--force, -f Overwrite existing data
|
||||
|
||||
health Check server health status
|
||||
--json Output as JSON
|
||||
|
||||
list List all projects
|
||||
|
||||
Examples:
|
||||
dss export my-project-id -o ./backup.json
|
||||
dss import ./backup.json
|
||||
dss backup
|
||||
dss restore ./dss-backup-2024.json
|
||||
dss health
|
||||
`);
|
||||
}
|
||||
|
||||
// === DATA ACCESS ===
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
figmaFileKey?: string;
|
||||
status: string;
|
||||
tokens: unknown[];
|
||||
components: unknown[];
|
||||
styles: unknown[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface DataFile {
|
||||
projects: Project[];
|
||||
teams: unknown[];
|
||||
activities: unknown[];
|
||||
}
|
||||
|
||||
function getDataPath(): string {
|
||||
return path.join(CONFIG.dataDir, "data.json");
|
||||
}
|
||||
|
||||
function loadData(): DataFile {
|
||||
const dataPath = getDataPath();
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
return { projects: [], teams: [], activities: [] };
|
||||
}
|
||||
return fs.readJsonSync(dataPath);
|
||||
}
|
||||
|
||||
function saveData(data: DataFile): void {
|
||||
fs.ensureDirSync(CONFIG.dataDir);
|
||||
fs.writeJsonSync(getDataPath(), data, { spaces: 2 });
|
||||
}
|
||||
|
||||
// === COMMANDS ===
|
||||
|
||||
async function cmdExport(args: string[]): Promise<void> {
|
||||
const outputIdx = args.findIndex((a) => a === "-o" || a === "--output");
|
||||
const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : undefined;
|
||||
const toStdout = args.includes("--stdout");
|
||||
|
||||
// Filter out -o and --output and their values to find projectId
|
||||
const filteredArgs = args.filter((a, i) => {
|
||||
if (a === "-o" || a === "--output") return false;
|
||||
if (i > 0 && (args[i - 1] === "-o" || args[i - 1] === "--output")) return false;
|
||||
if (a.startsWith("-")) return false;
|
||||
return true;
|
||||
});
|
||||
// Join remaining args to support project names with spaces
|
||||
const projectId = filteredArgs.length > 0 ? filteredArgs.join(" ") : undefined;
|
||||
|
||||
const data = loadData();
|
||||
|
||||
if (projectId) {
|
||||
// Export single project
|
||||
const project = data.projects.find((p) => p.id === projectId || p.name === projectId);
|
||||
if (!project) {
|
||||
log(`Project not found: ${projectId}`, "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bundle = {
|
||||
version: CONFIG.version,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project,
|
||||
};
|
||||
|
||||
if (toStdout) {
|
||||
console.log(JSON.stringify(bundle, null, 2));
|
||||
} else {
|
||||
const filename = outputFile || `${project.name.replace(/\s+/g, "-").toLowerCase()}.dss.json`;
|
||||
fs.writeJsonSync(filename, bundle, { spaces: 2 });
|
||||
log(`Exported to ${filename}`, "success");
|
||||
}
|
||||
} else {
|
||||
// Export all projects
|
||||
const bundle = {
|
||||
version: CONFIG.version,
|
||||
exportedAt: new Date().toISOString(),
|
||||
projects: data.projects,
|
||||
teams: data.teams,
|
||||
};
|
||||
|
||||
if (toStdout) {
|
||||
console.log(JSON.stringify(bundle, null, 2));
|
||||
} else {
|
||||
const filename = outputFile || `dss-export-${timestamp()}.json`;
|
||||
fs.writeJsonSync(filename, bundle, { spaces: 2 });
|
||||
log(`Exported ${data.projects.length} projects to ${filename}`, "success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdImport(args: string[]): Promise<void> {
|
||||
const file = args.find((a) => !a.startsWith("-"));
|
||||
const force = args.includes("-f") || args.includes("--force");
|
||||
|
||||
if (!file) {
|
||||
log("Please specify a file to import", "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
log(`File not found: ${file}`, "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bundle = fs.readJsonSync(file);
|
||||
const data = loadData();
|
||||
|
||||
if (bundle.project) {
|
||||
// Single project import
|
||||
const existing = data.projects.find((p) => p.name === bundle.project.name);
|
||||
if (existing && !force) {
|
||||
log(`Project "${bundle.project.name}" already exists. Use --force to overwrite.`, "warn");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
Object.assign(existing, bundle.project, { updatedAt: new Date().toISOString() });
|
||||
log(`Updated existing project: ${bundle.project.name}`, "success");
|
||||
} else {
|
||||
const newProject = {
|
||||
...bundle.project,
|
||||
id: `proj-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
data.projects.push(newProject);
|
||||
log(`Imported project: ${bundle.project.name}`, "success");
|
||||
}
|
||||
} else if (bundle.projects) {
|
||||
// Multi-project import
|
||||
let imported = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const project of bundle.projects) {
|
||||
const existing = data.projects.find((p) => p.name === project.name);
|
||||
if (existing) {
|
||||
if (force) {
|
||||
Object.assign(existing, project, { updatedAt: new Date().toISOString() });
|
||||
updated++;
|
||||
}
|
||||
} else {
|
||||
data.projects.push({
|
||||
...project,
|
||||
id: `proj-${Date.now()}-${imported}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
}
|
||||
|
||||
log(`Imported ${imported} new projects, updated ${updated}`, "success");
|
||||
}
|
||||
|
||||
saveData(data);
|
||||
}
|
||||
|
||||
async function cmdBackup(args: string[]): Promise<void> {
|
||||
const outputIdx = args.findIndex((a) => a === "-o" || a === "--output");
|
||||
const outputDir = outputIdx >= 0 ? args[outputIdx + 1] : CONFIG.defaultBackupDir;
|
||||
|
||||
fs.ensureDirSync(outputDir);
|
||||
|
||||
const data = loadData();
|
||||
const backup = {
|
||||
version: CONFIG.version,
|
||||
createdAt: new Date().toISOString(),
|
||||
data,
|
||||
};
|
||||
|
||||
const filename = path.join(outputDir, `dss-backup-${timestamp()}.json`);
|
||||
fs.writeJsonSync(filename, backup, { spaces: 2 });
|
||||
|
||||
log(`Full backup created: ${filename}`, "success");
|
||||
log(` Projects: ${data.projects.length}`, "info");
|
||||
log(` Teams: ${data.teams.length}`, "info");
|
||||
log(` Activities: ${data.activities.length}`, "info");
|
||||
}
|
||||
|
||||
async function cmdRestore(args: string[]): Promise<void> {
|
||||
const file = args.find((a) => !a.startsWith("-"));
|
||||
const force = args.includes("-f") || args.includes("--force");
|
||||
|
||||
if (!file) {
|
||||
log("Please specify a backup file to restore", "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(file)) {
|
||||
log(`File not found: ${file}`, "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const existingData = loadData();
|
||||
if (existingData.projects.length > 0 && !force) {
|
||||
log("Data already exists. Use --force to overwrite.", "warn");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const backup = fs.readJsonSync(file);
|
||||
|
||||
if (!backup.data && !backup.projects) {
|
||||
log("Invalid backup file format", "error");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const data = backup.data || { projects: backup.projects, teams: backup.teams || [], activities: [] };
|
||||
saveData(data);
|
||||
|
||||
log(`Restored from backup: ${file}`, "success");
|
||||
log(` Projects: ${data.projects.length}`, "info");
|
||||
log(` Teams: ${data.teams.length}`, "info");
|
||||
}
|
||||
|
||||
async function cmdHealth(args: string[]): Promise<void> {
|
||||
const asJson = args.includes("--json");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${CONFIG.serverUrl}/health`);
|
||||
const health = (await response.json()) as { status: string; version: string; uptime: number };
|
||||
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify(health, null, 2));
|
||||
} else {
|
||||
log(`Server Status: ${health.status}`, health.status === "ok" ? "success" : "error");
|
||||
log(`Version: ${health.version}`, "info");
|
||||
log(`Uptime: ${health.uptime}s`, "info");
|
||||
}
|
||||
} catch {
|
||||
if (asJson) {
|
||||
console.log(JSON.stringify({ status: "offline", error: "Could not connect to server" }));
|
||||
} else {
|
||||
log("Server is offline or unreachable", "error");
|
||||
log(`Tried: ${CONFIG.serverUrl}/health`, "info");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdList(): Promise<void> {
|
||||
const data = loadData();
|
||||
|
||||
if (data.projects.length === 0) {
|
||||
log("No projects found", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\nProjects:");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
for (const project of data.projects) {
|
||||
const tokens = project.tokens?.length || 0;
|
||||
const components = project.components?.length || 0;
|
||||
console.log(` ${project.id}`);
|
||||
console.log(` Name: ${project.name}`);
|
||||
console.log(` Status: ${project.status}`);
|
||||
console.log(` Tokens: ${tokens}, Components: ${components}`);
|
||||
console.log(` Updated: ${project.updatedAt}`);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
// === MAIN ===
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
const cmdArgs = args.slice(1);
|
||||
|
||||
if (!command || command === "help" || command === "--help" || command === "-h") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "export":
|
||||
await cmdExport(cmdArgs);
|
||||
break;
|
||||
case "import":
|
||||
await cmdImport(cmdArgs);
|
||||
break;
|
||||
case "backup":
|
||||
await cmdBackup(cmdArgs);
|
||||
break;
|
||||
case "restore":
|
||||
await cmdRestore(cmdArgs);
|
||||
break;
|
||||
case "health":
|
||||
await cmdHealth(cmdArgs);
|
||||
break;
|
||||
case "list":
|
||||
case "ls":
|
||||
await cmdList();
|
||||
break;
|
||||
default:
|
||||
log(`Unknown command: ${command}`, "error");
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(err.message, "error");
|
||||
process.exit(1);
|
||||
});
|
||||
672
servers/visual-qa/src/index.ts
Executable file
672
servers/visual-qa/src/index.ts
Executable file
@@ -0,0 +1,672 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user