figma-rust-mcp 8.0.0 → 8.0.1

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 CHANGED
@@ -65,7 +65,7 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
65
65
  npx -y figma-rust-mcp@latest --init
66
66
  ```
67
67
  The setup detects installed Codex, Claude Code, Antigravity, Cursor, Windsurf,
68
- VS Code/Copilot, and Zed clients. Choose an action from the menu:
68
+ VS Code, Copilot CLI, Gemini CLI, opencode, and Zed clients. Choose an action from the menu:
69
69
 
70
70
  ```text
71
71
  1) Quick setup — plugin + background service + detected agents
@@ -28,7 +28,10 @@ export function detectAgents({ home = os.homedir(), env = process.env, platform
28
28
  { id: 'antigravity', label: 'Google Antigravity', configPath: path.join(home, '.gemini', 'config', 'mcp_config.json'), installed: existsAny([path.join(home, '.antigravity-ide'), path.join(home, '.gemini', 'config', 'mcp_config.json'), platform === 'darwin' ? path.join(support, 'Antigravity') : null, platform === 'win32' ? path.join(local, 'Programs', 'Antigravity') : null, macApp('Antigravity')]) || hasCommand('antigravity', env, platform) },
29
29
  { id: 'cursor', label: 'Cursor', configPath: path.join(home, '.cursor', 'mcp.json'), installed: existsAny([path.join(home, '.cursor'), platform === 'darwin' ? path.join(support, 'Cursor') : null, platform === 'win32' ? path.join(local, 'Programs', 'Cursor') : null, macApp('Cursor')]) || hasCommand('cursor', env, platform) },
30
30
  { id: 'windsurf', label: 'Windsurf', configPath: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), installed: existsAny([path.join(home, '.codeium', 'windsurf'), platform === 'darwin' ? path.join(support, 'Windsurf') : null, platform === 'win32' ? path.join(local, 'Programs', 'Windsurf') : null, macApp('Windsurf')]) || hasCommand('windsurf', env, platform) },
31
- { id: 'vscode', label: 'VS Code / Copilot', configPath: path.join(home, '.copilot', 'mcp-config.json'), installed: existsAny([path.join(home, '.vscode'), path.join(home, '.copilot'), platform === 'darwin' ? path.join(support, 'Code') : null, platform === 'linux' ? path.join(xdg, 'Code') : null, platform === 'win32' ? path.join(local, 'Programs', 'Microsoft VS Code') : null, platform === 'win32' ? path.join(roaming, 'Code') : null, macApp('Visual Studio Code')]) || hasCommand('code', env, platform) },
31
+ { id: 'vscode', label: 'VS Code', configPath: path.join(platform === 'darwin' ? support : platform === 'win32' ? roaming : xdg, 'Code', 'User', 'mcp.json'), installed: existsAny([path.join(home, '.vscode'), platform === 'darwin' ? path.join(support, 'Code') : null, platform === 'linux' ? path.join(xdg, 'Code') : null, platform === 'win32' ? path.join(local, 'Programs', 'Microsoft VS Code') : null, platform === 'win32' ? path.join(roaming, 'Code') : null, macApp('Visual Studio Code')]) || hasCommand('code', env, platform) },
32
+ { id: 'copilot', label: 'Copilot CLI', configPath: path.join(env.COPILOT_HOME || path.join(home, '.copilot'), 'mcp-config.json'), installed: existsAny([env.COPILOT_HOME, path.join(home, '.copilot')]) || hasCommand('copilot', env, platform) },
33
+ { id: 'gemini', label: 'Gemini CLI', configPath: path.join(home, '.gemini', 'settings.json'), installed: hasCommand('gemini', env, platform) },
34
+ { id: 'opencode', label: 'opencode', configPath: path.join(xdg, 'opencode', 'opencode.json'), installed: existsAny([path.join(xdg, 'opencode'), path.join(home, '.opencode')]) || hasCommand('opencode', env, platform) },
32
35
  { id: 'zed', label: 'Zed', configPath: platform === 'darwin' ? path.join(support, 'Zed', 'settings.json') : platform === 'win32' ? path.join(roaming, 'Zed', 'settings.json') : path.join(xdg, 'zed', 'settings.json'), installed: existsAny([path.join(xdg, 'zed'), platform === 'darwin' ? path.join(support, 'Zed') : null, platform === 'win32' ? path.join(roaming, 'Zed') : null, macApp('Zed')]) || hasCommand('zed', env, platform) },
33
36
  ];
