@createlex/figma-swiftui-mcp 1.0.5 → 1.0.7

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.
@@ -51,13 +51,50 @@ async function waitForExistingBridge({ host, port }) {
51
51
  throw new Error(`HTTP ${response.status}`);
52
52
  }
53
53
  const data = await response.json();
54
+
55
+ // Verify WebSocket is also healthy, not just HTTP
56
+ await new Promise((resolve, reject) => {
57
+ const testWs = new WebSocket(`ws://${host}:${port}/bridge`);
58
+ const timeout = setTimeout(() => {
59
+ testWs.terminate();
60
+ reject(new Error('WebSocket health check timed out'));
61
+ }, 3000);
62
+ testWs.on('open', () => {
63
+ clearTimeout(timeout);
64
+ testWs.close();
65
+ resolve();
66
+ });
67
+ testWs.on('error', (err) => {
68
+ clearTimeout(timeout);
69
+ reject(new Error(`WebSocket health check failed: ${err.message}`));
70
+ });
71
+ });
72
+
54
73
  return {
55
74
  ok: true,
56
75
  alreadyRunning: true,
57
76
  info: data,
58
77
  };
59
78
  } catch (error) {
60
- throw new Error(`Port ${port} is already in use and does not look like a Figma SwiftUI bridge`);
79
+ throw new Error(`Port ${port} is already in use and does not look like a healthy Figma SwiftUI bridge (${error.message})`);
80
+ }
81
+ }
82
+
83
+ async function killStaleAndRetry({ host, port, logger }) {
84
+ const { execSync } = require('child_process');
85
+ try {
86
+ const lsofOutput = execSync(`lsof -ti :${port}`, { encoding: 'utf8' }).trim();
87
+ const pids = lsofOutput.split('\n').filter(Boolean);
88
+ if (pids.length > 0) {
89
+ logger.warn(`⚠️ Killing ${pids.length} stale process(es) on port ${port}: ${pids.join(', ')}`);
90
+ for (const pid of pids) {
91
+ try { process.kill(Number(pid), 'SIGTERM'); } catch (_) {}
92
+ }
93
+ // Wait for port to free up
94
+ await new Promise((r) => setTimeout(r, 1500));
95
+ }
96
+ } catch (_) {
97
+ // lsof returns non-zero if no matches — port is already free
61
98
  }
62
99
  }
63
100
 
