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:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user