34
37
  return agents.filter(agent => agent.installed).map(({ installed, ...agent }) => agent);
@@ -40,9 +43,9 @@ function upsertJsonc(filePath, rootKey, value) {
40
43
  const config = parse(source, errors, { allowTrailingComma: true, disallowComments: false });
41
44
  if (errors.length) throw new Error(`Invalid JSON/JSONC in ${filePath}; left it unchanged.`);
42
45
  const current = config?.[rootKey]?.[SERVER_NAME];
43
- if (current && Object.entries(value).every(([key, entryValue]) => current[key] === entryValue)) return false;
46
+ if (current && Object.entries(value).every(([key, entryValue]) => JSON.stringify(current[key]) === JSON.stringify(entryValue))) return false;
44
47
  const merged = current && typeof current === 'object' && !Array.isArray(current) ? { ...current, ...value } : value;
45
- for (const key of ['command', 'args', 'serverUrl', 'url', 'type']) {
48
+ for (const key of ['command', 'args', 'serverUrl', 'httpUrl', 'url', 'type']) {
46
49
  if (!Object.hasOwn(value, key) && Object.hasOwn(merged, key)) delete merged[key];
47
50
  }
48
51
  const edits = modify(source, [rootKey, SERVER_NAME], merged, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } });
@@ -76,14 +79,20 @@ function upsertCodex(filePath) {
76
79
  return true;
77
80
  }
78
81
 
82
+ const ENTRIES = {
83
+ antigravity: ['mcpServers', { serverUrl: 'http://127.0.0.1:41730/sse' }],
84
+ claude: ['mcpServers', { type: 'http', url: HTTP_URL }],
85
+ windsurf: ['mcpServers', { serverUrl: HTTP_URL }],
86
+ vscode: ['servers', { type: 'http', url: HTTP_URL }],
87
+ copilot: ['mcpServers', { type: 'http', url: HTTP_URL, tools: ['*'] }],
88
+ gemini: ['mcpServers', { httpUrl: HTTP_URL }],
89
+ opencode: ['mcp', { type: 'remote', url: HTTP_URL, enabled: true }],
90
+ zed: ['context_servers', { url: HTTP_URL }],
91
+ };
92
+
79
93
  export function configureAgent(agent) {
80
- const entry = agent.id === 'antigravity'
81
- ? { serverUrl: 'http://127.0.0.1:41730/sse' }
82
- : agent.id === 'claude' || agent.id === 'vscode'
83
- ? { type: 'http', url: HTTP_URL }
84
- : { url: HTTP_URL };
85
94
  if (agent.id === 'codex') return upsertCodex(agent.configPath);
86
- const rootKey = agent.id === 'zed' ? 'context_servers' : 'mcpServers';
95
+ const [rootKey, entry] = ENTRIES[agent.id] || ['mcpServers', { url: HTTP_URL }];
87
96
  return upsertJsonc(agent.configPath, rootKey, entry);
88
97
  }
89
98
 
@@ -40,6 +40,17 @@ try {
40
40
  } catch (_) {}
41
41
  if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
42
42
 
