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:
19
cli/.npmignore
Normal file
19
cli/.npmignore
Normal file
@@ -0,0 +1,19 @@
|
||||
# Source files
|
||||
src/
|
||||
|
||||
# TypeScript config
|
||||
tsconfig.json
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Python virtual environment (created during postinstall)
|
||||
python/venv/
|
||||
|
||||
# Cache files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Dev files
|
||||
*.log
|
||||
.DS_Store
|
||||
96
cli/README.md
Normal file
96
cli/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# @overbits/dss
|
||||
|
||||
Design System Server - UI Developer Companion
|
||||
|
||||
Extract design tokens and components from Figma, generate code, and sync with your codebase.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @overbits/dss
|
||||
# or
|
||||
npx @overbits/dss
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- Python 3.8+ (auto-configured during installation)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize DSS in your project
|
||||
dss init
|
||||
|
||||
# Configure Figma access
|
||||
dss config --set figmaToken=figd_your_token
|
||||
dss config --set figmaFileKey=your_file_key
|
||||
|
||||
# Start the server
|
||||
dss start
|
||||
|
||||
# Sync tokens from Figma
|
||||
dss sync --format css
|
||||
|
||||
# Extract components
|
||||
dss extract components
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `dss init`
|
||||
Initialize DSS in the current project. Creates `.dss/` directory with configuration.
|
||||
|
||||
### `dss start`
|
||||
Start the DSS server. Options:
|
||||
- `-p, --port <port>` - Port (default: 3456)
|
||||
- `-d, --dev` - Development mode with auto-reload
|
||||
- `--no-open` - Don't open browser
|
||||
|
||||
### `dss stop`
|
||||
Stop the running DSS server.
|
||||
|
||||
### `dss sync`
|
||||
Sync design tokens from Figma. Options:
|
||||
- `-f, --format <format>` - Output format: css, scss, json, ts (default: css)
|
||||
- `-o, --output <path>` - Output file path
|
||||
- `--file-key <key>` - Figma file key
|
||||
|
||||
### `dss extract <type>`
|
||||
Extract tokens, components, or styles from Figma. Types: tokens, components, styles, all
|
||||
|
||||
### `dss config`
|
||||
Manage configuration. Options:
|
||||
- `--set <key=value>` - Set a config value
|
||||
- `--get <key>` - Get a config value
|
||||
- `--list` - List all config values
|
||||
|
||||
### `dss status`
|
||||
Check DSS server and configuration status.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Global Config (stored in ~/.config/dss/)
|
||||
- `figmaToken` - Figma personal access token
|
||||
- `defaultPort` - Default server port
|
||||
- `defaultFormat` - Default token output format
|
||||
|
||||
### Project Config (stored in .dss/config.json)
|
||||
- `figmaFileKey` - Figma file key for this project
|
||||
- `port` - Server port
|
||||
- `outputDir` - Token output directory
|
||||
- `tokenFormat` - Token format (css, scss, json, ts)
|
||||
- `componentFramework` - Component framework (react, vue, svelte)
|
||||
|
||||
## Web Dashboard
|
||||
|
||||
Once the server is running, access the web dashboard at `http://localhost:3456`:
|
||||
|
||||
- **Projects** - Manage design system projects
|
||||
- **Tokens** - Browse and export design tokens
|
||||
- **Components** - View component gallery and generate code
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
624
cli/package-lock.json
generated
Normal file
624
cli/package-lock.json
generated
Normal file
@@ -0,0 +1,624 @@
|
||||
{
|
||||
"name": "@overbits/dss",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@overbits/dss",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"conf": "^12.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ora": "^8.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"dss": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/atomically": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz",
|
||||
"integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stubborn-fs": "^2.0.0",
|
||||
"when-exit": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"restore-cursor": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-spinners": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-12.0.0.tgz",
|
||||
"integrity": "sha512-fIWyWUXrJ45cHCIQX+Ck1hrZDIf/9DR0P0Zewn3uNht28hbt5OfGUq8rRWsxi96pZWPyBEd0eY9ama01JTaknA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"atomically": "^2.0.2",
|
||||
"debounce-fn": "^5.1.2",
|
||||
"dot-prop": "^8.0.2",
|
||||
"env-paths": "^3.0.0",
|
||||
"json-schema-typed": "^8.0.1",
|
||||
"semver": "^7.5.4",
|
||||
"uint8array-extras": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce-fn": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz",
|
||||
"integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz",
|
||||
"integrity": "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^3.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-interactive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"is-unicode-supported": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-function": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
|
||||
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
|
||||
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"cli-cursor": "^5.0.0",
|
||||
"cli-spinners": "^2.9.2",
|
||||
"is-interactive": "^2.0.0",
|
||||
"is-unicode-supported": "^2.0.0",
|
||||
"log-symbols": "^6.0.0",
|
||||
"stdin-discarder": "^0.2.2",
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"onetime": "^7.0.0",
|
||||
"signal-exit": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/stdin-discarder": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/stubborn-fs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
|
||||
"integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stubborn-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stubborn-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
|
||||
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8array-extras": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-0.3.0.tgz",
|
||||
"integrity": "sha512-erJsJwQ0tKdwuqI0359U8ijkFmfiTcq25JvvzRVc1VP+2son1NJRXhxcAKJmAW3ajM8JSGAfsAXye8g4s+znxA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/when-exit": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
|
||||
"integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
51
cli/package.json
Normal file
51
cli/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@overbits/dss",
|
||||
"version": "0.1.0",
|
||||
"description": "Design System Server - UI Developer Companion",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"dss": "dist/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/cli.js",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"design-system",
|
||||
"figma",
|
||||
"tokens",
|
||||
"components",
|
||||
"ui",
|
||||
"developer-tools"
|
||||
],
|
||||
"author": "Overbits",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/overbits/dss"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ora": "^8.0.1",
|
||||
"conf": "^12.0.0",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"typescript": "^5.3.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"python/api",
|
||||
"scripts",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
5
cli/python/api/requirements.txt
Normal file
5
cli/python/api/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi>=0.100.0
|
||||
uvicorn[standard]>=0.23.0
|
||||
httpx>=0.24.0
|
||||
python-dotenv>=1.0.0
|
||||
pydantic>=2.0.0
|
||||
724
cli/python/api/server.py
Normal file
724
cli/python/api/server.py
Normal file
@@ -0,0 +1,724 @@
|
||||
"""
|
||||
Design System Server (DSS) - FastAPI Server
|
||||
|
||||
Portable API server providing:
|
||||
- Project management (CRUD)
|
||||
- Figma integration endpoints
|
||||
- Discovery & health endpoints
|
||||
- Activity tracking
|
||||
- Runtime configuration management
|
||||
- Service discovery (Storybook, etc.)
|
||||
|
||||
Modes:
|
||||
- Server: Deployed remotely, serves design systems to teams
|
||||
- Local: Dev companion, UI advisor, local services
|
||||
|
||||
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 sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from config import config
|
||||
from storage.database import (
|
||||
Projects, Components, SyncHistory, ActivityLog, Teams, Cache, get_stats
|
||||
)
|
||||
from figma.figma_tools import FigmaToolSuite
|
||||
|
||||
|
||||
# === 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)
|
||||
self._data = self._load()
|
||||
|
||||
def _load(self) -> dict:
|
||||
if self.config_path.exists():
|
||||
try:
|
||||
return json.loads(self.config_path.read_text())
|
||||
except:
|
||||
pass
|
||||
return {
|
||||
"mode": "local", # "local" or "server"
|
||||
"figma": {"token": "", "configured": False},
|
||||
"services": {
|
||||
"storybook": {"enabled": False, "port": 6006, "url": ""},
|
||||
"chromatic": {"enabled": False, "project_token": ""},
|
||||
"github": {"enabled": False, "repo": ""},
|
||||
},
|
||||
"features": {
|
||||
"visual_qa": True,
|
||||
"token_sync": True,
|
||||
"code_gen": True,
|
||||
"ai_advisor": False,
|
||||
}
|
||||
}
|
||||
|
||||
def _save(self):
|
||||
self.config_path.write_text(json.dumps(self._data, indent=2))
|
||||
|
||||
def get(self, key: str = None):
|
||||
if key is None:
|
||||
# Return safe copy without secrets
|
||||
safe = self._data.copy()
|
||||
if safe.get("figma", {}).get("token"):
|
||||
safe["figma"]["token"] = "***configured***"
|
||||
return safe
|
||||
return self._data.get(key)
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
self._data[key] = value
|
||||
self._save()
|
||||
return self._data[key]
|
||||
|
||||
def update(self, updates: dict):
|
||||
for key, value in updates.items():
|
||||
if isinstance(value, dict) and isinstance(self._data.get(key), dict):
|
||||
self._data[key].update(value)
|
||||
else:
|
||||
self._data[key] = value
|
||||
self._save()
|
||||
return self.get()
|
||||
|
||||
def set_figma_token(self, token: str):
|
||||
self._data["figma"]["token"] = token
|
||||
self._data["figma"]["configured"] = bool(token)
|
||||
self._save()
|
||||
# Also update the global config
|
||||
os.environ["FIGMA_TOKEN"] = token
|
||||
return {"configured": bool(token)}
|
||||
|
||||
|
||||
runtime_config = RuntimeConfig()
|
||||
|
||||
|
||||
# === Service Discovery ===
|
||||
|
||||
class ServiceDiscovery:
|
||||
"""Discovers and manages companion services."""
|
||||
|
||||
KNOWN_SERVICES = {
|
||||
"storybook": {"ports": [6006, 6007], "health": "/"},
|
||||
"chromatic": {"ports": [], "health": None},
|
||||
"vite": {"ports": [5173, 5174, 3000], "health": "/"},
|
||||
"webpack": {"ports": [8080, 8081], "health": "/"},
|
||||
"nextjs": {"ports": [3000, 3001], "health": "/"},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def discover(cls) -> dict:
|
||||
"""Discover running services by checking known ports."""
|
||||
import socket
|
||||
|
||||
discovered = {}
|
||||
for service, info in cls.KNOWN_SERVICES.items():
|
||||
for port in info["ports"]:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(0.5)
|
||||
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}"
|
||||
}
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if service not in discovered:
|
||||
discovered[service] = {"running": False, "port": None, "url": None}
|
||||
|
||||
return discovered
|
||||
|
||||
@classmethod
|
||||
async def check_storybook(cls) -> dict:
|
||||
"""Check Storybook status specifically."""
|
||||
import httpx
|
||||
|
||||
configured = runtime_config.get("services").get("storybook", {})
|
||||
port = configured.get("port", 6006)
|
||||
url = configured.get("url") or f"http://localhost:{port}"
|
||||
|
||||
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
|
||||
}
|
||||
except:
|
||||
return {"running": False, "url": url, "port": port}
|
||||
|
||||
|
||||
# === App Setup ===
|
||||
|
||||
app = FastAPI(
|
||||
title="Design System Server (DSS)",
|
||||
description="API for design system management and Figma integration",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Mount Admin UI static files
|
||||
UI_DIR = Path(__file__).parent.parent.parent / "admin-ui"
|
||||
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"))
|
||||
|
||||
|
||||
# === 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 = ""
|
||||
|
||||
|
||||
# === Root & Health ===
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Redirect to Admin UI dashboard."""
|
||||
from fastapi.responses import RedirectResponse
|
||||
return RedirectResponse(url="/admin-ui/index.html")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint."""
|
||||
return {
|
||||
"status": "ok",
|
||||
"name": "dss-api",
|
||||
"version": "1.0.0",
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
"figma_mode": figma_suite.mode,
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# === 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."""
|
||||
project = Projects.get(project_id)
|
||||
if not project:
|
||||
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."""
|
||||
project_id = f"proj-{int(datetime.utcnow().timestamp() * 1000)}"
|
||||
created = Projects.create(
|
||||
id=project_id,
|
||||
name=project.name,
|
||||
description=project.description,
|
||||
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}
|
||||
)
|
||||
return created
|
||||
|
||||
@app.put("/api/projects/{project_id}")
|
||||
async def update_project(project_id: str, update: ProjectUpdate):
|
||||
"""Update a project."""
|
||||
existing = Projects.get(project_id)
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
update_data = {k: v for k, v in update.dict().items() if v is not None}
|
||||
if not update_data:
|
||||
return existing
|
||||
|
||||
updated = Projects.update(project_id, **update_data)
|
||||
ActivityLog.log(
|
||||
action="project_updated",
|
||||
entity_type="project",
|
||||
entity_id=project_id,
|
||||
project_id=project_id,
|
||||
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
|
||||
)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# === Components ===
|
||||
|
||||
@app.get("/api/projects/{project_id}/components")
|
||||
async def list_components(project_id: str):
|
||||
"""List components for a project."""
|
||||
if not Projects.get(project_id):
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
return Components.list(project_id)
|
||||
|
||||
|
||||
# === Figma Integration ===
|
||||
|
||||
@app.post("/api/figma/extract-variables")
|
||||
async def extract_variables(request: FigmaExtractRequest, background_tasks: BackgroundTasks):
|
||||
"""Extract design tokens from Figma file."""
|
||||
try:
|
||||
result = await figma_suite.extract_variables(request.file_key, request.format)
|
||||
ActivityLog.log(
|
||||
action="figma_extract_variables",
|
||||
entity_type="figma",
|
||||
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."""
|
||||
try:
|
||||
result = await figma_suite.extract_components(request.file_key)
|
||||
ActivityLog.log(
|
||||
action="figma_extract_components",
|
||||
entity_type="figma",
|
||||
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."""
|
||||
try:
|
||||
result = await figma_suite.extract_styles(request.file_key)
|
||||
return result
|
||||
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)
|
||||
ActivityLog.log(
|
||||
action="figma_sync_tokens",
|
||||
entity_type="figma",
|
||||
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."""
|
||||
try:
|
||||
result = await figma_suite.validate_components(request.file_key)
|
||||
return result
|
||||
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."""
|
||||
try:
|
||||
result = await figma_suite.generate_code(file_key, component_name, framework)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# === Discovery ===
|
||||
|
||||
@app.get("/api/discovery")
|
||||
async def run_discovery(path: str = "."):
|
||||
"""Run project discovery."""
|
||||
script_path = Path(__file__).parent.parent / "discovery" / "discover.sh"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(script_path), path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return json.loads(result.stdout)
|
||||
else:
|
||||
return {"error": result.stderr}
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=504, detail="Discovery timed out")
|
||||
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
|
||||
)
|
||||
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."""
|
||||
script_path = Path(__file__).parent.parent / "discovery" / "discover-env.sh"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(script_path), 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))
|
||||
|
||||
|
||||
# === 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)):
|
||||
"""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."""
|
||||
team_id = f"team-{int(datetime.utcnow().timestamp() * 1000)}"
|
||||
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."""
|
||||
team = Teams.get(team_id)
|
||||
if not team:
|
||||
raise HTTPException(status_code=404, detail="Team not found")
|
||||
return team
|
||||
|
||||
|
||||
# === 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."""
|
||||
Cache.clear_all()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# === Configuration Management ===
|
||||
|
||||
class ConfigUpdate(BaseModel):
|
||||
mode: Optional[str] = None
|
||||
figma_token: Optional[str] = None
|
||||
services: Optional[Dict[str, Any]] = None
|
||||
features: Optional[Dict[str, bool]] = None
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
async def get_config():
|
||||
"""Get current runtime configuration (secrets masked)."""
|
||||
return {
|
||||
"config": runtime_config.get(),
|
||||
"env": config.summary(),
|
||||
"mode": runtime_config.get("mode")
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/config")
|
||||
async def update_config(update: ConfigUpdate):
|
||||
"""Update runtime configuration."""
|
||||
updates = {}
|
||||
|
||||
if update.mode:
|
||||
updates["mode"] = update.mode
|
||||
|
||||
if update.figma_token is not None:
|
||||
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"))
|
||||
ActivityLog.log(
|
||||
action="figma_token_updated",
|
||||
entity_type="config",
|
||||
details={"configured": bool(update.figma_token)}
|
||||
)
|
||||
|
||||
if update.services:
|
||||
updates["services"] = update.services
|
||||
|
||||
if update.features:
|
||||
updates["features"] = update.features
|
||||
|
||||
if updates:
|
||||
runtime_config.update(updates)
|
||||
ActivityLog.log(
|
||||
action="config_updated",
|
||||
entity_type="config",
|
||||
details={"keys": list(updates.keys())}
|
||||
)
|
||||
|
||||
return runtime_config.get()
|
||||
|
||||
|
||||
@app.get("/api/config/figma")
|
||||
async def get_figma_config():
|
||||
"""Get Figma configuration status."""
|
||||
figma_cfg = runtime_config.get("figma")
|
||||
return {
|
||||
"configured": figma_cfg.get("configured", False),
|
||||
"mode": figma_suite.mode,
|
||||
"features": {
|
||||
"extract_variables": True,
|
||||
"extract_components": True,
|
||||
"extract_styles": True,
|
||||
"sync_tokens": True,
|
||||
"validate": True,
|
||||
"generate_code": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/config/figma/test")
|
||||
async def test_figma_connection():
|
||||
"""Test Figma API connection."""
|
||||
try:
|
||||
# Try to make a simple API call
|
||||
if not runtime_config.get("figma").get("configured"):
|
||||
return {"success": False, "error": "Figma token not configured"}
|
||||
|
||||
# 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}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
user = resp.json()
|
||||
return {
|
||||
"success": True,
|
||||
"user": user.get("email", "connected"),
|
||||
"handle": user.get("handle")
|
||||
}
|
||||
else:
|
||||
return {"success": False, "error": f"API returned {resp.status_code}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# === Service Discovery ===
|
||||
|
||||
@app.get("/api/services")
|
||||
async def list_services():
|
||||
"""List configured and discovered services."""
|
||||
configured = runtime_config.get("services")
|
||||
discovered = await ServiceDiscovery.discover()
|
||||
|
||||
return {
|
||||
"configured": configured,
|
||||
"discovered": discovered,
|
||||
"storybook": await ServiceDiscovery.check_storybook()
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/services/{service_name}")
|
||||
async def configure_service(service_name: str, config_data: Dict[str, Any]):
|
||||
"""Configure a service."""
|
||||
services = runtime_config.get("services") or {}
|
||||
services[service_name] = {**services.get(service_name, {}), **config_data}
|
||||
runtime_config.set("services", services)
|
||||
|
||||
ActivityLog.log(
|
||||
action="service_configured",
|
||||
entity_type="service",
|
||||
entity_id=service_name,
|
||||
details={"keys": list(config_data.keys())}
|
||||
)
|
||||
|
||||
return services[service_name]
|
||||
|
||||
|
||||
@app.get("/api/services/storybook")
|
||||
async def get_storybook_status():
|
||||
"""Get Storybook service status."""
|
||||
return await ServiceDiscovery.check_storybook()
|
||||
|
||||
|
||||
# === DSS Mode ===
|
||||
|
||||
@app.get("/api/mode")
|
||||
async def get_mode():
|
||||
"""Get current DSS mode."""
|
||||
mode = runtime_config.get("mode")
|
||||
return {
|
||||
"mode": mode,
|
||||
"description": "Local dev companion" if mode == "local" else "Remote design system server",
|
||||
"features": runtime_config.get("features")
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/mode")
|
||||
async def set_mode(mode: str):
|
||||
"""Set DSS mode (local or server)."""
|
||||
if mode not in ["local", "server"]:
|
||||
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}
|
||||
)
|
||||
|
||||
return {"mode": mode, "success": True}
|
||||
|
||||
|
||||
# === Run Server ===
|
||||
|
||||
# === Static Files (Admin UI) ===
|
||||
# Mount at the end so API routes take precedence
|
||||
# This enables portable mode: ./dss start serves everything on one port
|
||||
|
||||
UI_DIR = Path(__file__).parent.parent.parent / "admin-ui"
|
||||
if UI_DIR.exists():
|
||||
app.mount("/", StaticFiles(directory=str(UI_DIR), html=True), name="ui")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
port = int(os.getenv("PORT", "3456"))
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
|
||||
url = f"http://{host}:{port}"
|
||||
print(f"""
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ Design System Server (DSS) - Portable Server ║
|
||||
╠═══════════════════════════════════════════════════════════════╣
|
||||
║ Dashboard: {url + '/':^47}║
|
||||
║ API: {url + '/api':^47}║
|
||||
║ Docs: {url + '/docs':^47}║
|
||||
║ Environment: {config.server.env:^47}║
|
||||
║ Figma Mode: {figma_suite.mode:^47}║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
uvicorn.run(
|
||||
"server:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=config.server.env == "development"
|
||||
)
|
||||
74
cli/scripts/postinstall.js
Normal file
74
cli/scripts/postinstall.js
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* DSS Postinstall Script
|
||||
*
|
||||
* Sets up Python virtual environment and installs dependencies
|
||||
* after npm install.
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const pythonDir = join(__dirname, '..', 'python');
|
||||
const venvDir = join(pythonDir, 'venv');
|
||||
const apiDir = join(pythonDir, 'api');
|
||||
|
||||
console.log('[DSS] Setting up Python environment...');
|
||||
|
||||
// Check if Python 3 is available
|
||||
function getPythonCmd() {
|
||||
const commands = ['python3', 'python'];
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
execSync(`${cmd} --version`, { stdio: 'ignore' });
|
||||
return cmd;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const python = getPythonCmd();
|
||||
|
||||
if (!python) {
|
||||
console.error('[DSS] Error: Python 3 is required but not found.');
|
||||
console.error('[DSS] Please install Python 3.8+ and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`[DSS] Using ${python}`);
|
||||
|
||||
// Create virtual environment if needed
|
||||
if (!existsSync(venvDir)) {
|
||||
console.log('[DSS] Creating virtual environment...');
|
||||
try {
|
||||
execSync(`${python} -m venv ${venvDir}`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error('[DSS] Failed to create virtual environment');
|
||||
console.error('[DSS] Try: pip install virtualenv');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Install requirements
|
||||
const pip = join(venvDir, 'bin', 'pip');
|
||||
const requirements = join(apiDir, 'requirements.txt');
|
||||
|
||||
if (existsSync(requirements)) {
|
||||
console.log('[DSS] Installing Python dependencies...');
|
||||
try {
|
||||
execSync(`${pip} install -q -r ${requirements}`, { stdio: 'inherit' });
|
||||
console.log('[DSS] Python environment ready!');
|
||||
} catch (error) {
|
||||
console.error('[DSS] Failed to install Python dependencies');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log('[DSS] No requirements.txt found, skipping dependency install');
|
||||
}
|
||||
|
||||
console.log('[DSS] Setup complete!');
|
||||
40
cli/scripts/publish.sh
Executable file
40
cli/scripts/publish.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# DSS npm publish script
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Publishing @overbits/dss to npm..."
|
||||
|
||||
# Ensure we're in the cli directory
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Build
|
||||
echo "📦 Building..."
|
||||
npm run build
|
||||
|
||||
# Check if logged in to npm
|
||||
if ! npm whoami &> /dev/null; then
|
||||
echo "❌ Not logged in to npm. Run: npm login"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify package
|
||||
echo "📋 Package contents:"
|
||||
npm pack --dry-run
|
||||
|
||||
# Confirm
|
||||
read -p "Publish to npm? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Publish
|
||||
npm publish --access public
|
||||
|
||||
echo "✅ Published @overbits/dss!"
|
||||
echo ""
|
||||
echo "Users can now install with:"
|
||||
echo " npm install -g @overbits/dss"
|
||||
echo " npx @overbits/dss"
|
||||
103
cli/src/cli.ts
Normal file
103
cli/src/cli.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 🧬 DSS CLI - Design System Server Organism Controller
|
||||
*
|
||||
* A portable companion for UI developers - think of it as the organism's
|
||||
* command-line nervous system. Through these commands, you can:
|
||||
*
|
||||
* - 🧬 Awaken a new organism (init)
|
||||
* - 💚 Check the organism's vital signs (status)
|
||||
* - 🩸 Direct the sensory organs to perceive Figma (extract)
|
||||
* - 🔄 Circulate extracted nutrients (sync)
|
||||
* - ⚙️ Adjust the organism's behavior (config)
|
||||
* - 🧠 Birth a conscious instance (start)
|
||||
*
|
||||
* Framework: DSS Organism Framework
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { initCommand } from './commands/init.js';
|
||||
import { startCommand } from './commands/start.js';
|
||||
import { syncCommand } from './commands/sync.js';
|
||||
import { extractCommand } from './commands/extract.js';
|
||||
import { configCommand } from './commands/config.js';
|
||||
import { statusCommand } from './commands/status.js';
|
||||
import { stopCommand } from './commands/stop.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('dss')
|
||||
.description('🧬 Design System Server - Organism Controller for UI Developers')
|
||||
.version('0.1.0');
|
||||
|
||||
// Init command - setup DSS in a project
|
||||
program
|
||||
.command('init')
|
||||
.description('🧬 ORGANISM GENESIS - Create a new design system organism in your project')
|
||||
.option('-f, --figma-key <key>', 'Link to Figma genetic blueprint')
|
||||
.option('-t, --figma-token <token>', 'Figma sensory organ connection token')
|
||||
.action(initCommand);
|
||||
|
||||
// Start command - start the DSS server
|
||||
program
|
||||
.command('start')
|
||||
.description('💚 ORGANISM AWAKENING - Bring the design system organism to life')
|
||||
.option('-p, --port <port>', 'Neural pathway port', '3456')
|
||||
.option('-d, --dev', 'Live consciousness mode with hot-reload')
|
||||
.option('--no-open', 'Do not open sensory interface')
|
||||
.action(startCommand);
|
||||
|
||||
// Sync command - sync tokens from Figma
|
||||
program
|
||||
.command('sync')
|
||||
.description('🩸 NUTRIENT CIRCULATION - Distribute extracted tokens through the codebase')
|
||||
.option('-f, --format <format>', 'Nutrient format: css, scss, json, ts', 'css')
|
||||
.option('-o, --output <path>', 'Circulation destination')
|
||||
.option('--file-key <key>', 'Figma file key (overrides config)')
|
||||
.action(syncCommand);
|
||||
|
||||
// Extract command - extract components or tokens
|
||||
program
|
||||
.command('extract <type>')
|
||||
.description('👁️ SENSORY PERCEPTION - Direct organism eyes to perceive Figma designs')
|
||||
.option('-f, --format <format>', 'Perception output format', 'json')
|
||||
.option('-o, --output <path>', 'Memory storage location')
|
||||
.option('--file-key <key>', 'Figma file key')
|
||||
.action(extractCommand);
|
||||
|
||||
// Config command - manage configuration
|
||||
program
|
||||
.command('config')
|
||||
.description('⚙️ ENDOCRINE ADJUSTMENT - Configure organism behavior and preferences')
|
||||
.option('--set <key=value>', 'Set organism hormone value')
|
||||
.option('--get <key>', 'Read organism hormone value')
|
||||
.option('--list', 'View all hormones')
|
||||
.action(configCommand);
|
||||
|
||||
// Stop command - stop the server
|
||||
program
|
||||
.command('stop')
|
||||
.description('😴 ORGANISM REST - Put the design system organism into sleep mode')
|
||||
.action(stopCommand);
|
||||
|
||||
// Status command - check DSS status
|
||||
program
|
||||
.command('status')
|
||||
.description('🏥 VITAL SIGNS CHECK - Monitor organism health and configuration')
|
||||
.action(statusCommand);
|
||||
|
||||
// Parse arguments
|
||||
program.parse();
|
||||
|
||||
// Show help if no command provided
|
||||
if (!process.argv.slice(2).length) {
|
||||
console.log(chalk.blue(`
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ ${chalk.bold('DSS')} - Design System Server ║
|
||||
║ UI Developer Companion ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
`));
|
||||
program.outputHelp();
|
||||
}
|
||||
138
cli/src/commands/config.ts
Normal file
138
cli/src/commands/config.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* DSS Config Command
|
||||
*
|
||||
* Manage DSS configuration.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
getConfig,
|
||||
loadProjectConfig,
|
||||
saveProjectConfig,
|
||||
setGlobalConfig,
|
||||
getGlobalConfig,
|
||||
listGlobalConfig,
|
||||
hasProjectConfig,
|
||||
} from '../lib/config.js';
|
||||
|
||||
interface ConfigOptions {
|
||||
set?: string;
|
||||
get?: string;
|
||||
list?: boolean;
|
||||
}
|
||||
|
||||
export async function configCommand(options: ConfigOptions): Promise<void> {
|
||||
if (options.set) {
|
||||
await setConfig(options.set);
|
||||
} else if (options.get) {
|
||||
await getConfigValue(options.get);
|
||||
} else if (options.list) {
|
||||
await listConfig();
|
||||
} else {
|
||||
await listConfig();
|
||||
}
|
||||
}
|
||||
|
||||
async function setConfig(keyValue: string): Promise<void> {
|
||||
const [key, ...valueParts] = keyValue.split('=');
|
||||
const value = valueParts.join('=');
|
||||
|
||||
if (!key || value === undefined) {
|
||||
console.log(chalk.red(' Invalid format. Use: --set key=value'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Determine if this is a global or project config
|
||||
const globalKeys = ['figmaToken', 'defaultPort', 'defaultFormat'];
|
||||
const projectKeys = ['figmaFileKey', 'port', 'outputDir', 'tokenFormat', 'componentFramework'];
|
||||
|
||||
if (globalKeys.includes(key)) {
|
||||
// Global config
|
||||
const parsedValue = parseValue(value);
|
||||
setGlobalConfig(key, parsedValue as string | number);
|
||||
console.log(chalk.green(` Set global config: ${key}`));
|
||||
console.log(chalk.dim(` Value: ${key === 'figmaToken' ? '***hidden***' : value}`));
|
||||
} else if (projectKeys.includes(key)) {
|
||||
// Project config
|
||||
if (!hasProjectConfig()) {
|
||||
console.log(chalk.yellow(' No project config found. Run: dss init'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadProjectConfig();
|
||||
(config as Record<string, unknown>)[key] = parseValue(value);
|
||||
saveProjectConfig(config);
|
||||
|
||||
console.log(chalk.green(` Set project config: ${key}`));
|
||||
console.log(chalk.dim(` Value: ${value}`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Unknown config key: ${key}`));
|
||||
console.log('');
|
||||
console.log(chalk.dim(' Global keys: figmaToken, defaultPort, defaultFormat'));
|
||||
console.log(chalk.dim(' Project keys: figmaFileKey, port, outputDir, tokenFormat, componentFramework'));
|
||||
}
|
||||
}
|
||||
|
||||
async function getConfigValue(key: string): Promise<void> {
|
||||
const config = getConfig();
|
||||
const value = (config as Record<string, unknown>)[key];
|
||||
|
||||
if (value === undefined) {
|
||||
// Try global config
|
||||
const globalValue = getGlobalConfig(key);
|
||||
if (globalValue !== undefined) {
|
||||
console.log(chalk.dim(` ${key} (global):`), key === 'figmaToken' ? '***hidden***' : String(globalValue));
|
||||
} else {
|
||||
console.log(chalk.yellow(` Config key not found: ${key}`));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.dim(` ${key}:`), key === 'figmaToken' ? '***hidden***' : String(value));
|
||||
}
|
||||
}
|
||||
|
||||
async function listConfig(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(chalk.blue(' DSS Configuration'));
|
||||
console.log(chalk.dim(' ─────────────────'));
|
||||
console.log('');
|
||||
|
||||
// Project config
|
||||
if (hasProjectConfig()) {
|
||||
console.log(chalk.green(' Project Config:'));
|
||||
const projectConfig = loadProjectConfig();
|
||||
Object.entries(projectConfig).forEach(([key, value]) => {
|
||||
console.log(chalk.dim(` ${key}:`), String(value));
|
||||
});
|
||||
} else {
|
||||
console.log(chalk.yellow(' No project config (run: dss init)'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Global config
|
||||
console.log(chalk.green(' Global Config:'));
|
||||
const globalConf = listGlobalConfig();
|
||||
Object.entries(globalConf).forEach(([key, value]) => {
|
||||
const displayValue = key === 'figmaToken' ? '***hidden***' : String(value);
|
||||
console.log(chalk.dim(` ${key}:`), displayValue);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
// Merged config
|
||||
console.log(chalk.green(' Effective Config:'));
|
||||
const merged = getConfig();
|
||||
Object.entries(merged).forEach(([key, value]) => {
|
||||
const displayValue = key === 'figmaToken' ? (value ? '***configured***' : 'not set') : String(value);
|
||||
console.log(chalk.dim(` ${key}:`), displayValue);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function parseValue(value: string): string | number | boolean {
|
||||
if (value === 'true') return true;
|
||||
if (value === 'false') return false;
|
||||
if (!isNaN(Number(value))) return Number(value);
|
||||
return value;
|
||||
}
|
||||
146
cli/src/commands/extract.ts
Normal file
146
cli/src/commands/extract.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* DSS Extract Command
|
||||
*
|
||||
* Extract tokens or components from Figma.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { getConfig, getProjectRoot } from '../lib/config.js';
|
||||
import { getApiClient } from '../lib/api.js';
|
||||
import { isServerRunning } from '../lib/server.js';
|
||||
|
||||
interface ExtractOptions {
|
||||
format: string;
|
||||
output?: string;
|
||||
fileKey?: string;
|
||||
}
|
||||
|
||||
export async function extractCommand(
|
||||
type: string,
|
||||
options: ExtractOptions
|
||||
): Promise<void> {
|
||||
const validTypes = ['tokens', 'components', 'styles', 'all'];
|
||||
if (!validTypes.includes(type)) {
|
||||
console.log(chalk.red(` Invalid type: ${type}`));
|
||||
console.log(chalk.dim(` Valid types: ${validTypes.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = getConfig();
|
||||
const cwd = getProjectRoot();
|
||||
|
||||
// Check if server is running
|
||||
if (!isServerRunning(cwd)) {
|
||||
console.log(chalk.yellow(' DSS server is not running'));
|
||||
console.log(chalk.dim(' Start it with: dss start'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileKey = options.fileKey || config.figmaFileKey;
|
||||
if (!fileKey) {
|
||||
console.log(chalk.red(' No Figma file key configured'));
|
||||
console.log(chalk.dim(' Set it with: dss config --set figmaFileKey=YOUR_KEY'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = options.output || join(cwd, '.dss', 'output');
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
const api = getApiClient({ port: config.port });
|
||||
|
||||
if (type === 'tokens' || type === 'all') {
|
||||
await extractTokens(api, fileKey, options.format, outputDir);
|
||||
}
|
||||
|
||||
if (type === 'components' || type === 'all') {
|
||||
await extractComponents(api, fileKey, outputDir);
|
||||
}
|
||||
|
||||
if (type === 'styles' || type === 'all') {
|
||||
await extractStyles(api, fileKey, outputDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractTokens(
|
||||
api: ReturnType<typeof getApiClient>,
|
||||
fileKey: string,
|
||||
format: string,
|
||||
outputDir: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Extracting tokens...').start();
|
||||
|
||||
try {
|
||||
const result = await api.extractTokens(fileKey, format || 'json');
|
||||
|
||||
const filename = format === 'css' ? 'tokens.css' :
|
||||
format === 'scss' ? '_tokens.scss' :
|
||||
format === 'ts' ? 'tokens.ts' : 'tokens.json';
|
||||
|
||||
const outputPath = join(outputDir, filename);
|
||||
writeFileSync(outputPath, result.formatted_output);
|
||||
|
||||
spinner.succeed(`Extracted ${result.tokens_count} tokens`);
|
||||
console.log(chalk.dim(` Output: ${outputPath}`));
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to extract tokens');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function extractComponents(
|
||||
api: ReturnType<typeof getApiClient>,
|
||||
fileKey: string,
|
||||
outputDir: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Extracting components...').start();
|
||||
|
||||
try {
|
||||
const result = await api.extractComponents(fileKey);
|
||||
|
||||
const outputPath = join(outputDir, 'components.json');
|
||||
writeFileSync(outputPath, JSON.stringify(result.components, null, 2));
|
||||
|
||||
spinner.succeed(`Extracted ${result.components_count} components`);
|
||||
console.log(chalk.dim(` Output: ${outputPath}`));
|
||||
|
||||
// Show component summary
|
||||
console.log('');
|
||||
result.components.forEach(comp => {
|
||||
console.log(chalk.dim(` - ${comp.name}`));
|
||||
if (comp.variants?.length) {
|
||||
console.log(chalk.dim(` Variants: ${comp.variants.join(', ')}`));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to extract components');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStyles(
|
||||
api: ReturnType<typeof getApiClient>,
|
||||
fileKey: string,
|
||||
outputDir: string
|
||||
): Promise<void> {
|
||||
const spinner = ora('Extracting styles...').start();
|
||||
|
||||
try {
|
||||
// Note: This would need a corresponding API endpoint
|
||||
// For now, we'll extract tokens which include style information
|
||||
const result = await api.extractTokens(fileKey, 'json');
|
||||
|
||||
const outputPath = join(outputDir, 'styles.json');
|
||||
writeFileSync(outputPath, JSON.stringify(result.tokens, null, 2));
|
||||
|
||||
spinner.succeed(`Extracted styles`);
|
||||
console.log(chalk.dim(` Output: ${outputPath}`));
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to extract styles');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
}
|
||||
}
|
||||
107
cli/src/commands/init.ts
Normal file
107
cli/src/commands/init.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* DSS Init Command
|
||||
*
|
||||
* Initialize DSS in the current project.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
getProjectRoot,
|
||||
hasProjectConfig,
|
||||
saveProjectConfig,
|
||||
setGlobalConfig,
|
||||
type DSSConfig,
|
||||
} from '../lib/config.js';
|
||||
|
||||
interface InitOptions {
|
||||
figmaKey?: string;
|
||||
figmaToken?: string;
|
||||
}
|
||||
|
||||
export async function initCommand(options: InitOptions): Promise<void> {
|
||||
const spinner = ora('Initializing DSS...').start();
|
||||
|
||||
try {
|
||||
const projectRoot = getProjectRoot();
|
||||
const dssDir = join(projectRoot, '.dss');
|
||||
|
||||
// Check if already initialized
|
||||
if (hasProjectConfig()) {
|
||||
spinner.warn('DSS is already initialized in this project');
|
||||
console.log(chalk.dim(` Config: ${join(dssDir, 'config.json')}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create .dss directory
|
||||
if (!existsSync(dssDir)) {
|
||||
mkdirSync(dssDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
const outputDir = join(dssDir, 'output');
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Build config
|
||||
const config: DSSConfig = {
|
||||
port: 3456,
|
||||
outputDir: '.dss/output',
|
||||
tokenFormat: 'css',
|
||||
componentFramework: 'react',
|
||||
};
|
||||
|
||||
if (options.figmaKey) {
|
||||
config.figmaFileKey = options.figmaKey;
|
||||
}
|
||||
|
||||
// Save Figma token globally (not in project config for security)
|
||||
if (options.figmaToken) {
|
||||
setGlobalConfig('figmaToken', options.figmaToken);
|
||||
spinner.info('Figma token saved to global config');
|
||||
}
|
||||
|
||||
// Save project config
|
||||
saveProjectConfig(config);
|
||||
|
||||
spinner.succeed('DSS initialized successfully!');
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.green(' Created:'));
|
||||
console.log(chalk.dim(` .dss/config.json`));
|
||||
console.log(chalk.dim(` .dss/output/`));
|
||||
console.log('');
|
||||
|
||||
// Next steps
|
||||
console.log(chalk.blue(' Next steps:'));
|
||||
if (!options.figmaToken) {
|
||||
console.log(chalk.dim(' 1. Set your Figma token:'));
|
||||
console.log(chalk.white(' dss config --set figmaToken=figd_xxxxx'));
|
||||
}
|
||||
if (!options.figmaKey) {
|
||||
console.log(chalk.dim(` ${options.figmaToken ? '1' : '2'}. Set your Figma file key:`));
|
||||
console.log(chalk.white(' dss config --set figmaFileKey=abc123'));
|
||||
}
|
||||
console.log(chalk.dim(` ${options.figmaToken && options.figmaKey ? '1' : '3'}. Start the server:`));
|
||||
console.log(chalk.white(' dss start'));
|
||||
console.log('');
|
||||
|
||||
// Add to .gitignore if exists
|
||||
const gitignorePath = join(projectRoot, '.gitignore');
|
||||
if (existsSync(gitignorePath)) {
|
||||
const { readFileSync, appendFileSync } = await import('fs');
|
||||
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
||||
if (!gitignore.includes('.dss/')) {
|
||||
appendFileSync(gitignorePath, '\n# DSS\n.dss/\n');
|
||||
console.log(chalk.dim(' Added .dss/ to .gitignore'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to initialize DSS');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
109
cli/src/commands/start.ts
Normal file
109
cli/src/commands/start.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* DSS Start Command
|
||||
*
|
||||
* Start the DSS server.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { exec } from 'child_process';
|
||||
import { getConfig, getProjectRoot, hasProjectConfig } from '../lib/config.js';
|
||||
import {
|
||||
startServer,
|
||||
isServerRunning,
|
||||
getServerPid,
|
||||
waitForServer,
|
||||
stopServer,
|
||||
} from '../lib/server.js';
|
||||
|
||||
interface StartOptions {
|
||||
port: string;
|
||||
dev: boolean;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export async function startCommand(options: StartOptions): Promise<void> {
|
||||
const port = parseInt(options.port, 10);
|
||||
const cwd = getProjectRoot();
|
||||
const config = getConfig();
|
||||
|
||||
// Check if already running
|
||||
if (isServerRunning(cwd)) {
|
||||
const pid = getServerPid(cwd);
|
||||
console.log(chalk.yellow(` DSS is already running (PID: ${pid})`));
|
||||
console.log(chalk.dim(` Dashboard: http://localhost:${port}`));
|
||||
console.log('');
|
||||
console.log(chalk.dim(' To restart: dss stop && dss start'));
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora('Starting DSS server...').start();
|
||||
|
||||
try {
|
||||
// Start server
|
||||
const serverProcess = await startServer({
|
||||
port,
|
||||
dev: options.dev,
|
||||
cwd,
|
||||
});
|
||||
|
||||
if (options.dev) {
|
||||
spinner.succeed('DSS running in development mode');
|
||||
console.log(chalk.dim(' Press Ctrl+C to stop'));
|
||||
console.log('');
|
||||
|
||||
// In dev mode, we're attached to the process
|
||||
serverProcess.on('exit', (code) => {
|
||||
console.log(chalk.dim(`\n Server exited with code ${code}`));
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
// Handle Ctrl+C
|
||||
process.on('SIGINT', () => {
|
||||
console.log(chalk.dim('\n Stopping server...'));
|
||||
serverProcess.kill('SIGTERM');
|
||||
});
|
||||
} else {
|
||||
// Wait for server to be ready
|
||||
spinner.text = 'Waiting for server to be ready...';
|
||||
const ready = await waitForServer(port, 15000);
|
||||
|
||||
if (!ready) {
|
||||
spinner.fail('Server failed to start');
|
||||
console.error(chalk.red(' Check logs: .dss/dss.log'));
|
||||
await stopServer(cwd);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.succeed(`DSS started (PID: ${serverProcess.pid})`);
|
||||
console.log('');
|
||||
console.log(chalk.green(' Dashboard:'), chalk.blue(`http://localhost:${port}`));
|
||||
console.log(chalk.green(' API: '), chalk.blue(`http://localhost:${port}/api`));
|
||||
console.log(chalk.green(' Docs: '), chalk.blue(`http://localhost:${port}/docs`));
|
||||
console.log('');
|
||||
console.log(chalk.dim(' Logs: .dss/dss.log'));
|
||||
console.log(chalk.dim(' Stop: dss stop'));
|
||||
console.log('');
|
||||
|
||||
// Show Figma status
|
||||
if (config.figmaFileKey) {
|
||||
console.log(chalk.dim(` Figma file: ${config.figmaFileKey}`));
|
||||
} else {
|
||||
console.log(chalk.yellow(' No Figma file configured'));
|
||||
console.log(chalk.dim(' Run: dss config --set figmaFileKey=YOUR_KEY'));
|
||||
}
|
||||
|
||||
// Open browser if requested
|
||||
if (options.open) {
|
||||
const url = `http://localhost:${port}`;
|
||||
const openCmd = process.platform === 'darwin' ? 'open' :
|
||||
process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
exec(`${openCmd} ${url}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to start DSS');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
96
cli/src/commands/status.ts
Normal file
96
cli/src/commands/status.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 🏥 DSS Status Command - Organism Vital Signs
|
||||
*
|
||||
* Check DSS design system organism's vital signs, consciousness state,
|
||||
* and sensory organ configuration.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { getConfig, getProjectRoot, hasProjectConfig } from '../lib/config.js';
|
||||
import { isServerRunning, getServerPid } from '../lib/server.js';
|
||||
import { getApiClient } from '../lib/api.js';
|
||||
|
||||
export async function statusCommand(): Promise<void> {
|
||||
const cwd = getProjectRoot();
|
||||
const config = getConfig();
|
||||
|
||||
console.log('');
|
||||
console.log(chalk.cyan(' 🏥 ORGANISM VITAL SIGNS'));
|
||||
console.log(chalk.dim(' ────────────────────────'));
|
||||
console.log('');
|
||||
|
||||
// Organism status
|
||||
if (hasProjectConfig()) {
|
||||
console.log(chalk.green(' 🧬 Organism:'), chalk.dim('Born and conscious'));
|
||||
console.log(chalk.dim(` Home: ${cwd}`));
|
||||
} else {
|
||||
console.log(chalk.yellow(' 🧬 Organism:'), chalk.dim('Not yet born'));
|
||||
console.log(chalk.dim(' Genesis: dss init'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Consciousness status (server)
|
||||
const running = isServerRunning(cwd);
|
||||
const pid = getServerPid(cwd);
|
||||
const port = config.port || 3456;
|
||||
|
||||
if (running) {
|
||||
console.log(chalk.green(' 💚 Consciousness:'), chalk.dim(`Awake (PID: ${pid})`));
|
||||
console.log(chalk.dim(` Neural port: http://localhost:${port}`));
|
||||
|
||||
// Try to get health info
|
||||
try {
|
||||
const api = getApiClient({ port });
|
||||
const health = await api.health();
|
||||
console.log(chalk.dim(` Awareness: ${health.figma_mode}`));
|
||||
} catch {
|
||||
console.log(chalk.yellow(' ⚠️ Unable to read consciousness'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow(' 💚 Consciousness:'), chalk.dim('Sleeping'));
|
||||
console.log(chalk.dim(' Awaken: dss start'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Sensory organs (Figma)
|
||||
if (config.figmaToken) {
|
||||
console.log(chalk.green(' 👁️ Sensory Eyes:'), chalk.dim('Configured'));
|
||||
|
||||
// Test connection if server is running
|
||||
if (running) {
|
||||
try {
|
||||
const api = getApiClient({ port });
|
||||
const test = await api.testFigmaConnection();
|
||||
if (test.success) {
|
||||
console.log(chalk.green(' Perception:'), chalk.dim(`Clear (${test.user})`));
|
||||
} else {
|
||||
console.log(chalk.red(' Perception:'), chalk.dim(test.error || 'Blinded'));
|
||||
}
|
||||
} catch {
|
||||
console.log(chalk.yellow(' Perception:'), chalk.dim('Cannot test'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow(' 👁️ Sensory Eyes:'), chalk.dim('Not configured'));
|
||||
console.log(chalk.dim(' Configure: dss config --set figmaToken=figd_xxxxx'));
|
||||
}
|
||||
|
||||
if (config.figmaFileKey) {
|
||||
console.log(chalk.green(' 📋 Genetic Blueprint:'), chalk.dim(config.figmaFileKey));
|
||||
} else {
|
||||
console.log(chalk.yellow(' Figma File:'), chalk.dim('Not configured'));
|
||||
console.log(chalk.dim(' Set: dss config --set figmaFileKey=abc123'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Output config
|
||||
console.log(chalk.dim(' Output:'));
|
||||
console.log(chalk.dim(` Format: ${config.tokenFormat || 'css'}`));
|
||||
console.log(chalk.dim(` Framework: ${config.componentFramework || 'react'}`));
|
||||
console.log(chalk.dim(` Directory: ${config.outputDir || '.dss/output'}`));
|
||||
|
||||
console.log('');
|
||||
}
|
||||
25
cli/src/commands/stop.ts
Normal file
25
cli/src/commands/stop.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* DSS Stop Command
|
||||
*
|
||||
* Stop the DSS server.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { getProjectRoot } from '../lib/config.js';
|
||||
import { isServerRunning, stopServer, getServerPid } from '../lib/server.js';
|
||||
|
||||
export async function stopCommand(): Promise<void> {
|
||||
const cwd = getProjectRoot();
|
||||
|
||||
if (!isServerRunning(cwd)) {
|
||||
console.log(chalk.yellow(' DSS is not running'));
|
||||
return;
|
||||
}
|
||||
|
||||
const pid = getServerPid(cwd);
|
||||
console.log(chalk.dim(` Stopping DSS (PID: ${pid})...`));
|
||||
|
||||
await stopServer(cwd);
|
||||
|
||||
console.log(chalk.green(' DSS stopped'));
|
||||
}
|
||||
102
cli/src/commands/sync.ts
Normal file
102
cli/src/commands/sync.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* DSS Sync Command
|
||||
*
|
||||
* Sync design tokens from Figma to local files.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { getConfig, getProjectRoot } from '../lib/config.js';
|
||||
import { getApiClient } from '../lib/api.js';
|
||||
import { isServerRunning } from '../lib/server.js';
|
||||
|
||||
interface SyncOptions {
|
||||
format: string;
|
||||
output?: string;
|
||||
fileKey?: string;
|
||||
}
|
||||
|
||||
export async function syncCommand(options: SyncOptions): Promise<void> {
|
||||
const config = getConfig();
|
||||
const cwd = getProjectRoot();
|
||||
|
||||
// Check if server is running
|
||||
if (!isServerRunning(cwd)) {
|
||||
console.log(chalk.yellow(' DSS server is not running'));
|
||||
console.log(chalk.dim(' Start it with: dss start'));
|
||||
console.log('');
|
||||
console.log(chalk.dim(' Or sync directly via API if running remotely'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileKey = options.fileKey || config.figmaFileKey;
|
||||
if (!fileKey) {
|
||||
console.log(chalk.red(' No Figma file key configured'));
|
||||
console.log(chalk.dim(' Set it with: dss config --set figmaFileKey=YOUR_KEY'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const format = options.format || config.tokenFormat || 'css';
|
||||
const outputPath = options.output || getDefaultOutputPath(format, cwd);
|
||||
|
||||
const spinner = ora(`Syncing tokens from Figma (${format})...`).start();
|
||||
|
||||
try {
|
||||
const api = getApiClient({ port: config.port });
|
||||
|
||||
// Extract tokens
|
||||
spinner.text = 'Extracting tokens from Figma...';
|
||||
const result = await api.extractTokens(fileKey, format);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Token extraction failed');
|
||||
}
|
||||
|
||||
// Write output file
|
||||
spinner.text = `Writing ${result.tokens_count} tokens to ${outputPath}...`;
|
||||
|
||||
const dir = dirname(outputPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, result.formatted_output);
|
||||
|
||||
spinner.succeed(`Synced ${result.tokens_count} tokens`);
|
||||
console.log('');
|
||||
console.log(chalk.green(' Output:'), chalk.dim(outputPath));
|
||||
console.log('');
|
||||
|
||||
// Show token summary
|
||||
const categories = result.tokens.reduce((acc, t) => {
|
||||
acc[t.category] = (acc[t.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
console.log(chalk.dim(' Token breakdown:'));
|
||||
Object.entries(categories).forEach(([cat, count]) => {
|
||||
console.log(chalk.dim(` ${cat}: ${count}`));
|
||||
});
|
||||
console.log('');
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('Sync failed');
|
||||
console.error(chalk.red(` ${(error as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultOutputPath(format: string, cwd: string): string {
|
||||
const extensions: Record<string, string> = {
|
||||
css: 'tokens.css',
|
||||
scss: '_tokens.scss',
|
||||
json: 'tokens.json',
|
||||
ts: 'tokens.ts',
|
||||
js: 'tokens.js',
|
||||
};
|
||||
|
||||
const filename = extensions[format] || 'tokens.css';
|
||||
return join(cwd, '.dss', 'output', filename);
|
||||
}
|
||||
15
cli/src/index.ts
Normal file
15
cli/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* DSS - Design System Server
|
||||
*
|
||||
* Programmatic API for DSS.
|
||||
*/
|
||||
|
||||
export { DSSApiClient, getApiClient, type ApiOptions } from './lib/api.js';
|
||||
export { getConfig, getProjectRoot, hasProjectConfig, type DSSConfig } from './lib/config.js';
|
||||
export {
|
||||
startServer,
|
||||
stopServer,
|
||||
isServerRunning,
|
||||
getServerPid,
|
||||
waitForServer,
|
||||
} from './lib/server.js';
|
||||
144
cli/src/lib/api.ts
Normal file
144
cli/src/lib/api.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* DSS API Client
|
||||
*
|
||||
* Communicates with the DSS server.
|
||||
*/
|
||||
|
||||
import { getConfig } from './config.js';
|
||||
|
||||
export interface ApiOptions {
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export class DSSApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(options: ApiOptions = {}) {
|
||||
const config = getConfig();
|
||||
const port = options.port || config.port || 3456;
|
||||
this.baseUrl = options.baseUrl || `http://localhost:${port}/api`;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ message: response.statusText })) as { message?: string };
|
||||
throw new Error(errorData.message || `Request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) as T : {} as T;
|
||||
}
|
||||
|
||||
async health(): Promise<{ status: string; figma_mode: string }> {
|
||||
// Health endpoint is at root, not under /api
|
||||
const url = this.baseUrl.replace('/api', '') + '/health';
|
||||
const response = await fetch(url);
|
||||
return response.json() as Promise<{ status: string; figma_mode: string }>;
|
||||
}
|
||||
|
||||
async getConfig(): Promise<Record<string, unknown>> {
|
||||
return this.request('/config');
|
||||
}
|
||||
|
||||
async setFigmaToken(token: string): Promise<void> {
|
||||
await this.request('/config', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ figma_token: token }),
|
||||
});
|
||||
}
|
||||
|
||||
async testFigmaConnection(): Promise<{ success: boolean; user?: string; error?: string }> {
|
||||
return this.request('/config/figma/test', { method: 'POST' });
|
||||
}
|
||||
|
||||
async extractTokens(fileKey: string, format: string = 'css'): Promise<{
|
||||
success: boolean;
|
||||
tokens_count: number;
|
||||
tokens: Array<{ name: string; value: string; type: string; category: string }>;
|
||||
formatted_output: string;
|
||||
output_path: string;
|
||||
}> {
|
||||
return this.request('/figma/extract-variables', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_key: fileKey, format }),
|
||||
});
|
||||
}
|
||||
|
||||
async extractComponents(fileKey: string): Promise<{
|
||||
success: boolean;
|
||||
components_count: number;
|
||||
components: Array<{ name: string; key: string; description: string; variants: string[] }>;
|
||||
output_path: string;
|
||||
}> {
|
||||
return this.request('/figma/extract-components', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_key: fileKey }),
|
||||
});
|
||||
}
|
||||
|
||||
async syncTokens(fileKey: string, targetPath: string, format: string = 'css'): Promise<{
|
||||
success: boolean;
|
||||
tokens_synced: number;
|
||||
output_file: string;
|
||||
}> {
|
||||
return this.request('/figma/sync-tokens', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_key: fileKey, target_path: targetPath, format }),
|
||||
});
|
||||
}
|
||||
|
||||
async generateCode(fileKey: string, componentName: string, framework: string = 'react'): Promise<{
|
||||
success: boolean;
|
||||
component: string;
|
||||
framework: string;
|
||||
code: string;
|
||||
}> {
|
||||
return this.request('/figma/generate-code', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_key: fileKey, component_name: componentName, framework }),
|
||||
});
|
||||
}
|
||||
|
||||
async getProjects(): Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
figma_file_key: string;
|
||||
status: string;
|
||||
}>> {
|
||||
return this.request('/projects');
|
||||
}
|
||||
|
||||
async createProject(data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
figma_file_key?: string;
|
||||
}): Promise<{ id: string; name: string }> {
|
||||
return this.request('/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let apiClient: DSSApiClient | null = null;
|
||||
|
||||
export function getApiClient(options?: ApiOptions): DSSApiClient {
|
||||
if (!apiClient || options) {
|
||||
apiClient = new DSSApiClient(options);
|
||||
}
|
||||
return apiClient;
|
||||
}
|
||||
95
cli/src/lib/config.ts
Normal file
95
cli/src/lib/config.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* DSS Configuration Manager
|
||||
*
|
||||
* Manages local and project configuration.
|
||||
*/
|
||||
|
||||
import Conf from 'conf';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Global user config (stored in home directory)
|
||||
const globalConfig = new Conf({
|
||||
projectName: 'dss',
|
||||
schema: {
|
||||
figmaToken: { type: 'string' },
|
||||
defaultPort: { type: 'number', default: 3456 },
|
||||
defaultFormat: { type: 'string', default: 'css' },
|
||||
},
|
||||
});
|
||||
|
||||
// Project-level config file
|
||||
const PROJECT_CONFIG_FILE = '.dss/config.json';
|
||||
|
||||
export interface DSSConfig {
|
||||
figmaFileKey?: string;
|
||||
figmaToken?: string;
|
||||
port?: number;
|
||||
outputDir?: string;
|
||||
tokenFormat?: 'css' | 'scss' | 'json' | 'ts';
|
||||
componentFramework?: 'react' | 'vue' | 'svelte' | 'webcomponent';
|
||||
}
|
||||
|
||||
export function getProjectRoot(): string {
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
export function getProjectConfigPath(): string {
|
||||
return join(getProjectRoot(), PROJECT_CONFIG_FILE);
|
||||
}
|
||||
|
||||
export function hasProjectConfig(): boolean {
|
||||
return existsSync(getProjectConfigPath());
|
||||
}
|
||||
|
||||
export function loadProjectConfig(): DSSConfig {
|
||||
const configPath = getProjectConfigPath();
|
||||
if (!existsSync(configPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse project config:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function saveProjectConfig(config: DSSConfig): void {
|
||||
const configPath = getProjectConfigPath();
|
||||
const dir = join(getProjectRoot(), '.dss');
|
||||
|
||||
// Ensure .dss directory exists
|
||||
if (!existsSync(dir)) {
|
||||
const { mkdirSync } = require('fs');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
export function getConfig(): DSSConfig {
|
||||
const project = loadProjectConfig();
|
||||
return {
|
||||
figmaToken: project.figmaToken || globalConfig.get('figmaToken') as string | undefined,
|
||||
figmaFileKey: project.figmaFileKey,
|
||||
port: project.port || globalConfig.get('defaultPort') as number,
|
||||
outputDir: project.outputDir || '.dss/output',
|
||||
tokenFormat: project.tokenFormat || (globalConfig.get('defaultFormat') as DSSConfig['tokenFormat']),
|
||||
componentFramework: project.componentFramework || 'react',
|
||||
};
|
||||
}
|
||||
|
||||
export function setGlobalConfig(key: string, value: string | number): void {
|
||||
globalConfig.set(key, value);
|
||||
}
|
||||
|
||||
export function getGlobalConfig(key: string): unknown {
|
||||
return globalConfig.get(key);
|
||||
}
|
||||
|
||||
export function listGlobalConfig(): Record<string, unknown> {
|
||||
return globalConfig.store;
|
||||
}
|
||||
197
cli/src/lib/server.ts
Normal file
197
cli/src/lib/server.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* DSS Server Manager
|
||||
*
|
||||
* Manages the Python server subprocess.
|
||||
*/
|
||||
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
interface ServerOptions {
|
||||
port: number;
|
||||
dev: boolean;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
|
||||
export function getPythonPath(): string {
|
||||
// Check for bundled Python venv in npm package (dist/lib -> ../../python/venv)
|
||||
const bundledPython = join(__dirname, '../../python/venv/bin/python3');
|
||||
if (existsSync(bundledPython)) {
|
||||
return bundledPython;
|
||||
}
|
||||
|
||||
// Development path (src/lib -> ../../python/venv)
|
||||
const devPython = join(__dirname, '../../../python/venv/bin/python3');
|
||||
if (existsSync(devPython)) {
|
||||
return devPython;
|
||||
}
|
||||
|
||||
return 'python3';
|
||||
}
|
||||
|
||||
export function getServerPath(): string {
|
||||
// Check for bundled server or use from package
|
||||
const bundledServer = join(__dirname, '../../python/api/server.py');
|
||||
if (existsSync(bundledServer)) {
|
||||
return dirname(bundledServer);
|
||||
}
|
||||
|
||||
// Fall back to relative path from CLI (cli/dist/lib -> ../../../../tools/api)
|
||||
const devServer = join(__dirname, '../../../../tools/api');
|
||||
if (existsSync(join(devServer, 'server.py'))) {
|
||||
return devServer;
|
||||
}
|
||||
|
||||
// Also check development src path (cli/src/lib -> ../../../../tools/api)
|
||||
const srcServer = join(__dirname, '../../../tools/api');
|
||||
if (existsSync(join(srcServer, 'server.py'))) {
|
||||
return srcServer;
|
||||
}
|
||||
|
||||
throw new Error('Could not find DSS server. Run from the design-system-swarm directory or install the full package.');
|
||||
}
|
||||
|
||||
export function getPidFile(cwd: string): string {
|
||||
return join(cwd, '.dss', 'dss.pid');
|
||||
}
|
||||
|
||||
export function getLogFile(cwd: string): string {
|
||||
return join(cwd, '.dss', 'dss.log');
|
||||
}
|
||||
|
||||
export function isServerRunning(cwd: string): boolean {
|
||||
const pidFile = getPidFile(cwd);
|
||||
if (!existsSync(pidFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
||||
// Check if process exists
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
// Process doesn't exist, clean up stale PID file
|
||||
try {
|
||||
unlinkSync(pidFile);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerPid(cwd: string): number | null {
|
||||
const pidFile = getPidFile(cwd);
|
||||
if (!existsSync(pidFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function startServer(options: ServerOptions): Promise<ChildProcess> {
|
||||
const { port, dev, cwd } = options;
|
||||
|
||||
if (isServerRunning(cwd)) {
|
||||
throw new Error(`Server is already running (PID: ${getServerPid(cwd)})`);
|
||||
}
|
||||
|
||||
const pythonPath = getPythonPath();
|
||||
const serverDir = getServerPath();
|
||||
|
||||
// Build command args
|
||||
const args = [
|
||||
'-m', 'uvicorn',
|
||||
'server:app',
|
||||
'--host', '0.0.0.0',
|
||||
'--port', String(port),
|
||||
];
|
||||
|
||||
if (dev) {
|
||||
args.push('--reload');
|
||||
}
|
||||
|
||||
// Set environment
|
||||
const env = {
|
||||
...process.env,
|
||||
PYTHONPATH: join(serverDir, '..'),
|
||||
PORT: String(port),
|
||||
};
|
||||
|
||||
// Spawn server process
|
||||
serverProcess = spawn(pythonPath, args, {
|
||||
cwd: serverDir,
|
||||
env,
|
||||
stdio: dev ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
||||
detached: !dev,
|
||||
});
|
||||
|
||||
if (!dev && serverProcess.pid) {
|
||||
// Write PID file
|
||||
const dssDir = join(cwd, '.dss');
|
||||
if (!existsSync(dssDir)) {
|
||||
const { mkdirSync } = await import('fs');
|
||||
mkdirSync(dssDir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(getPidFile(cwd), String(serverProcess.pid));
|
||||
|
||||
// Write logs
|
||||
const logStream = await import('fs').then(fs =>
|
||||
fs.createWriteStream(getLogFile(cwd), { flags: 'a' })
|
||||
);
|
||||
|
||||
serverProcess.stdout?.pipe(logStream);
|
||||
serverProcess.stderr?.pipe(logStream);
|
||||
|
||||
// Detach from parent
|
||||
serverProcess.unref();
|
||||
}
|
||||
|
||||
return serverProcess;
|
||||
}
|
||||
|
||||
export async function stopServer(cwd: string): Promise<boolean> {
|
||||
const pid = getServerPid(cwd);
|
||||
if (!pid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
unlinkSync(getPidFile(cwd));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForServer(port: number, timeout = 10000): Promise<boolean> {
|
||||
const start = Date.now();
|
||||
const url = `http://localhost:${port}/health`;
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Server not ready yet
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
20
cli/tsconfig.json
Normal file
20
cli/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user