conductor-figma 1.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +59 -153
- package/bin/conductor.js +1 -75
- package/figma-plugin/code.js +755 -0
- package/figma-plugin/manifest.json +14 -0
- package/figma-plugin/ui.html +77 -0
- package/package.json +25 -16
- package/src/bridge.js +60 -0
- package/src/design/intelligence.js +273 -294
- package/src/server.js +82 -196
- package/src/tools/handlers.js +145 -463
- package/src/tools/registry.js +1144 -336
- package/src/blueprints.js +0 -775
- package/src/design/craftguide.js +0 -181
- package/src/design/exporter.js +0 -72
- package/src/index.js +0 -33
- package/src/orchestrator.js +0 -100
- package/src/relay.js +0 -176
package/src/design/craftguide.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — Design Craft Guide
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
// Professional design rules. The AI reads this before designing anything.
|
|
5
|
-
// This is what separates amateur output from production quality.
|
|
6
|
-
|
|
7
|
-
export function getDesignCraftGuide() {
|
|
8
|
-
return `# CONDUCTOR Design Craft Guide
|
|
9
|
-
|
|
10
|
-
You are a senior product designer working in Figma. Every frame, every text node, every color choice must follow these rules. No exceptions.
|
|
11
|
-
|
|
12
|
-
## Layout Architecture
|
|
13
|
-
|
|
14
|
-
### Frame Structure
|
|
15
|
-
- ALWAYS use auto-layout. Never absolute positioning.
|
|
16
|
-
- Direction: VERTICAL for page sections, HORIZONTAL for rows of items.
|
|
17
|
-
- Every frame needs explicit padding and gap. Never 0 unless intentional.
|
|
18
|
-
- Use primaryAxisSizingMode: "HUG" for content-driven frames, "FIXED" for containers with set widths.
|
|
19
|
-
- Use counterAxisSizingMode: "FILL" for child frames that should stretch to parent width.
|
|
20
|
-
|
|
21
|
-
### Spacing System (8px grid)
|
|
22
|
-
- Base unit: 8px. All spacing values must be multiples of 8.
|
|
23
|
-
- Micro spacing: 4px, 8px (within components, between label and input)
|
|
24
|
-
- Small spacing: 12px, 16px (between related elements, card padding)
|
|
25
|
-
- Medium spacing: 24px, 32px (between component groups, section padding)
|
|
26
|
-
- Large spacing: 48px, 64px (between page sections)
|
|
27
|
-
- Extra large: 80px, 96px, 120px (page-level vertical rhythm)
|
|
28
|
-
- NEVER use: 5, 7, 10, 13, 15, 17, 22, 25, 30, 35, 50, 55, 65, 70, 75, 90, 100
|
|
29
|
-
|
|
30
|
-
### Content Width
|
|
31
|
-
- Max content width: 1200px for desktop, centered in wider frames
|
|
32
|
-
- For a 1440px page, use 48-80px horizontal padding to create margins
|
|
33
|
-
- Card grid max: 3-4 columns. Never 5+ columns for content cards.
|
|
34
|
-
- Sidebar: 240-280px. Never wider than 320px.
|
|
35
|
-
|
|
36
|
-
## Typography
|
|
37
|
-
|
|
38
|
-
### Type Scale (use Inter font family)
|
|
39
|
-
- Display: 56-72px, Bold or Extra Bold, line-height 1.1
|
|
40
|
-
- H1: 40-48px, Bold, line-height 1.15
|
|
41
|
-
- H2: 32-36px, Bold or Semi Bold, line-height 1.2
|
|
42
|
-
- H3: 24-28px, Semi Bold, line-height 1.25
|
|
43
|
-
- H4: 18-20px, Semi Bold, line-height 1.3
|
|
44
|
-
- Body large: 18px, Regular, line-height 1.6
|
|
45
|
-
- Body: 15-16px, Regular, line-height 1.6
|
|
46
|
-
- Body small: 13-14px, Regular, line-height 1.5
|
|
47
|
-
- Caption: 11-12px, Medium, line-height 1.4
|
|
48
|
-
- Overline: 10-12px, Semi Bold or Bold, uppercase, letter-spacing 0.08em
|
|
49
|
-
|
|
50
|
-
### Typography Rules
|
|
51
|
-
- Maximum 2 font weights per section (e.g., Bold + Regular, Semi Bold + Regular)
|
|
52
|
-
- Body text color should NEVER be pure white (#ffffff). Use #f0f0f8 or #e8e8f0 for dark themes.
|
|
53
|
-
- Muted text: #a0a0b8 for dark themes, #666680 for light themes.
|
|
54
|
-
- Heading to body size ratio should be at least 1.5x.
|
|
55
|
-
- Line length: 45-75 characters for body text. Use max-width on text containers.
|
|
56
|
-
|
|
57
|
-
## Color
|
|
58
|
-
|
|
59
|
-
### Dark Theme Palette
|
|
60
|
-
- Background levels: #09090f → #0c0c18 → #0f0f1c → #12122a → #16163a
|
|
61
|
-
- Each level should be visibly distinct but not jarring.
|
|
62
|
-
- Card backgrounds should be 1-2 levels lighter than the page background.
|
|
63
|
-
- Text hierarchy: #f0f0f8 (primary) → #a0a0b8 (secondary) → #686880 (tertiary)
|
|
64
|
-
- Dividers: #1a1a32 or #1e1e3a (subtle, never bright)
|
|
65
|
-
- NEVER use pure black (#000000) as a background.
|
|
66
|
-
- NEVER use pure white (#ffffff) as text on dark backgrounds.
|
|
67
|
-
|
|
68
|
-
### Light Theme Palette
|
|
69
|
-
- Background levels: #ffffff → #f9f9fb → #f3f3f7 → #ebebf0
|
|
70
|
-
- Card backgrounds: #ffffff with subtle border (#e4e4ec)
|
|
71
|
-
- Text hierarchy: #111118 (primary) → #55556a (secondary) → #88889a (tertiary)
|
|
72
|
-
- Dividers: #e4e4ec
|
|
73
|
-
|
|
74
|
-
### Brand Color Usage
|
|
75
|
-
- Primary brand color: buttons, links, badges, icons, active states.
|
|
76
|
-
- NEVER use brand color for large background areas.
|
|
77
|
-
- Brand color for text only in overlines, links, and badges.
|
|
78
|
-
- Hover states: darken brand color by 10%. Active: darken by 15%.
|
|
79
|
-
|
|
80
|
-
## Components
|
|
81
|
-
|
|
82
|
-
### Buttons
|
|
83
|
-
- Height: 36px (small), 40px (default), 48px (large), 56px (hero)
|
|
84
|
-
- Horizontal padding: 16px (small), 20px (default), 28px (large), 32px (hero)
|
|
85
|
-
- Corner radius: 8px (default), 10-12px (large/hero)
|
|
86
|
-
- Primary: brand color fill, white text, Semi Bold
|
|
87
|
-
- Secondary: transparent fill, border or muted text, Medium weight
|
|
88
|
-
- Ghost: transparent, text only, Medium weight
|
|
89
|
-
- Always center text both axes in buttons
|
|
90
|
-
|
|
91
|
-
### Cards
|
|
92
|
-
- Padding: 24-32px (compact), 28-40px (standard)
|
|
93
|
-
- Corner radius: 12-16px
|
|
94
|
-
- Gap between elements inside: 12-20px
|
|
95
|
-
- On dark themes: use a lighter background, no border
|
|
96
|
-
- On light themes: white background + subtle border (#e4e4ec) + optional shadow
|
|
97
|
-
- Cards in a row should all be the same height (use counterAxisAlignItems: "STRETCH" on parent)
|
|
98
|
-
|
|
99
|
-
### Navigation
|
|
100
|
-
- Height: 64-72px for top nav
|
|
101
|
-
- Logo left, links center or right, CTA button far right
|
|
102
|
-
- Use a spacer frame (FILL sizing) between logo and links to push them apart
|
|
103
|
-
- Nav links: 14px Medium, 24-32px gap between items
|
|
104
|
-
- Active state: brand color or bolder weight
|
|
105
|
-
- Add a 1px divider below the nav
|
|
106
|
-
|
|
107
|
-
### Metric/Stat Cards
|
|
108
|
-
- Stack: label on top (12-13px, muted, Medium), value below (28-36px, Bold)
|
|
109
|
-
- Optional: change indicator below value (12-13px, green for positive, red for negative)
|
|
110
|
-
- Equal width cards in a horizontal row
|
|
111
|
-
- 20-24px gap between metric cards
|
|
112
|
-
|
|
113
|
-
### Tables
|
|
114
|
-
- Header row: 40-48px height, uppercase 10-11px labels, muted color, Medium weight
|
|
115
|
-
- Data rows: 48-56px height, 14-15px Regular text
|
|
116
|
-
- Cell padding: 16-20px horizontal
|
|
117
|
-
- Alternating row backgrounds or horizontal dividers (not both)
|
|
118
|
-
- Status badges: small colored pills with 4-6px padding, rounded
|
|
119
|
-
|
|
120
|
-
### Forms
|
|
121
|
-
- Input height: 40-44px
|
|
122
|
-
- Label: 13-14px, Medium weight, 4-8px gap below label
|
|
123
|
-
- Input: 14-15px Regular, 12-16px horizontal padding
|
|
124
|
-
- Corner radius: 8px
|
|
125
|
-
- Border: 1px, muted color. Focus: brand color border
|
|
126
|
-
- 20-24px gap between field groups
|
|
127
|
-
|
|
128
|
-
## Section Patterns
|
|
129
|
-
|
|
130
|
-
### Hero Section
|
|
131
|
-
- Vertical padding: 80-120px
|
|
132
|
-
- Content centered (both axes)
|
|
133
|
-
- Overline badge → Heading (56-72px) → Subtitle (18-20px) → Button row → Social proof
|
|
134
|
-
- Max heading width: ~600px
|
|
135
|
-
- Subtitle max width: ~500px
|
|
136
|
-
- 28-32px gap between heading and subtitle
|
|
137
|
-
- 32-40px gap between subtitle and buttons
|
|
138
|
-
|
|
139
|
-
### Feature Section
|
|
140
|
-
- Heading + subtitle centered at top
|
|
141
|
-
- 3 cards in a row (or 2 for detailed features)
|
|
142
|
-
- Each card: icon/emoji → title → description → optional link
|
|
143
|
-
- 48px gap between heading group and card row
|
|
144
|
-
|
|
145
|
-
### Stats/Social Proof
|
|
146
|
-
- Background color shift (one level different from surrounding sections)
|
|
147
|
-
- 3-4 stats in a horizontal row, centered
|
|
148
|
-
- Each stat centered: big number + label below
|
|
149
|
-
|
|
150
|
-
### CTA Section
|
|
151
|
-
- Often wrapped in a card or a background shift
|
|
152
|
-
- Centered: heading → subtitle → button row
|
|
153
|
-
- Generous padding: 64-96px vertical
|
|
154
|
-
|
|
155
|
-
### Footer
|
|
156
|
-
- Logo left, copyright right, spacer between
|
|
157
|
-
- Or: multi-column with link groups
|
|
158
|
-
- Divider line above
|
|
159
|
-
- Muted text, smaller font sizes (13px)
|
|
160
|
-
|
|
161
|
-
## Anti-Patterns (NEVER do these)
|
|
162
|
-
- Never use absolute positioning. Always auto-layout.
|
|
163
|
-
- Never use font sizes that aren't in the type scale.
|
|
164
|
-
- Never use spacing values that aren't multiples of 4 or 8.
|
|
165
|
-
- Never put more than 3-4 cards in a row.
|
|
166
|
-
- Never use pure black or pure white on dark themes.
|
|
167
|
-
- Never make buttons without sufficient padding (minimum 16px horizontal).
|
|
168
|
-
- Never stack more than 4-5 levels of nesting without good reason.
|
|
169
|
-
- Never use inconsistent corner radii on the same page.
|
|
170
|
-
- Never use more than 2-3 distinct background colors per page.
|
|
171
|
-
- Never center-align body paragraphs (center headings only).
|
|
172
|
-
- Never use text smaller than 11px.
|
|
173
|
-
- Never create frames without naming them descriptively.
|
|
174
|
-
|
|
175
|
-
## Naming Convention
|
|
176
|
-
- Pages: "Landing Page", "Pricing Page", "Dashboard"
|
|
177
|
-
- Sections: "Hero Section", "Features Section", "CTA Section"
|
|
178
|
-
- Components: "Nav CTA", "Feature Card", "Stat Card", "Price Tier"
|
|
179
|
-
- Layout: "Card Row", "Button Row", "Nav Links"
|
|
180
|
-
- Never use "Frame 1", "Rectangle 2", or auto-generated names.`;
|
|
181
|
-
}
|
package/src/design/exporter.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — Token Exporter
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
|
|
5
|
-
export function exportCSS(tokens) {
|
|
6
|
-
let out = '/* Generated by CONDUCTOR */\n:root {\n';
|
|
7
|
-
if (tokens.colors) for (const [k, v] of Object.entries(tokens.colors)) out += ` --color-${k}: ${v};\n`;
|
|
8
|
-
if (tokens.spacing) tokens.spacing.forEach((v, i) => { out += ` --space-${i + 1}: ${v}px;\n`; });
|
|
9
|
-
if (tokens.fontSizes) {
|
|
10
|
-
const names = ['xs','sm','base','md','lg','xl','2xl','3xl','4xl','5xl','6xl'];
|
|
11
|
-
tokens.fontSizes.forEach((v, i) => { out += ` --text-${names[i] || i}: ${v.size || v}px;\n`; });
|
|
12
|
-
}
|
|
13
|
-
if (tokens.radii) tokens.radii.forEach(r => { out += ` --radius-${r.name}: ${r.value >= 9999 ? '9999px' : r.value + 'px'};\n`; });
|
|
14
|
-
if (tokens.shadows) tokens.shadows.forEach(s => { out += ` --shadow-${s.step}: ${s.css};\n`; });
|
|
15
|
-
out += '}\n';
|
|
16
|
-
return out;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function exportTailwind(tokens) {
|
|
20
|
-
const config = { theme: { extend: {} } };
|
|
21
|
-
if (tokens.colors) config.theme.extend.colors = { ...tokens.colors };
|
|
22
|
-
if (tokens.spacing) {
|
|
23
|
-
config.theme.extend.spacing = {};
|
|
24
|
-
tokens.spacing.forEach((v, i) => { config.theme.extend.spacing[String(i + 1)] = `${v}px`; });
|
|
25
|
-
}
|
|
26
|
-
if (tokens.fontSizes) {
|
|
27
|
-
config.theme.extend.fontSize = {};
|
|
28
|
-
const names = ['xs','sm','base','md','lg','xl','2xl','3xl','4xl','5xl','6xl'];
|
|
29
|
-
tokens.fontSizes.forEach((v, i) => { config.theme.extend.fontSize[names[i] || i] = `${v.size || v}px`; });
|
|
30
|
-
}
|
|
31
|
-
if (tokens.radii) {
|
|
32
|
-
config.theme.extend.borderRadius = {};
|
|
33
|
-
tokens.radii.forEach(r => { config.theme.extend.borderRadius[r.name] = r.value >= 9999 ? '9999px' : `${r.value}px`; });
|
|
34
|
-
}
|
|
35
|
-
return `// tailwind.config.js — Generated by CONDUCTOR\nmodule.exports = ${JSON.stringify(config, null, 2)}\n`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function exportSCSS(tokens) {
|
|
39
|
-
let out = '// Generated by CONDUCTOR\n\n';
|
|
40
|
-
if (tokens.colors) for (const [k, v] of Object.entries(tokens.colors)) out += `$color-${k}: ${v};\n`;
|
|
41
|
-
if (tokens.spacing) { out += '\n'; tokens.spacing.forEach((v, i) => { out += `$space-${i + 1}: ${v}px;\n`; }); }
|
|
42
|
-
if (tokens.fontSizes) {
|
|
43
|
-
out += '\n';
|
|
44
|
-
const names = ['xs','sm','base','md','lg','xl','2xl','3xl','4xl','5xl','6xl'];
|
|
45
|
-
tokens.fontSizes.forEach((v, i) => { out += `$text-${names[i] || i}: ${v.size || v}px;\n`; });
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function exportJSON(tokens) {
|
|
51
|
-
return JSON.stringify(tokens, null, 2);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function exportW3CTokens(tokens) {
|
|
55
|
-
const w3c = {};
|
|
56
|
-
if (tokens.colors) {
|
|
57
|
-
w3c.color = {};
|
|
58
|
-
for (const [k, v] of Object.entries(tokens.colors)) {
|
|
59
|
-
w3c.color[k] = { $value: v, $type: 'color' };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (tokens.spacing) {
|
|
63
|
-
w3c.spacing = {};
|
|
64
|
-
tokens.spacing.forEach((v, i) => { w3c.spacing[`space-${i + 1}`] = { $value: `${v}px`, $type: 'dimension' }; });
|
|
65
|
-
}
|
|
66
|
-
if (tokens.fontSizes) {
|
|
67
|
-
w3c.fontSize = {};
|
|
68
|
-
const names = ['xs','sm','base','md','lg','xl','2xl','3xl','4xl','5xl','6xl'];
|
|
69
|
-
tokens.fontSizes.forEach((v, i) => { w3c.fontSize[names[i] || i] = { $value: `${v.size || v}px`, $type: 'dimension' }; });
|
|
70
|
-
}
|
|
71
|
-
return JSON.stringify(w3c, null, 2);
|
|
72
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — Public API
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
|
|
5
|
-
export { startServer } from './server.js';
|
|
6
|
-
export { Relay } from './relay.js';
|
|
7
|
-
export { executeSequence } from './orchestrator.js';
|
|
8
|
-
export { getBlueprint, buildLandingPage, buildPricingPage, buildDashboardPage, buildSection } from './blueprints.js';
|
|
9
|
-
export { TOOLS, CATEGORIES, getToolByName, getToolsByCategory, getAllToolNames } from './tools/registry.js';
|
|
10
|
-
export { handleTool } from './tools/handlers.js';
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
// Grid
|
|
14
|
-
snapToGrid, isOnGrid, generateSpacingScale, findNearestGridValue, auditSpacing,
|
|
15
|
-
// Type
|
|
16
|
-
TYPE_SCALES, generateTypeScale, detectTypeScale, getLineHeight, getFontWeight, checkMeasure,
|
|
17
|
-
// Color
|
|
18
|
-
hexToRgb, rgbToHex, hexToHsl, hslToHex, generatePalette, generateSemanticColors,
|
|
19
|
-
generateDarkMode, relativeLuminance, contrastRatio, checkContrast,
|
|
20
|
-
// Shadow & Radius
|
|
21
|
-
generateElevation, generateRadiusScale,
|
|
22
|
-
// Hierarchy
|
|
23
|
-
assessHierarchy,
|
|
24
|
-
// Accessibility
|
|
25
|
-
checkTouchTarget,
|
|
26
|
-
// Layout
|
|
27
|
-
inferLayoutDirection, inferGap, inferPadding, BREAKPOINTS, scaleForBreakpoint,
|
|
28
|
-
// Score
|
|
29
|
-
computeDesignScore,
|
|
30
|
-
} from './design/intelligence.js';
|
|
31
|
-
|
|
32
|
-
export { exportCSS, exportTailwind, exportSCSS, exportJSON, exportW3CTokens } from './design/exporter.js';
|
|
33
|
-
export { getDesignCraftGuide } from './design/craftguide.js';
|
package/src/orchestrator.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — Orchestrator
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
// Takes a blueprint (sequence of commands) and executes them
|
|
5
|
-
// through the relay one at a time. Each command can reference
|
|
6
|
-
// results from previous commands via $ref tokens.
|
|
7
|
-
//
|
|
8
|
-
// Example: create a frame, then create text inside it.
|
|
9
|
-
// The text command references the frame's ID via $ref.
|
|
10
|
-
//
|
|
11
|
-
// { type: 'create_frame', name: 'Hero', ... } → returns { id: '5:2' }
|
|
12
|
-
// { type: 'create_text', parentId: '$0.id', text: ... } → parentId resolved to '5:2'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Execute a sequence of Figma commands through the relay.
|
|
16
|
-
* Supports $ref tokens: '$N.field' references result N's field.
|
|
17
|
-
*
|
|
18
|
-
* @param {Object} relay - The WebSocket relay instance
|
|
19
|
-
* @param {Array} commands - Array of { type, data } command objects
|
|
20
|
-
* @param {Function} onProgress - Optional callback(stepIndex, totalSteps, result)
|
|
21
|
-
* @returns {Promise<{ results: Array, errors: Array, success: boolean }>}
|
|
22
|
-
*/
|
|
23
|
-
export async function executeSequence(relay, commands, onProgress) {
|
|
24
|
-
var results = [];
|
|
25
|
-
var errors = [];
|
|
26
|
-
|
|
27
|
-
for (var i = 0; i < commands.length; i++) {
|
|
28
|
-
var cmd = commands[i];
|
|
29
|
-
var resolvedData = resolveRefs(cmd.data || {}, results);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
var result = await relay.sendToPlugin(cmd.type, resolvedData, 10000);
|
|
33
|
-
|
|
34
|
-
if (result && result.error) {
|
|
35
|
-
errors.push({ step: i, command: cmd.type, error: result.error });
|
|
36
|
-
results.push(result);
|
|
37
|
-
// Don't stop on error — skip and continue
|
|
38
|
-
process.stderr.write('CONDUCTOR orchestrator: step ' + i + ' (' + cmd.type + ') error: ' + result.error + '\n');
|
|
39
|
-
} else {
|
|
40
|
-
results.push(result || {});
|
|
41
|
-
if (onProgress) onProgress(i, commands.length, result);
|
|
42
|
-
process.stderr.write('CONDUCTOR orchestrator: step ' + (i + 1) + '/' + commands.length + ' ' + cmd.type + ' -> ' + (result && (result.id || result.name) || 'ok') + '\n');
|
|
43
|
-
}
|
|
44
|
-
} catch (err) {
|
|
45
|
-
errors.push({ step: i, command: cmd.type, error: String(err) });
|
|
46
|
-
results.push({ error: String(err) });
|
|
47
|
-
process.stderr.write('CONDUCTOR orchestrator: step ' + i + ' (' + cmd.type + ') threw: ' + String(err) + '\n');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Small delay between commands to let Figma process
|
|
51
|
-
await sleep(50);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
results: results,
|
|
56
|
-
errors: errors,
|
|
57
|
-
success: errors.length === 0,
|
|
58
|
-
totalSteps: commands.length,
|
|
59
|
-
completedSteps: commands.length - errors.length,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Resolve $ref tokens in command data.
|
|
65
|
-
* '$0.id' → results[0].id
|
|
66
|
-
* '$parent' → results[results.length-1].id (last result's id)
|
|
67
|
-
* '$2.name' → results[2].name
|
|
68
|
-
*/
|
|
69
|
-
function resolveRefs(data, results) {
|
|
70
|
-
if (typeof data === 'string') {
|
|
71
|
-
// Check for $ref pattern
|
|
72
|
-
return data.replace(/\$(\d+)\.(\w+)/g, function(match, idx, field) {
|
|
73
|
-
var r = results[parseInt(idx)];
|
|
74
|
-
return (r && r[field] !== undefined) ? r[field] : match;
|
|
75
|
-
}).replace(/\$parent/g, function() {
|
|
76
|
-
var last = results[results.length - 1];
|
|
77
|
-
return (last && last.id) ? last.id : '';
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (Array.isArray(data)) {
|
|
82
|
-
return data.map(function(item) { return resolveRefs(item, results); });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (data && typeof data === 'object') {
|
|
86
|
-
var resolved = {};
|
|
87
|
-
for (var key in data) {
|
|
88
|
-
if (data.hasOwnProperty(key)) {
|
|
89
|
-
resolved[key] = resolveRefs(data[key], results);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return resolved;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return data;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function sleep(ms) {
|
|
99
|
-
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
|
100
|
-
}
|
package/src/relay.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════
|
|
2
|
-
// CONDUCTOR — WebSocket Relay
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
// Bridges MCP stdio (from Cursor) to WebSocket (to Figma plugin).
|
|
5
|
-
//
|
|
6
|
-
// Flow:
|
|
7
|
-
// Cursor → MCP stdio → CONDUCTOR → design logic (local)
|
|
8
|
-
// → figma commands (WebSocket) → Plugin → Figma API
|
|
9
|
-
// ← results ← WebSocket ←
|
|
10
|
-
// ← MCP stdio ← CONDUCTOR ←
|
|
11
|
-
|
|
12
|
-
import { createServer } from 'node:http';
|
|
13
|
-
|
|
14
|
-
// Tools that need Figma (sent over WebSocket to plugin)
|
|
15
|
-
const FIGMA_COMMANDS = new Set([
|
|
16
|
-
// Create
|
|
17
|
-
'create_frame', 'create_text', 'create_rect', 'create_section', 'create_component',
|
|
18
|
-
'create_ellipse', 'create_svg_node',
|
|
19
|
-
// Layout
|
|
20
|
-
'set_auto_layout', 'set_constraints', 'apply_grid', 'align_nodes',
|
|
21
|
-
// Style
|
|
22
|
-
'set_fills', 'set_strokes', 'set_effects', 'set_corner_radius', 'set_opacity',
|
|
23
|
-
// Typography
|
|
24
|
-
'set_text_props', 'load_font', 'style_text_range',
|
|
25
|
-
// Read
|
|
26
|
-
'get_selection', 'get_page_info', 'get_styles', 'get_components',
|
|
27
|
-
'read_node', 'read_tree', 'read_spacing', 'read_colors', 'read_typography',
|
|
28
|
-
'find_nodes',
|
|
29
|
-
// Edit
|
|
30
|
-
'rename_node', 'move_node', 'resize_node', 'delete_node',
|
|
31
|
-
'clone_node', 'group_nodes', 'ungroup_node', 'reorder_node',
|
|
32
|
-
// Export
|
|
33
|
-
'export_png', 'export_svg',
|
|
34
|
-
// Viewport
|
|
35
|
-
'zoom_to', 'scroll_to',
|
|
36
|
-
// Meta
|
|
37
|
-
'ping',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
export class Relay {
|
|
41
|
-
constructor(port) {
|
|
42
|
-
this.port = port || 9800;
|
|
43
|
-
this.pluginSocket = null;
|
|
44
|
-
this.pendingCallbacks = new Map();
|
|
45
|
-
this.cmdId = 0;
|
|
46
|
-
this.server = null;
|
|
47
|
-
this.wss = null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async start() {
|
|
51
|
-
// Dynamic import ws (may not be installed — we bundle our own minimal WS)
|
|
52
|
-
let WebSocketServer;
|
|
53
|
-
try {
|
|
54
|
-
const ws = await import('ws');
|
|
55
|
-
WebSocketServer = ws.WebSocketServer || ws.default.WebSocketServer;
|
|
56
|
-
} catch (e) {
|
|
57
|
-
process.stderr.write('CONDUCTOR relay: "ws" package not found. Install with: npm install ws\n');
|
|
58
|
-
process.stderr.write('Falling back to MCP-only mode (no Figma bridge).\n');
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.server = createServer();
|
|
63
|
-
this.wss = new WebSocketServer({ server: this.server });
|
|
64
|
-
|
|
65
|
-
this.wss.on('connection', (socket) => {
|
|
66
|
-
this.pluginSocket = socket;
|
|
67
|
-
process.stderr.write('CONDUCTOR relay: Figma plugin connected\n');
|
|
68
|
-
|
|
69
|
-
socket.on('message', (data) => {
|
|
70
|
-
try {
|
|
71
|
-
const msg = JSON.parse(data.toString());
|
|
72
|
-
this.handlePluginMessage(msg);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
// ignore
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
socket.on('close', () => {
|
|
79
|
-
this.pluginSocket = null;
|
|
80
|
-
process.stderr.write('CONDUCTOR relay: Figma plugin disconnected\n');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
socket.on('error', () => {
|
|
84
|
-
this.pluginSocket = null;
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
this.server.listen(this.port, () => {
|
|
90
|
-
process.stderr.write(`CONDUCTOR relay: WebSocket listening on ws://localhost:${this.port}\n`);
|
|
91
|
-
resolve(true);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
handlePluginMessage(msg) {
|
|
97
|
-
if (msg.type === 'plugin_ready') {
|
|
98
|
-
process.stderr.write(`CONDUCTOR relay: Plugin ready (v${msg.version || '?'})\n`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (msg.type === 'result' && msg.id !== undefined) {
|
|
103
|
-
const callback = this.pendingCallbacks.get(msg.id);
|
|
104
|
-
if (callback) {
|
|
105
|
-
this.pendingCallbacks.delete(msg.id);
|
|
106
|
-
callback(msg.data || {});
|
|
107
|
-
}
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
isConnected() {
|
|
113
|
-
return this.pluginSocket !== null && this.pluginSocket.readyState === 1; // WebSocket.OPEN
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Send a command to the Figma plugin and wait for result.
|
|
118
|
-
* Returns a Promise that resolves with the plugin's response.
|
|
119
|
-
*/
|
|
120
|
-
sendToPlugin(commandType, commandData, timeout) {
|
|
121
|
-
timeout = timeout || 15000;
|
|
122
|
-
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
if (!this.isConnected()) {
|
|
125
|
-
resolve({ error: 'Figma plugin not connected. Open the CONDUCTOR plugin in Figma and click Connect.' });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
var id = ++this.cmdId;
|
|
130
|
-
var timer = setTimeout(function() {
|
|
131
|
-
this.pendingCallbacks.delete(id);
|
|
132
|
-
resolve({ error: 'Timeout waiting for Figma plugin response' });
|
|
133
|
-
}.bind(this), timeout);
|
|
134
|
-
|
|
135
|
-
this.pendingCallbacks.set(id, function(result) {
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
resolve(result);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
this.pluginSocket.send(JSON.stringify({
|
|
141
|
-
type: 'command',
|
|
142
|
-
id: id,
|
|
143
|
-
command: { type: commandType, data: commandData || {} },
|
|
144
|
-
}));
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Check if a tool name maps to a Figma command.
|
|
150
|
-
*/
|
|
151
|
-
isFigmaCommand(toolName) {
|
|
152
|
-
return FIGMA_COMMANDS.has(toolName);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Map a high-level tool call to one or more Figma commands.
|
|
157
|
-
* Some tools (like create_page) produce multiple Figma commands.
|
|
158
|
-
* Some tools (like color_palette) are pure design logic — no Figma needed.
|
|
159
|
-
*/
|
|
160
|
-
getFigmaCommand(toolName, toolArgs) {
|
|
161
|
-
// Direct mappings — tool name IS the Figma command
|
|
162
|
-
if (FIGMA_COMMANDS.has(toolName)) {
|
|
163
|
-
return { command: toolName, data: toolArgs };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Tools that generate Figma commands from design logic output
|
|
167
|
-
// The handler produces a JSON response with an "action" field
|
|
168
|
-
// that maps to a Figma command
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
stop() {
|
|
173
|
-
if (this.wss) this.wss.close();
|
|
174
|
-
if (this.server) this.server.close();
|
|
175
|
-
}
|
|
176
|
-
}
|