43
+ // ─── TERMINAL UI ──────────────────────────────────────────────────────────────
44
+
45
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
46
+ const paint = code => text => useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text);
47
+ const bold = paint('1'), dim = paint('2'), green = paint('32'), yellow = paint('33'), red = paint('31'), cyan = paint('36');
48
+ const tag = code => process.stderr.isTTY && !process.env.NO_COLOR ? `\x1b[${code}m[figma-rust-mcp]\x1b[0m` : '[figma-rust-mcp]';
49
+ const tilde = p => p.startsWith(os.homedir()) ? `~${p.slice(os.homedir().length)}` : p;
50
+ const ok = (msg, detail = '') => console.log(` ${green('✓')} ${msg}${detail ? ` ${dim(detail)}` : ''}`);
51
+ const note = msg => console.log(` ${dim(msg)}`);
52
+ const step = (n, total, title) => console.log(`\n${cyan(`[${n}/${total}]`)} ${bold(title)}`);
53
+
43
54
  function getPlatformArch() {
44
55
  const platform = process.platform;
45
56
  const arch = process.arch;
@@ -83,6 +94,27 @@ function getCacheDir(version) {
83
94
  return path.join(baseCache, 'figma-rust-mcp', `v${version}`);
84
95
  }
85
96
 
97
+ // Progress bar on stderr (stdout carries MCP stdio traffic); silent when stderr is not a terminal.
98
+ function downloadProgress(total) {
99
+ const tty = process.stderr.isTTY;
100
+ const started = Date.now();
101
+ let received = 0, lastDraw = 0;
102
+ const mb = bytes => (bytes / 1048576).toFixed(1);
103
+ const draw = () => {
104
+ const speed = `${mb(received / Math.max((Date.now() - started) / 1000, 0.001))} MB/s`;
105
+ if (!total) return process.stderr.write(`\r\x1b[2K ↓ ${mb(received)} MB ${speed}`);
106
+ const ratio = Math.min(received / total, 1), width = 24, filled = Math.round(ratio * width);
107
+ process.stderr.write(`\r\x1b[2K ↓ \x1b[36m${'█'.repeat(filled)}\x1b[2m${'░'.repeat(width - filled)}\x1b[0m ${String(Math.round(ratio * 100)).padStart(3)}% ${mb(received)}/${mb(total)} MB ${speed}`);
108
+ };
109
+ return {
110
+ add(bytes) {
111
+ received += bytes;
112
+ if (tty && Date.now() - lastDraw > 80) { lastDraw = Date.now(); draw(); }
113
+ },
114
+ done() { if (tty) { draw(); process.stderr.write('\n'); } },
115
+ };
116
+ }
117
+
86
118
  function downloadFile(url, destPath) {
87
119
  return new Promise((resolve, reject) => {
88
120
  const followRedirects = (currentUrl, redirectCount = 0) => {
@@ -104,9 +136,12 @@ function downloadFile(url, destPath) {
104
136
  }
105
137
 
106
138
  const fileStream = fs.createWriteStream(destPath);
139
+ const progress = downloadProgress(Number(res.headers['content-length']) || 0);
140
+ res.on('data', chunk => progress.add(chunk.length));
107
141
  res.pipe(fileStream);
108
142
 
109
143
  fileStream.on('finish', () => {
144
+ progress.done();
110
145
  fileStream.close(() => resolve());
111
146
  });
112
147
 
@@ -147,8 +182,12 @@ async function findOrDownloadBinary() {
147
182
  const localRelease = path.resolve(__dirname, '../target/release/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
148
183
  const localDebug = path.resolve(__dirname, '../target/debug/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
149
184
 
150
- if (fs.existsSync(localRelease)) return localRelease;
151
- if (fs.existsSync(localDebug)) return localDebug;
185
+ // A stale local build would silently downgrade the service; only use one built from this version.
186
+ const isCurrentBuild = bin => {
187
+ try { return fs.existsSync(bin) && execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 5000 }).trim().endsWith(` ${pkgVersion}`); }
188
+ catch (_) { return false; }
189
+ };
190
+ for (const bin of [localRelease, localDebug]) if (isCurrentBuild(bin)) return bin;
152
191
 
153
192
  const info = getPlatformArch();
154
193
  const cacheDir = getCacheDir(pkgVersion);
@@ -167,12 +206,12 @@ async function findOrDownloadBinary() {
167
206
  const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
168
207
  const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
169
208
 
170
- process.stderr.write(`\x1b[36m[figma-rust-mcp]\x1b[0m Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
209
+ process.stderr.write(`${tag(36)} Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
171
210
 
172
211
  try {
173
212
  await downloadFile(releaseUrl, archivePath);
174
213
  } catch (err) {
175
- process.stderr.write(`\x1b[33m[figma-rust-mcp]\x1b[0m Release v${pkgVersion} not found, trying latest release...\n`);
214
+ process.stderr.write(`${tag(33)} Release v${pkgVersion} not found, trying latest release...\n`);
176
215
  try {
177
216
  await downloadFile(latestUrl, archivePath);
178
217
  } catch (fallbackErr) {
@@ -201,7 +240,7 @@ async function findOrDownloadBinary() {
201
240
  fs.chmodSync(cachedBin, 0o755);
202
241
  }
203
242
 
204
- process.stderr.write(`\x1b[32m[figma-rust-mcp]\x1b[0m Ready! (cached in ${cacheDir})\n\n`);
243
+ process.stderr.write(`${tag(32)} Downloaded to ${tilde(cacheDir)}\n`);
205
244
  return cachedBin;
206
245
  }
207
246
 
@@ -215,6 +254,7 @@ async function findOrDownloadBinary() {
215
254
  */
216
255
  async function installService(binPath) {
217
256
  removeLegacyService();
257
+ await stopRunningServer();
218
258
  const platform = process.platform;
219
259
 
220
260
  if (platform === 'darwin') {
@@ -253,15 +293,9 @@ async function installService(binPath) {
253
293
 
254
294
  fs.writeFileSync(plistPath, plist, 'utf8');
255
295
 
256
- // Unload first (ignore error if not loaded)
257
- try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
258
- execSync(`launchctl load -w "${plistPath}"`, { stdio: 'inherit' });
296
+ execSync(`launchctl load -w "${plistPath}"`, { stdio: 'pipe' });
259
297
 
260
- console.log(`\x1b[32m✓ figma-rust-mcp service installed!\x1b[0m`);
261
- console.log(` Plist: ${plistPath}`);
262
- console.log(` Logs: ${logDir}/stdout.log`);
263
- console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
264
- console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
298
+ await serviceInstalled(`Logs ${tilde(path.join(logDir, 'stdout.log'))}`);
265
299
 
266
300
  } else if (platform === 'linux') {
267
301
  const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
@@ -284,13 +318,10 @@ WantedBy=default.target
284
318
  `;
285
319
 
286
320
  fs.writeFileSync(servicePath, unit, 'utf8');
287
- execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' });
288
- execSync(`systemctl --user enable --now figma-rust-mcp`, { stdio: 'inherit' });
321
+ execSync(`systemctl --user daemon-reload`, { stdio: 'pipe' });
322
+ execSync(`systemctl --user enable --now figma-rust-mcp`, { stdio: 'pipe' });
289
323
 
290
- console.log(`\x1b[32m✓ figma-rust-mcp systemd user service installed!\x1b[0m`);
291
- console.log(` Service: ${servicePath}`);
292
- console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
293
- console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
324
+ await serviceInstalled('Logs: journalctl --user -u figma-rust-mcp');
294
325
 
295
326
  } else if (platform === 'win32') {
296
327
  const taskName = 'FigmaRustMCPServer';
@@ -298,15 +329,12 @@ WantedBy=default.target
298
329
  // The native launcher detaches the server and redirects output to a log file.
299
330
  const taskCommand = `"${binPath}" --background`;
300
331
  execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
301
- '/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
332
+ '/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'pipe', windowsHide: true });
302
333
 
303
334
  // Start it immediately too
304
335
  try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
305
336
 
306
- console.log(`\x1b[32m✓ figma-rust-mcp Windows Task installed!\x1b[0m`);
307
- console.log(` Task: ${taskName}`);
308
- console.log(`\n\x1b[36mThe server will now start automatically on every login.\x1b[0m`);
309
- console.log(`\x1b[36mTo uninstall: npx figma-rust-mcp --uninstall-service\x1b[0m`);
337
+ await serviceInstalled(`Task Scheduler: ${taskName}`);
310
338
 
311
339
  } else {
312
340
  console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
@@ -314,6 +342,55 @@ WantedBy=default.target
314
342
  }
315
343
  }
316
344
 
345
+ async function serviceInstalled(where) {
346
+ const port = Number(process.env.FIGMA_RUST_MCP_PORT) || 41730;
347
+ const deadline = Date.now() + 10000;
348
+ while (!(await isOldServerUp(port)) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 300));
349
+ if (await isOldServerUp(port)) ok('Background service running', `http://127.0.0.1:${port}`);
350
+ else console.log(` ${yellow('!')} Service installed but not answering on port ${port} yet`);
351
+ note(`Starts on login · ${where}`);
352
+ }
353
+
354
+ function isOldServerUp(port) {
355
+ return new Promise(resolve => {
356
+ const req = http.get({ host: '127.0.0.1', port, path: '/plugin/version', timeout: 500 }, res => {
357
+ let body = '';
358
+ res.on('data', chunk => { body += chunk; });
359
+ res.on('end', () => { try { resolve(JSON.parse(body).name === 'figma-rust-mcp'); } catch (_) { resolve(false); } });
360
+ });
361
+ req.on('error', () => resolve(false));
362
+ req.on('timeout', () => { req.destroy(); resolve(false); });
363
+ });
364
+ }
365
+
366
+ /**
367
+ * Stop the installed service and any other running server before installing the new one.
368
+ * A still-running old version keeps port 41730, so the new one would move to 41731 while
369
+ * agents stay pointed at the old server.
370
+ */
371
+ async function stopRunningServer() {
372
+ const quiet = cmd => { try { execSync(cmd, { stdio: 'pipe', windowsHide: true }); } catch (_) {} };
373
+ if (process.platform === 'darwin') {
374
+ quiet(`launchctl unload "${path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-rust-mcp.server.plist')}"`);
375
+ quiet('pkill -x figma-rust-mcp');
376
+ } else if (process.platform === 'linux') {
377
+ quiet('systemctl --user stop figma-rust-mcp');
378
+ quiet('pkill -x figma-rust-mcp');
379
+ } else if (process.platform === 'win32') {
380
+ quiet('schtasks /End /TN "FigmaRustMCPServer"');
381
+ quiet('taskkill /F /T /IM figma-rust-mcp.exe');
382
+ }
383
+ const port = Number(process.env.FIGMA_RUST_MCP_PORT) || 41730;
384
+ const deadline = Date.now() + 10000;
385
+ while (await isOldServerUp(port)) {
386
+ if (Date.now() > deadline) {
387
+ console.log(` ${yellow('!')} An old figma-rust-mcp still answers on port ${port}; stop it, then rerun setup.`);
388
+ return;
389
+ }
390
+ await new Promise(resolve => setTimeout(resolve, 300));
391
+ }
392
+ }
393
+
317
394
  /**
318
395
  * Uninstall the background service.
319
396
  */
@@ -331,7 +408,7 @@ async function uninstallService() {
331
408
  }
332
409
  removeLegacyService();
333
410
 
334
- console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
411
+ ok('Background service removed');
335
412
 
336
413
  } else if (platform === 'linux') {
337
414
  try { execSync(`systemctl --user disable --now figma-rust-mcp`, { stdio: 'inherit' }); } catch (_) {}
@@ -340,14 +417,14 @@ async function uninstallService() {
340
417
  try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
341
418
  removeLegacyService();
342
419
 
343
- console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
420
+ ok('Background service removed');
344
421
 
345
422
  } else if (platform === 'win32') {
346
423
  const taskName = 'FigmaRustMCPServer';
347
424
  try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
348
425
  removeLegacyService();
349
426
 
350
- console.log(`\x1b[32m✓ figma-rust-mcp Windows Task removed.\x1b[0m`);
427
+ ok('Background service removed');
351
428
 
352
429
  } else {
353
430
  console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
@@ -415,13 +492,9 @@ function setupPlugin(customDir) {
415
492
  }
416
493
 
417
494
  const manifestPath = path.join(pluginDir, 'manifest.json');
418
- console.log(`\n\x1b[32m✓ Figma Rust MCP Dynamic Thin Plugin installed to:\x1b[0m`);
419
- console.log(` \x1b[36m${manifestPath}\x1b[0m\n`);
420
- console.log(`\x1b[1mTo connect Figma (Do this ONCE forever):\x1b[0m`);
421
- console.log(` 1. Open Figma Desktop`);
422
- console.log(` 2. Go to Plugins → Development → Import plugin from manifest...`);
423
- console.log(` 3. Select: ${manifestPath}`);
424
- console.log(` 4. Done! All future updates load dynamically from figma-rust-mcp without file re-imports.\n`);
495
+ ok('Figma plugin installed', tilde(manifestPath));
496
+ note('First time only: Figma Desktop → Plugins → Development → Import plugin from manifest…');
497
+ note('Later updates load automatically; no re-import needed.');
425
498
  return manifestPath;
426
499
  }
427
500
 
@@ -490,7 +563,7 @@ async function upgrade() {
490
563
  try {
491
564
  const major = pkgVersion ? pkgVersion.split('.')[0] : 'latest';
492
565
  execSync(`npm install -g figma-rust-mcp@${major}`, { stdio: 'inherit' });
493
- console.log(`\x1b[32m✓ Upgraded figma-rust-mcp (${major === 'latest' ? 'latest' : `v${major}.x`}).\x1b[0m`);
566
+ ok(`Upgraded figma-rust-mcp (${major === 'latest' ? 'latest' : `v${major}.x`})`);
494
567
  setupPlugin();
495
568
  if (isServiceInstalled()) {
496
569
  console.log('\x1b[36mUpdating background service binary...\x1b[0m');
@@ -510,25 +583,42 @@ async function setupMenu() {
510
583
 
511
584
  const rl = createInterface({ input: process.stdin, output: process.stdout });
512
585
  const agents = detectAgents();
586
+ const items = [
587
+ ['1', 'Quick setup', 'plugin + background service + agents', true],
588
+ ['2', 'Figma plugin', 'install or update only'],
589
+ ['3', 'Run server', 'in this terminal'],
590
+ ['4', 'Agents', `configure detected agents (${agents.length} found)`],
591
+ ['5', 'Service status', 'is the background service installed?'],
592
+ ['6', 'Upgrade', 'update package and refresh setup'],
593
+ ['7', 'Remove service', 'stop and uninstall the background service'],
594
+ ['0', 'Exit', ''],
595
+ ];
513
596
  try {
514
- console.log(`\n\x1b[1mFigma Rust MCP setup\x1b[0m v${pkgVersion}\n\n 1) Quick setup — plugin + background service + detected agents\n 2) Install/update Figma plugin only\n 3) Start server in this terminal\n 4) Configure detected agents\n 5) Check background service\n 6) Upgrade package and refresh setup\n 7) Remove background service\n 0) Exit\n`);
515
- const choice = (await rl.question('Choose an option [1]: ')).trim() || '1';
597
+ console.log(`\n ${bold('Figma Rust MCP')} ${dim(`v${pkgVersion}`)}\n`);
598
+ for (const [key, label, hint, recommended] of items) {
599
+ console.log(` ${cyan(key)} ${label.padEnd(16)}${dim(hint)}${recommended ? ` ${green('(recommended)')}` : ''}`);
600
+ }
601
+ const choice = (await rl.question(`\n${cyan('›')} Choose [1]: `)).trim() || '1';
516
602
  switch (choice) {
517
603
  case '1': {
604
+ step(1, 3, 'Figma plugin');
518
605
  setupPlugin();
606
+ step(2, 3, 'Background service');
519
607
  const binPath = await findOrDownloadBinary();
520
608
  await installService(binPath);
609
+ step(3, 3, 'AI agents');
521
610
  await configureDetectedAgents(rl, agents);
611
+ console.log(`\n${green('✓')} ${bold('Setup complete.')} Restart your agents, then run the plugin in Figma.\n`);
522
612
  break;
523
613
  }
524
614
  case '2': setupPlugin(); break;
525
615
  case '3': await runBinary(['--server']); break;
526
- case '4': await configureDetectedAgents(rl, agents); break;
527
- case '5': console.log(isServiceInstalled() ? '✓ Background service is installed.' : '✗ Background service is not installed.'); break;
616
+ case '4': await configureDetectedAgents(rl, agents); note('Restart the configured agents to connect.'); break;
617
+ case '5': isServiceInstalled() ? ok('Background service is installed') : console.log(` ${yellow('!')} Background service is not installed`); break;
528
618
  case '6': await upgrade(); break;
529
619
  case '7': await uninstallService(); break;
530
620
  case '0': break;
531
- default: console.log('Unknown option. Run --init to try again.');
621
+ default: console.log(` ${yellow('!')} Unknown option. Run --init to try again.`);
532
622
  }
533
623
  } finally {
534
624
  rl.close();
@@ -537,33 +627,34 @@ async function setupMenu() {
537
627
 
538
628
  async function configureDetectedAgents(rl, agents) {
539
629
  if (!agents.length) {
540
- console.log('No supported AI coding agents were detected. Install an agent and rerun --init.');
630
+ console.log(` ${yellow('!')} No supported AI agents detected. Install one and rerun --init.`);
541
631
  return;
542
632
  }
543
633
 
544
- console.log('\nDetected agents:');
545
- agents.forEach((agent, index) => console.log(` ${index + 1}) ${agent.label} — ${agent.configPath}`));
546
- const answer = (await rl.question('Configure which agents? Enter numbers separated by commas, a=all, Enter=skip [a]: ')).trim().toLowerCase();
547
- if (answer === '') return;
634
+ const width = Math.max(...agents.map(agent => agent.label.length));
635
+ agents.forEach((agent, index) => console.log(` ${cyan(index + 1)} ${agent.label.padEnd(width + 2)}${dim(tilde(agent.configPath))}`));
636
+ const answer = (await rl.question(`\n${cyan('›')} Configure which? ${dim('numbers like 1,3 · a = all · n = skip')} [a]: `)).trim().toLowerCase() || 'a';
637
+ if (answer === 'n') {
638
+ note('Skipped; MCP configuration was not changed.');
639
+ return;
640
+ }
548
641
  const selected = answer === 'a'
549
642
  ? agents
550
643
  : [...new Set(answer.split(',').map(value => Number(value.trim()) - 1))]
551
644
  .filter(index => Number.isInteger(index) && index >= 0 && index < agents.length)
552
645
  .map(index => agents[index]);
553
646
  if (!selected.length) {
554
- console.log('No valid agent selection; MCP configuration was not changed.');
647
+ console.log(` ${yellow('!')} No valid selection; MCP configuration was not changed.`);
555
648
  return;
556
649
  }
557
650
 
558
651
  for (const agent of selected) {
559
652
  try {
560
- const changed = configureAgent(agent);
561
- console.log(` ${changed ? '✓ Configured' : '✓ Already configured'} ${agent.label}`);
653
+ ok(agent.label.padEnd(width + 2), configureAgent(agent) ? 'configured' : 'already configured');
562
654
  } catch (err) {
563
- console.error(` ✗ Could not configure ${agent.label}: ${err.message}`);
655
+ console.log(` ${red('✗')} ${agent.label.padEnd(width + 2)}${dim(err.message)}`);
564
656
  }
565
657
  }
566
- console.log('Restart or reload the selected agent to connect it to Figma Rust MCP.');
567
658
  }
568
659
 
569
660
  function runBinary(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-rust-mcp",
3
- "version": "8.0.0",
3
+ "version": "8.0.1",
4
4
  "description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
5
5
  "mcpName": "io.github.BuiHung1612/figma-rust-mcp",
6
6
  "type": "module",
@@ -289,21 +289,33 @@ test('setup command prints scriptable choices when no terminal is attached', ()
289
289
  test('agent setup detects installed clients and preserves existing JSONC and TOML config', () => {
290
290
  const home = mkdtempSync(path.join(os.tmpdir(), 'figma-rust-mcp-'));
291
291
  try {
292
- const env = { PATH: '' };
293
- for (const dir of ['.codex', '.claude', '.antigravity-ide', '.cursor', '.codeium/windsurf', '.copilot', '.config/zed']) {
292
+ const bin = path.join(home, 'bin');
293
+ mkdirSync(bin);
294
+ writeFileSync(path.join(bin, 'gemini'), '');
295
+ const env = { PATH: bin };
296
+ for (const dir of ['.codex', '.claude', '.antigravity-ide', '.cursor', '.codeium/windsurf', '.config/Code', '.copilot', '.config/opencode', '.config/zed']) {
294
297
  mkdirSync(path.join(home, dir), { recursive: true });
295
298
  }
296
299
  const agents = detectAgents({ home, env, platform: 'linux' });
297
- assert.deepEqual(agents.map(agent => agent.id), ['codex', 'claude', 'antigravity', 'cursor', 'windsurf', 'vscode', 'zed']);
298
-
299
- for (const id of ['claude', 'antigravity', 'windsurf', 'vscode']) {
300
+ assert.deepEqual(agents.map(agent => agent.id), ['codex', 'claude', 'antigravity', 'cursor', 'windsurf', 'vscode', 'copilot', 'gemini', 'opencode', 'zed']);
301
+
302
+ const expected = {
303
+ claude: ['mcpServers', 'url', 'http://127.0.0.1:41730/mcp'],
304
+ antigravity: ['mcpServers', 'serverUrl', 'http://127.0.0.1:41730/sse'],
305
+ windsurf: ['mcpServers', 'serverUrl', 'http://127.0.0.1:41730/mcp'],
306
+ vscode: ['servers', 'url', 'http://127.0.0.1:41730/mcp'],
307
+ copilot: ['mcpServers', 'url', 'http://127.0.0.1:41730/mcp'],
308
+ gemini: ['mcpServers', 'httpUrl', 'http://127.0.0.1:41730/mcp'],
309
+ opencode: ['mcp', 'url', 'http://127.0.0.1:41730/mcp'],
310
+ };
311
+ for (const [id, [rootKey, key, value]] of Object.entries(expected)) {
300
312
  const agent = agents.find(candidate => candidate.id === id);
301
- configureAgent(agent);
302
- const config = parseJsonc(readFileSync(agent.configPath, 'utf8')).mcpServers['figma-rust-mcp'];
303
- assert.ok(config);
304
- if (id === 'antigravity') assert.equal(config.serverUrl, 'http://127.0.0.1:41730/sse');
305
- else assert.equal(config.url, 'http://127.0.0.1:41730/mcp');
313
+ assert.equal(configureAgent(agent), true);
314
+ const config = parseJsonc(readFileSync(agent.configPath, 'utf8'))[rootKey]['figma-rust-mcp'];
315
+ assert.equal(config[key], value);
316
+ assert.equal(configureAgent(agent), false);
306
317
  }
318
+ assert.equal(agents.find(agent => agent.id === 'vscode').configPath, path.join(home, '.config', 'Code', 'User', 'mcp.json'));
307
319
 
308
320
  const cursor = agents.find(agent => agent.id === 'cursor');
309
321
  writeFileSync(cursor.configPath, '{\n // keep this note\n "mcpServers": { "other": { "url": "http://example.test" } }\n}\n');