@@ -527,7 +564,7 @@ function startBridgeServer(options = {}) {
527
564
  if (error && error.code === 'EADDRINUSE') {
528
565
  try {
529
566
  const running = await waitForExistingBridge({ host, port });
530
- logger.warn(`ℹ️ Using existing bridge at http://${host}:${port}`);
567
+ logger.warn(`ℹ️ Using existing healthy bridge at http://${host}:${port}`);
531
568
  resolve({
532
569
  app,
533
570
  server: null,
@@ -542,8 +579,47 @@ function startBridgeServer(options = {}) {
542
579
  });
543
580
  return;
544
581
  } catch (existingError) {
545
- reject(existingError);
546
- return;
582
+ // Existing bridge is unhealthy — kill stale processes and start fresh
583
+ logger.warn(`⚠️ ${existingError.message}`);
584
+ try {
585
+ await killStaleAndRetry({ host, port, logger });
586
+ // Re-attempt listen after killing stale processes
587
+ server.listen(port, host, () => {
588
+ logger.info(`🚀 Figma SwiftUI bridge running at http://${host}:${port} (recovered from stale)`);
589
+ if (projectPath) {
590
+ logger.info(`📂 Project path: ${projectPath}`);
591
+ }
592
+ logger.info(`🌉 Bridge ready at ws://${host}:${port}/bridge`);
593
+ resolve({
594
+ app,
595
+ server,
596
+ bridgeWss,
597
+ port,
598
+ host,
599
+ alreadyRunning: false,
600
+ getProjectPath: () => projectPath,
601
+ getBridgeInfo: () => ({
602
+ protocolVersion: BRIDGE_PROTOCOL_VERSION,
603
+ pluginConnected: !!pluginBridgeClient,
604
+ connectedAgents: agentBridgeClients.size,
605
+ pendingRequests: pendingBridgeRequests.size,
606
+ supportedActions: SUPPORTED_BRIDGE_ACTIONS,
607
+ }),
608
+ close: () => new Promise((closeResolve, closeReject) => {
609
+ bridgeWss.close(() => {
610
+ server.close((closeError) => {
611
+ if (closeError) closeReject(closeError);
612
+ else closeResolve();
613
+ });
614
+ });
615
+ }),
616
+ });
617
+ });
618
+ return;
619
+ } catch (retryError) {
620
+ reject(new Error(`Failed to recover bridge: ${retryError.message}`));
621
+ return;
622
+ }
547
623
  }
548
624
  }
549
625
 
@@ -237,7 +237,15 @@ function handleBridgeMessage(rawMessage) {
237
237
  }
238
238
 
239
239
  if (message.type === 'bridge-event') {
240
- console.error(`[figma-swiftui-mcp] Bridge event ${message.event}: ${JSON.stringify(message.data)}`);
240
+ // Log bridge events concisely — skip full JSON payloads for noisy events
241
+ const summary = message.data && message.event === 'selectionchange'
242
+ ? `${message.data.selection?.count ?? 0} node(s) selected`
243
+ : message.data && message.event === 'currentpagechange'
244
+ ? `page "${message.data.currentPage?.name || 'unknown'}"`
245
+ : '';
246
+ if (summary) {
247
+ console.error(`[figma-swiftui-mcp] ${message.event}: ${summary}`);
248
+ }
241
249
  }
242
250
  }
243
251
 
@@ -21,38 +21,68 @@ const os = require('node:os');
21
21
 
22
22
  const MCP_KEY = 'figma-swiftui';
23
23
 
24
- // ── Resolve the absolute path to this binary ──────────────────────────
25
- function resolveBinaryPath() {
26
- // If installed globally via npm, process.argv[1] points at the bin script
24
+ // ── Resolve the absolute path to the bin script and node ─────────────
25
+ // IDEs (Cursor, VS Code, etc.) do NOT source shell profiles, so
26
+ // `#!/usr/bin/env node` fails when node is managed by nvm/fnm/volta.
27
+ // Instead we record the absolute node path and the bin script separately.
28
+ function resolvePaths() {
29
+ const nodePath = process.execPath; // absolute path to node binary
30
+
31
+ // Try to find the bin JS file
27
32
  const binScript = process.argv[1];
28
33
  if (binScript) {
29
34
  const resolved = fs.realpathSync(binScript);
30
35
  const dir = path.dirname(resolved);
36
+ // Check for the .js bin entry point
37
+ const jsCandidate = path.join(dir, 'figma-swiftui-mcp.js');
38
+ if (fs.existsSync(jsCandidate)) {
39
+ return { nodePath, scriptPath: jsCandidate };
40
+ }
41
+ // Check for the wrapper (symlink without .js)
31
42
  const candidate = path.join(dir, 'figma-swiftui-mcp');
32
43
  if (fs.existsSync(candidate)) {
33
- return candidate;
44
+ // Resolve symlinks to get the actual .js file
45
+ const real = fs.realpathSync(candidate);
46
+ return { nodePath, scriptPath: real };
34
47
  }
35
48
  }
36
49
 
37
- // Fallback: search PATH
50
+ // Fallback: search PATH for the binary, then resolve to its .js source
38
51
  const whichCmd = require('node:child_process')
39
52
  .execSync('which figma-swiftui-mcp 2>/dev/null || true')
40
53
  .toString()
41
54
  .trim();
42
- if (whichCmd) return whichCmd;
55
+ if (whichCmd) {
56
+ const real = fs.realpathSync(whichCmd);
57
+ return { nodePath, scriptPath: real };
58
+ }
43
59
 
44
60
  // Last resort: use npx invocation
45
- return null;
61
+ return { nodePath, scriptPath: null };
62
+ }
63
+
64
+ // ── Resolve absolute npx path for fallback ───────────────────────────
65
+ function resolveNpxPath() {
66
+ try {
67
+ const npxPath = require('node:child_process')
68
+ .execSync('which npx 2>/dev/null || true')
69
+ .toString()
70
+ .trim();
71
+ return npxPath || 'npx';
72
+ } catch {
73
+ return 'npx';
74
+ }
46
75
  }
47
76
 
48
77
  // ── IDE config definitions ────────────────────────────────────────────
49
- function getTargets(binaryPath) {
78
+ function getTargets({ nodePath, scriptPath }) {
50
79
  const home = os.homedir();
51
80
 
52
- // The entry we want in every config
53
- const stdioEntry = binaryPath
54
- ? { command: binaryPath, args: ['start'] }
55
- : { command: 'npx', args: ['-y', '@createlex/figma-swiftui-mcp', 'start'] };
81
+ // Use absolute node path + script to avoid #!/usr/bin/env node failures
82
+ // in IDEs that don't source shell profiles (nvm/fnm/volta).
83
+ const stdioEntry = scriptPath
84
+ ? { command: nodePath, args: [scriptPath, 'start'] }
85
+ : { command: resolveNpxPath(), args: ['-y', '@createlex/figma-swiftui-mcp', 'start'] };
56
86
 
57
87
  const stdioEntryWithType = { type: 'stdio', ...stdioEntry };
58
88
 
@@ -120,19 +150,20 @@ function runSetup(flags = {}) {
120
150
  const force = flags.force || false;
121
151
  const dryRun = flags.dryRun || false;
122
152
 
123
- const binaryPath = resolveBinaryPath();
153
+ const paths = resolvePaths();
124
154
 
125
155
  console.log();
126
156
  console.log(' 🔧 figma-swiftui-mcp setup');
127
157
  console.log(' ─────────────────────────────────────');
128
- if (binaryPath) {
129
- console.log(` Binary: ${binaryPath}`);
158
+ console.log(` Node: ${paths.nodePath}`);
159
+ if (paths.scriptPath) {
160
+ console.log(` Script: ${paths.scriptPath}`);
130
161
  } else {
131
- console.log(' Binary: not found — will use npx fallback');
162
+ console.log(' Script: not found — will use npx fallback');
132
163
  }
133
164
  console.log();
134
165
 
135
- const targets = getTargets(binaryPath);
166
+ const targets = getTargets(paths);
136
167
  let configured = 0;
137
168
  let skipped = 0;
138
169
  let notInstalled = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createlex/figma-swiftui-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CreateLex MCP runtime for Figma-to-SwiftUI generation and Xcode export",
5
5
  "bin": {
6
6
  "figma-swiftui-mcp": "bin/figma-swiftui-mcp.js"