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
512 lines
12 KiB
JavaScript
512 lines
12 KiB
JavaScript
/**
|
|
* DSS Workflow State Machines
|
|
*
|
|
* State machine implementation for orchestrating multi-step workflows
|
|
* with transition guards, side effects, and progress tracking.
|
|
*
|
|
* @module workflows
|
|
*/
|
|
|
|
import { notifySuccess, notifyError, notifyInfo, ErrorCode } from './messaging.js';
|
|
import router from './router.js';
|
|
|
|
/**
|
|
* Base State Machine class
|
|
*/
|
|
class StateMachine {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.currentState = config.initial;
|
|
this.previousState = null;
|
|
this.context = {};
|
|
this.history = [];
|
|
this.listeners = new Map();
|
|
}
|
|
|
|
/**
|
|
* Get current state definition
|
|
*/
|
|
getCurrentStateDefinition() {
|
|
return this.config.states[this.currentState];
|
|
}
|
|
|
|
/**
|
|
* Check if transition is allowed
|
|
* @param {string} event - Event to transition on
|
|
* @returns {string|null} Next state or null if not allowed
|
|
*/
|
|
canTransition(event) {
|
|
const stateDefinition = this.getCurrentStateDefinition();
|
|
|
|
if (!stateDefinition || !stateDefinition.on) {
|
|
return null;
|
|
}
|
|
|
|
return stateDefinition.on[event] || null;
|
|
}
|
|
|
|
/**
|
|
* Send an event to trigger state transition
|
|
* @param {string} event - Event name
|
|
* @param {Object} [data] - Event data
|
|
* @returns {Promise<boolean>} Whether transition succeeded
|
|
*/
|
|
async send(event, data = {}) {
|
|
const nextState = this.canTransition(event);
|
|
|
|
if (!nextState) {
|
|
console.warn(`No transition for event "${event}" in state "${this.currentState}"`);
|
|
return false;
|
|
}
|
|
|
|
// Call exit actions for current state
|
|
await this.callActions(this.getCurrentStateDefinition().exit, { event, data });
|
|
|
|
// Store previous state
|
|
this.previousState = this.currentState;
|
|
|
|
// Transition to next state
|
|
this.currentState = nextState;
|
|
|
|
// Record history
|
|
this.history.push({
|
|
from: this.previousState,
|
|
to: this.currentState,
|
|
event,
|
|
timestamp: new Date().toISOString(),
|
|
data,
|
|
});
|
|
|
|
// Call entry actions for new state
|
|
await this.callActions(this.getCurrentStateDefinition().entry, { event, data });
|
|
|
|
// Emit state change
|
|
this.emit('stateChange', {
|
|
previous: this.previousState,
|
|
current: this.currentState,
|
|
event,
|
|
data,
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Call state actions
|
|
* @param {string[]|undefined} actions - Action names to execute
|
|
* @param {Object} context - Action context
|
|
*/
|
|
async callActions(actions, context) {
|
|
if (!actions || !Array.isArray(actions)) {
|
|
return;
|
|
}
|
|
|
|
for (const actionName of actions) {
|
|
const action = this.config.actions?.[actionName];
|
|
|
|
if (action) {
|
|
try {
|
|
await action.call(this, { ...context, machine: this });
|
|
} catch (error) {
|
|
console.error(`Action "${actionName}" failed:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if machine is in a specific state
|
|
* @param {string} state - State name
|
|
* @returns {boolean} Whether machine is in that state
|
|
*/
|
|
isIn(state) {
|
|
return this.currentState === state;
|
|
}
|
|
|
|
/**
|
|
* Check if machine is in final state
|
|
* @returns {boolean} Whether machine is in final state
|
|
*/
|
|
isFinal() {
|
|
const stateDefinition = this.getCurrentStateDefinition();
|
|
return stateDefinition?.type === 'final' || false;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to machine events
|
|
* @param {string} event - Event name
|
|
* @param {Function} handler - Event handler
|
|
* @returns {Function} Unsubscribe function
|
|
*/
|
|
on(event, handler) {
|
|
if (!this.listeners.has(event)) {
|
|
this.listeners.set(event, []);
|
|
}
|
|
|
|
this.listeners.get(event).push(handler);
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
const handlers = this.listeners.get(event);
|
|
const index = handlers.indexOf(handler);
|
|
if (index > -1) {
|
|
handlers.splice(index, 1);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Emit event to listeners
|
|
* @param {string} event - Event name
|
|
* @param {*} data - Event data
|
|
*/
|
|
emit(event, data) {
|
|
const handlers = this.listeners.get(event) || [];
|
|
handlers.forEach(handler => {
|
|
try {
|
|
handler(data);
|
|
} catch (error) {
|
|
console.error(`Event handler error for "${event}":`, error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get workflow progress
|
|
* @returns {Object} Progress information
|
|
*/
|
|
getProgress() {
|
|
const states = Object.keys(this.config.states);
|
|
const currentIndex = states.indexOf(this.currentState);
|
|
const total = states.length;
|
|
|
|
return {
|
|
current: currentIndex + 1,
|
|
total,
|
|
percentage: Math.round(((currentIndex + 1) / total) * 100),
|
|
state: this.currentState,
|
|
isComplete: this.isFinal(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset machine to initial state
|
|
*/
|
|
reset() {
|
|
this.currentState = this.config.initial;
|
|
this.previousState = null;
|
|
this.context = {};
|
|
this.history = [];
|
|
this.emit('reset', {});
|
|
}
|
|
|
|
/**
|
|
* Get state history
|
|
* @returns {Array} State transition history
|
|
*/
|
|
getHistory() {
|
|
return [...this.history];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create Project Workflow
|
|
*
|
|
* Guides user through: Create Project → Configure Settings → Extract Tokens → Success
|
|
*/
|
|
export class CreateProjectWorkflow extends StateMachine {
|
|
constructor(options = {}) {
|
|
const config = {
|
|
initial: 'init',
|
|
states: {
|
|
init: {
|
|
on: {
|
|
CREATE_PROJECT: 'creating',
|
|
},
|
|
entry: ['showCreateForm'],
|
|
},
|
|
creating: {
|
|
on: {
|
|
PROJECT_CREATED: 'created',
|
|
CREATE_FAILED: 'init',
|
|
},
|
|
entry: ['createProject'],
|
|
},
|
|
created: {
|
|
on: {
|
|
CONFIGURE_SETTINGS: 'configuring',
|
|
SKIP_CONFIG: 'ready',
|
|
},
|
|
entry: ['showSuccessMessage', 'promptConfiguration'],
|
|
},
|
|
configuring: {
|
|
on: {
|
|
SETTINGS_SAVED: 'ready',
|
|
CONFIG_CANCELLED: 'created',
|
|
},
|
|
entry: ['navigateToSettings'],
|
|
},
|
|
ready: {
|
|
on: {
|
|
EXTRACT_TOKENS: 'extracting',
|
|
RECONFIGURE: 'configuring',
|
|
},
|
|
entry: ['showReadyMessage'],
|
|
},
|
|
extracting: {
|
|
on: {
|
|
EXTRACTION_SUCCESS: 'complete',
|
|
EXTRACTION_FAILED: 'ready',
|
|
},
|
|
entry: ['extractTokens'],
|
|
},
|
|
complete: {
|
|
type: 'final',
|
|
entry: ['showCompletionMessage', 'navigateToTokens'],
|
|
},
|
|
},
|
|
actions: {
|
|
showCreateForm: async ({ machine }) => {
|
|
notifyInfo('Create a new project to get started');
|
|
if (options.onShowCreateForm) {
|
|
await options.onShowCreateForm(machine);
|
|
}
|
|
},
|
|
|
|
createProject: async ({ data, machine }) => {
|
|
machine.context.projectName = data.projectName;
|
|
machine.context.projectId = data.projectId;
|
|
|
|
if (options.onCreateProject) {
|
|
await options.onCreateProject(machine.context);
|
|
}
|
|
},
|
|
|
|
showSuccessMessage: async ({ machine }) => {
|
|
notifySuccess(
|
|
`Project "${machine.context.projectName}" created successfully!`,
|
|
ErrorCode.SUCCESS_CREATED,
|
|
{ projectId: machine.context.projectId }
|
|
);
|
|
},
|
|
|
|
promptConfiguration: async ({ machine }) => {
|
|
notifyInfo(
|
|
'Next: Configure your project settings (Figma key, description)',
|
|
{ duration: 7000 }
|
|
);
|
|
|
|
if (options.onPromptConfiguration) {
|
|
await options.onPromptConfiguration(machine);
|
|
}
|
|
},
|
|
|
|
navigateToSettings: async ({ machine }) => {
|
|
router.navigate('figma');
|
|
|
|
if (options.onNavigateToSettings) {
|
|
await options.onNavigateToSettings(machine);
|
|
}
|
|
},
|
|
|
|
showReadyMessage: async ({ machine }) => {
|
|
notifyInfo('Project configured! Ready to extract tokens from Figma');
|
|
|
|
if (options.onReady) {
|
|
await options.onReady(machine);
|
|
}
|
|
},
|
|
|
|
extractTokens: async ({ machine }) => {
|
|
if (options.onExtractTokens) {
|
|
await options.onExtractTokens(machine);
|
|
}
|
|
},
|
|
|
|
showCompletionMessage: async ({ machine }) => {
|
|
notifySuccess(
|
|
'Tokens extracted successfully! Your design system is ready.',
|
|
ErrorCode.SUCCESS_OPERATION
|
|
);
|
|
|
|
if (options.onComplete) {
|
|
await options.onComplete(machine);
|
|
}
|
|
},
|
|
|
|
navigateToTokens: async ({ machine }) => {
|
|
router.navigate('tokens');
|
|
},
|
|
},
|
|
};
|
|
|
|
super(config);
|
|
|
|
// Store options
|
|
this.options = options;
|
|
|
|
// Subscribe to progress updates
|
|
this.on('stateChange', ({ current, previous }) => {
|
|
const progress = this.getProgress();
|
|
|
|
if (options.onProgress) {
|
|
options.onProgress(current, progress);
|
|
}
|
|
|
|
// Emit to global event bus
|
|
window.dispatchEvent(new CustomEvent('workflow-progress', {
|
|
detail: {
|
|
workflow: 'create-project',
|
|
current,
|
|
previous,
|
|
progress,
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start the workflow
|
|
* @param {Object} data - Initial data
|
|
*/
|
|
async start(data = {}) {
|
|
this.reset();
|
|
this.context = { ...data };
|
|
await this.send('CREATE_PROJECT', data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Token Extraction Workflow
|
|
*
|
|
* Guides through: Connect Figma → Select File → Extract → Sync
|
|
*/
|
|
export class TokenExtractionWorkflow extends StateMachine {
|
|
constructor(options = {}) {
|
|
const config = {
|
|
initial: 'disconnected',
|
|
states: {
|
|
disconnected: {
|
|
on: {
|
|
CONNECT_FIGMA: 'connecting',
|
|
},
|
|
entry: ['promptFigmaConnection'],
|
|
},
|
|
connecting: {
|
|
on: {
|
|
CONNECTION_SUCCESS: 'connected',
|
|
CONNECTION_FAILED: 'disconnected',
|
|
},
|
|
entry: ['testFigmaConnection'],
|
|
},
|
|
connected: {
|
|
on: {
|
|
SELECT_FILE: 'fileSelected',
|
|
DISCONNECT: 'disconnected',
|
|
},
|
|
entry: ['showFileSelector'],
|
|
},
|
|
fileSelected: {
|
|
on: {
|
|
EXTRACT: 'extracting',
|
|
CHANGE_FILE: 'connected',
|
|
},
|
|
entry: ['showExtractButton'],
|
|
},
|
|
extracting: {
|
|
on: {
|
|
EXTRACT_SUCCESS: 'extracted',
|
|
EXTRACT_FAILED: 'fileSelected',
|
|
},
|
|
entry: ['performExtraction'],
|
|
},
|
|
extracted: {
|
|
on: {
|
|
SYNC: 'syncing',
|
|
EXTRACT_AGAIN: 'extracting',
|
|
},
|
|
entry: ['showSyncOption'],
|
|
},
|
|
syncing: {
|
|
on: {
|
|
SYNC_SUCCESS: 'complete',
|
|
SYNC_FAILED: 'extracted',
|
|
},
|
|
entry: ['performSync'],
|
|
},
|
|
complete: {
|
|
type: 'final',
|
|
entry: ['showSuccess'],
|
|
},
|
|
},
|
|
actions: {
|
|
promptFigmaConnection: async () => {
|
|
notifyInfo('Connect to Figma to extract design tokens');
|
|
},
|
|
|
|
testFigmaConnection: async ({ data, machine }) => {
|
|
if (options.onTestConnection) {
|
|
await options.onTestConnection(data);
|
|
}
|
|
},
|
|
|
|
showFileSelector: async () => {
|
|
notifySuccess('Connected to Figma! Select a file to extract tokens.');
|
|
},
|
|
|
|
showExtractButton: async ({ data }) => {
|
|
notifyInfo(`File selected: ${data.fileName || 'Unknown'}. Ready to extract.`);
|
|
},
|
|
|
|
performExtraction: async ({ data, machine }) => {
|
|
if (options.onExtract) {
|
|
await options.onExtract(data);
|
|
}
|
|
},
|
|
|
|
showSyncOption: async () => {
|
|
notifySuccess('Tokens extracted! Sync to your codebase?');
|
|
},
|
|
|
|
performSync: async ({ data }) => {
|
|
if (options.onSync) {
|
|
await options.onSync(data);
|
|
}
|
|
},
|
|
|
|
showSuccess: async () => {
|
|
notifySuccess('Workflow complete! Tokens are synced and ready to use.');
|
|
},
|
|
},
|
|
};
|
|
|
|
super(config);
|
|
this.options = options;
|
|
}
|
|
|
|
async start(data = {}) {
|
|
this.reset();
|
|
await this.send('CONNECT_FIGMA', data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create workflow instances
|
|
*/
|
|
export function createProjectWorkflow(options) {
|
|
return new CreateProjectWorkflow(options);
|
|
}
|
|
|
|
export function tokenExtractionWorkflow(options) {
|
|
return new TokenExtractionWorkflow(options);
|
|
}
|
|
|
|
export { StateMachine };
|
|
export default {
|
|
StateMachine,
|
|
CreateProjectWorkflow,
|
|
TokenExtractionWorkflow,
|
|
createProjectWorkflow,
|
|
tokenExtractionWorkflow,
|
|
};
|