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
673 lines
20 KiB
TypeScript
Executable File
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();
|