@createlex/figgen 1.4.2
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/README.md +164 -0
- package/bin/figgen.js +156 -0
- package/companion/bridge-server.cjs +786 -0
- package/companion/createlex-auth.cjs +364 -0
- package/companion/local-llm-generator.cjs +437 -0
- package/companion/login.mjs +189 -0
- package/companion/mcp-server.mjs +1365 -0
- package/companion/package.json +17 -0
- package/companion/server.js +65 -0
- package/companion/setup.cjs +309 -0
- package/companion/xcode-writer.cjs +516 -0
- package/package.json +50 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figma-swiftui-companion",
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "Local server that writes Figma-generated SwiftUI code and images into an Xcode project",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"login": "node login.mjs",
|
|
8
|
+
"start": "node mcp-server.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"express": "^4.18.0",
|
|
14
|
+
"ws": "^8.20.0",
|
|
15
|
+
"zod": "^4.3.6"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { startBridgeServer } = require('./bridge-server.cjs');
|
|
2
|
+
const { authorizeRuntimeStartup, validateRuntimeSession } = require('./createlex-auth.cjs');
|
|
3
|
+
|
|
4
|
+
const AUTH_REVALIDATION_INTERVAL_MS = Number(process.env.FIGMA_SWIFTUI_AUTH_REVALIDATION_MS || (10 * 60 * 1000));
|
|
5
|
+
|
|
6
|
+
function readProjectPathArg(argv) {
|
|
7
|
+
const argIdx = argv.indexOf('--project');
|
|
8
|
+
if (argIdx !== -1 && argv[argIdx + 1]) {
|
|
9
|
+
return argv[argIdx + 1];
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
let authState = await authorizeRuntimeStartup();
|
|
16
|
+
console.log(
|
|
17
|
+
authState.bypass
|
|
18
|
+
? '[figma-swiftui-bridge] Authorization bypass enabled'
|
|
19
|
+
: `[figma-swiftui-bridge] Authorized CreateLex user ${authState.email || authState.userId || 'unknown-user'}`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const projectPath = readProjectPathArg(process.argv);
|
|
23
|
+
const bridgeRuntime = await startBridgeServer({
|
|
24
|
+
projectPath,
|
|
25
|
+
logger: console,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const authValidationTimer = setInterval(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const validation = await validateRuntimeSession(authState);
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
console.error(`[figma-swiftui-bridge] Authorization lost: ${validation.error}`);
|
|
33
|
+
// Signal the bridge to return authRequired from /ping so the plugin UI
|
|
34
|
+
// can show a "Login required" panel instead of just "runtime off".
|
|
35
|
+
if (bridgeRuntime && typeof bridgeRuntime.setAuthRequired === 'function') {
|
|
36
|
+
bridgeRuntime.setAuthRequired(validation.error || 'Token expired. Run: npx @createlex/figma-swiftui-mcp login');
|
|
37
|
+
} else {
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
authState = validation.session;
|
|
44
|
+
if (validation.refreshed) {
|
|
45
|
+
console.log('[figma-swiftui-bridge] Refreshed CreateLex MCP authorization');
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const reason = error instanceof Error ? error.message : 'unknown_error';
|
|
49
|
+
console.error(`[figma-swiftui-bridge] Authorization revalidation failed: ${reason}`);
|
|
50
|
+
if (bridgeRuntime && typeof bridgeRuntime.setAuthRequired === 'function') {
|
|
51
|
+
bridgeRuntime.setAuthRequired(`Session revalidation failed: ${reason}`);
|
|
52
|
+
} else {
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, AUTH_REVALIDATION_INTERVAL_MS);
|
|
57
|
+
|
|
58
|
+
authValidationTimer.unref?.();
|
|
59
|
+
console.log('\nReady to receive from Figma plugin.\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((error) => {
|
|
63
|
+
console.error('[figma-swiftui-bridge] Server error:', error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* figma-swiftui-mcp setup
|
|
3
|
+
*
|
|
4
|
+
* Auto-detect installed IDEs / CLI tools and add the figma-swiftui MCP
|
|
5
|
+
* server entry to each config file.
|
|
6
|
+
*
|
|
7
|
+
* Supported targets:
|
|
8
|
+
* - Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
9
|
+
* - Claude Code (~/.claude.json → mcpServers) (covers CLI + desktop + web)
|
|
10
|
+
* - Cursor (~/.cursor/mcp.json)
|
|
11
|
+
* - Windsurf (~/.codeium/windsurf/mcp_config.json)
|
|
12
|
+
* - VS Code (~/.vscode/mcp.json — user-level)
|
|
13
|
+
* - OpenCode (~/.config/opencode/opencode.json → mcp)
|
|
14
|
+
* - Codex CLI (~/.codex/config.toml → [mcp_servers.figma-swiftui])
|
|
15
|
+
* - Gemini CLI (~/.gemini/settings.json)
|
|
16
|
+
* - Antigravity (~/.gemini/antigravity/mcp_config.json)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
|
|
25
|
+
const MCP_KEY = 'figma-swiftui';
|
|
26
|
+
|
|
27
|
+
// ── Resolve the absolute path to the bin script and node ─────────────
|
|
28
|
+
// IDEs (Cursor, VS Code, etc.) do NOT source shell profiles, so
|
|
29
|
+
// `#!/usr/bin/env node` fails when node is managed by nvm/fnm/volta.
|
|
30
|
+
// Instead we record the absolute node path and the bin script separately.
|
|
31
|
+
function resolvePaths() {
|
|
32
|
+
const nodePath = process.execPath; // absolute path to node binary
|
|
33
|
+
|
|
34
|
+
// Try to find the bin JS file
|
|
35
|
+
const binScript = process.argv[1];
|
|
36
|
+
if (binScript) {
|
|
37
|
+
const resolved = fs.realpathSync(binScript);
|
|
38
|
+
const dir = path.dirname(resolved);
|
|
39
|
+
// Check for the .js bin entry point
|
|
40
|
+
const jsCandidate = path.join(dir, 'figma-swiftui-mcp.js');
|
|
41
|
+
if (fs.existsSync(jsCandidate)) {
|
|
42
|
+
return { nodePath, scriptPath: jsCandidate };
|
|
43
|
+
}
|
|
44
|
+
// Check for the wrapper (symlink without .js)
|
|
45
|
+
const candidate = path.join(dir, 'figma-swiftui-mcp');
|
|
46
|
+
if (fs.existsSync(candidate)) {
|
|
47
|
+
// Resolve symlinks to get the actual .js file
|
|
48
|
+
const real = fs.realpathSync(candidate);
|
|
49
|
+
return { nodePath, scriptPath: real };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback: search PATH for the binary, then resolve to its .js source
|
|
54
|
+
const whichCmd = require('node:child_process')
|
|
55
|
+
.execSync('which figma-swiftui-mcp 2>/dev/null || true')
|
|
56
|
+
.toString()
|
|
57
|
+
.trim();
|
|
58
|
+
if (whichCmd) {
|
|
59
|
+
const real = fs.realpathSync(whichCmd);
|
|
60
|
+
return { nodePath, scriptPath: real };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Last resort: use npx invocation
|
|
64
|
+
return { nodePath, scriptPath: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Resolve absolute npx path for fallback ───────────────────────────
|
|
68
|
+
function resolveNpxPath() {
|
|
69
|
+
try {
|
|
70
|
+
const npxPath = require('node:child_process')
|
|
71
|
+
.execSync('which npx 2>/dev/null || true')
|
|
72
|
+
.toString()
|
|
73
|
+
.trim();
|
|
74
|
+
return npxPath || 'npx';
|
|
75
|
+
} catch {
|
|
76
|
+
return 'npx';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── IDE config definitions ────────────────────────────────────────────
|
|
81
|
+
function getTargets({ nodePath, scriptPath }) {
|
|
82
|
+
const home = os.homedir();
|
|
83
|
+
|
|
84
|
+
// Use absolute node path + script to avoid #!/usr/bin/env node failures
|
|
85
|
+
// in IDEs that don't source shell profiles (nvm/fnm/volta).
|
|
86
|
+
const stdioEntry = scriptPath
|
|
87
|
+
? { command: nodePath, args: [scriptPath, 'start'] }
|
|
88
|
+
: { command: resolveNpxPath(), args: ['-y', '@createlex/figma-swiftui-mcp', 'start'] };
|
|
89
|
+
|
|
90
|
+
const stdioEntryWithType = { type: 'stdio', ...stdioEntry };
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
name: 'Claude Desktop',
|
|
95
|
+
path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
96
|
+
key: 'mcpServers',
|
|
97
|
+
entry: stdioEntry,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Claude Code',
|
|
101
|
+
path: path.join(home, '.claude.json'),
|
|
102
|
+
key: 'mcpServers',
|
|
103
|
+
entry: stdioEntryWithType,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'Cursor',
|
|
107
|
+
path: path.join(home, '.cursor', 'mcp.json'),
|
|
108
|
+
key: 'mcpServers',
|
|
109
|
+
entry: stdioEntry,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Windsurf',
|
|
113
|
+
path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
114
|
+
key: 'mcpServers',
|
|
115
|
+
entry: stdioEntry,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'VS Code (user)',
|
|
119
|
+
path: path.join(home, '.vscode', 'mcp.json'),
|
|
120
|
+
key: 'servers',
|
|
121
|
+
entry: stdioEntry,
|
|
122
|
+
wrapKey: null, // VS Code uses { servers: { ... } } at top level
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'OpenCode',
|
|
126
|
+
path: path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
127
|
+
key: 'mcp',
|
|
128
|
+
entry: scriptPath
|
|
129
|
+
? { type: 'local', command: [nodePath, scriptPath, 'start'], enabled: true }
|
|
130
|
+
: { type: 'local', command: [resolveNpxPath(), '-y', '@createlex/figma-swiftui-mcp', 'start'], enabled: true },
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'Codex CLI',
|
|
134
|
+
path: path.join(home, '.codex', 'config.toml'),
|
|
135
|
+
format: 'toml',
|
|
136
|
+
entry: { command: stdioEntry.command, args: stdioEntry.args },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Gemini CLI',
|
|
140
|
+
path: path.join(home, '.gemini', 'settings.json'),
|
|
141
|
+
key: 'mcpServers',
|
|
142
|
+
entry: stdioEntry,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Antigravity (Gemini)',
|
|
146
|
+
path: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
|
|
147
|
+
key: 'mcpServers',
|
|
148
|
+
entry: { ...stdioEntry, env: {}, disabled: false },
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Read / write helpers ──────────────────────────────────────────────
|
|
154
|
+
function readJsonSafe(filePath) {
|
|
155
|
+
try {
|
|
156
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
157
|
+
return JSON.parse(raw);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeJsonSafe(filePath, data) {
|
|
164
|
+
const dir = path.dirname(filePath);
|
|
165
|
+
if (!fs.existsSync(dir)) {
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── TOML helpers (minimal, for Codex config.toml) ────────────────────
|
|
172
|
+
// We only need to append/check a [mcp_servers.<name>] section — no full
|
|
173
|
+
// TOML parser required.
|
|
174
|
+
|
|
175
|
+
function tomlHasServer(raw, serverName) {
|
|
176
|
+
const pattern = new RegExp(`^\\[mcp_servers\\.${serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
|
|
177
|
+
return pattern.test(raw);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function tomlFormatValue(v) {
|
|
181
|
+
if (typeof v === 'string') return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
182
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
183
|
+
if (typeof v === 'number') return String(v);
|
|
184
|
+
if (Array.isArray(v)) return `[${v.map(tomlFormatValue).join(', ')}]`;
|
|
185
|
+
return `"${v}"`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildTomlSection(serverName, entry) {
|
|
189
|
+
const lines = [`[mcp_servers.${serverName}]`];
|
|
190
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
191
|
+
if (k === 'env' && typeof v === 'object') continue; // handle env as sub-table
|
|
192
|
+
lines.push(`${k} = ${tomlFormatValue(v)}`);
|
|
193
|
+
}
|
|
194
|
+
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
195
|
+
lines.push(`[mcp_servers.${serverName}.env]`);
|
|
196
|
+
for (const [ek, ev] of Object.entries(entry.env)) {
|
|
197
|
+
lines.push(`${ek} = ${tomlFormatValue(ev)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lines.join('\n') + '\n';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function appendTomlServer(filePath, serverName, entry, dryRun) {
|
|
204
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
205
|
+
if (tomlHasServer(raw, serverName)) return false; // already present
|
|
206
|
+
const section = '\n' + buildTomlSection(serverName, entry);
|
|
207
|
+
if (!dryRun) {
|
|
208
|
+
fs.appendFileSync(filePath, section, 'utf-8');
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
214
|
+
function runSetup(flags = {}) {
|
|
215
|
+
const force = flags.force || false;
|
|
216
|
+
const dryRun = flags.dryRun || false;
|
|
217
|
+
|
|
218
|
+
const paths = resolvePaths();
|
|
219
|
+
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(' 🔧 figma-swiftui-mcp setup');
|
|
222
|
+
console.log(' ─────────────────────────────────────');
|
|
223
|
+
console.log(` Node: ${paths.nodePath}`);
|
|
224
|
+
if (paths.scriptPath) {
|
|
225
|
+
console.log(` Script: ${paths.scriptPath}`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(' Script: not found — will use npx fallback');
|
|
228
|
+
}
|
|
229
|
+
console.log();
|
|
230
|
+
|
|
231
|
+
const targets = getTargets(paths);
|
|
232
|
+
let configured = 0;
|
|
233
|
+
let skipped = 0;
|
|
234
|
+
let notInstalled = 0;
|
|
235
|
+
|
|
236
|
+
for (const target of targets) {
|
|
237
|
+
const exists = fs.existsSync(target.path);
|
|
238
|
+
|
|
239
|
+
if (!exists) {
|
|
240
|
+
console.log(` ⚪ ${target.name} — not installed (${path.basename(target.path)} not found)`);
|
|
241
|
+
notInstalled++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── TOML targets (Codex) ──────────────────────────────────────
|
|
246
|
+
if (target.format === 'toml') {
|
|
247
|
+
const raw = fs.readFileSync(target.path, 'utf-8');
|
|
248
|
+
if (tomlHasServer(raw, MCP_KEY) && !force) {
|
|
249
|
+
console.log(` ✅ ${target.name} — already configured`);
|
|
250
|
+
skipped++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (dryRun) {
|
|
254
|
+
console.log(` 🟡 ${target.name} — would configure (dry run)`);
|
|
255
|
+
} else {
|
|
256
|
+
appendTomlServer(target.path, MCP_KEY, target.entry, false);
|
|
257
|
+
console.log(` ✅ ${target.name} — configured!`);
|
|
258
|
+
}
|
|
259
|
+
configured++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── JSON targets ────────────────────────────────────────────
|
|
264
|
+
const config = readJsonSafe(target.path);
|
|
265
|
+
if (!config) {
|
|
266
|
+
console.log(` ⚠️ ${target.name} — could not parse config`);
|
|
267
|
+
skipped++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Ensure the server container key exists
|
|
272
|
+
if (!config[target.key]) {
|
|
273
|
+
config[target.key] = {};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const servers = config[target.key];
|
|
277
|
+
|
|
278
|
+
// Check if already configured
|
|
279
|
+
if (servers[MCP_KEY] && !force) {
|
|
280
|
+
console.log(` ✅ ${target.name} — already configured`);
|
|
281
|
+
skipped++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Add the entry
|
|
286
|
+
servers[MCP_KEY] = target.entry;
|
|
287
|
+
|
|
288
|
+
if (dryRun) {
|
|
289
|
+
console.log(` 🟡 ${target.name} — would configure (dry run)`);
|
|
290
|
+
} else {
|
|
291
|
+
writeJsonSafe(target.path, config);
|
|
292
|
+
console.log(` ✅ ${target.name} — configured!`);
|
|
293
|
+
}
|
|
294
|
+
configured++;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(` ─────────────────────────────────────`);
|
|
299
|
+
console.log(` ${configured} configured · ${skipped} skipped · ${notInstalled} not installed`);
|
|
300
|
+
|
|
301
|
+
if (configured > 0 && !dryRun) {
|
|
302
|
+
console.log();
|
|
303
|
+
console.log(' 💡 Restart your IDE(s) for the new MCP config to take effect.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { runSetup };
|