figma-rust-mcp 3.2.12 → 4.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 +21 -21
- package/README.md +436 -404
- package/bin/agent-config.js +90 -0
- package/bin/{figma-mcp.js → figma-rust-mcp.js} +668 -591
- package/package.json +64 -62
- package/plugin/code.js +52 -52
- package/plugin/manifest.json +25 -25
- package/plugin/ui.html +211 -211
- package/scripts/build-plugin.js +53 -52
- package/scripts/test-bridge-sessions.mjs +160 -155
- package/scripts/test-plugin-tokens.mjs +537 -400
- package/scripts/test-rust-mcp.sh +38 -38
|
@@ -1,591 +1,668 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Figma MCP - NPX Binary Runner
|
|
5
|
-
* Automatically detects platform/arch, downloads prebuilt Rust binary from GitHub Releases,
|
|
6
|
-
* caches it locally, and executes it transparently.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import os from 'node:os';
|
|
13
|
-
import https from 'node:https';
|
|
14
|
-
import http from 'node:http';
|
|
15
|
-
import { fileURLToPath } from 'node:url';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
baseCache = path.join(os.homedir(), '
|
|
77
|
-
} else {
|
|
78
|
-
baseCache =
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
* -
|
|
212
|
-
* -
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
<
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
<string
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
<
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
<
|
|
248
|
-
<
|
|
249
|
-
<
|
|
250
|
-
</
|
|
251
|
-
</
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
execSync(`launchctl
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(`
|
|
262
|
-
console.log(
|
|
263
|
-
console.log(`\x1b[
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
execSync(`systemctl --user
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
console.log(
|
|
291
|
-
console.log(
|
|
292
|
-
console.log(`\x1b[
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
console.log(
|
|
307
|
-
console.log(
|
|
308
|
-
console.log(`\x1b[
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
344
|
-
|
|
345
|
-
} else if (platform === 'win32') {
|
|
346
|
-
const taskName = '
|
|
347
|
-
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.log(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
child.on('
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Figma Rust MCP - NPX Binary Runner
|
|
5
|
+
* Automatically detects platform/arch, downloads prebuilt Rust binary from GitHub Releases,
|
|
6
|
+
* caches it locally, and executes it transparently.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createInterface } from 'node:readline/promises';
|
|
17
|
+
import { configureAgent, detectAgents } from './agent-config.js';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Read package.json version
|
|
23
|
+
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
24
|
+
let pkgVersion = null;
|
|
25
|
+
let repoOwner = 'BuiHung1612';
|
|
26
|
+
let repoName = 'figma-rust-mcp';
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(pkgPath)) {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
31
|
+
if (pkg.version) pkgVersion = pkg.version;
|
|
32
|
+
if (pkg.repository && typeof pkg.repository.url === 'string') {
|
|
33
|
+
const m = pkg.repository.url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
34
|
+
if (m) {
|
|
35
|
+
repoOwner = m[1];
|
|
36
|
+
repoName = m[2];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
42
|
+
|
|
43
|
+
function getPlatformArch() {
|
|
44
|
+
const platform = process.platform;
|
|
45
|
+
const arch = process.arch;
|
|
46
|
+
|
|
47
|
+
let osName = '';
|
|
48
|
+
let archName = '';
|
|
49
|
+
let ext = 'tar.gz';
|
|
50
|
+
let binName = 'figma-rust-mcp';
|
|
51
|
+
|
|
52
|
+
if (platform === 'darwin') {
|
|
53
|
+
osName = 'macos';
|
|
54
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
55
|
+
} else if (platform === 'linux') {
|
|
56
|
+
osName = 'linux';
|
|
57
|
+
archName = arch === 'arm64' ? 'arm64' : 'x86_64';
|
|
58
|
+
} else if (platform === 'win32') {
|
|
59
|
+
osName = 'windows';
|
|
60
|
+
archName = 'x86_64';
|
|
61
|
+
ext = 'zip';
|
|
62
|
+
binName = 'figma-rust-mcp.exe';
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Unsupported operating system: ${platform}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const assetName = `figma-rust-mcp-${osName}-${archName}.${ext}`;
|
|
68
|
+
return { osName, archName, ext, binName, assetName };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getCacheDir(version) {
|
|
72
|
+
const platform = process.platform;
|
|
73
|
+
let baseCache = '';
|
|
74
|
+
|
|
75
|
+
if (platform === 'win32') {
|
|
76
|
+
baseCache = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
77
|
+
} else if (platform === 'darwin') {
|
|
78
|
+
baseCache = path.join(os.homedir(), 'Library', 'Caches');
|
|
79
|
+
} else {
|
|
80
|
+
baseCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return path.join(baseCache, 'figma-rust-mcp', `v${version}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function downloadFile(url, destPath) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const followRedirects = (currentUrl, redirectCount = 0) => {
|
|
89
|
+
if (redirectCount > 10) {
|
|
90
|
+
reject(new Error('Too many redirects while downloading binary'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const client = currentUrl.startsWith('https') ? https : http;
|
|
95
|
+
client.get(currentUrl, { headers: { 'User-Agent': 'figma-rust-mcp-npx' } }, (res) => {
|
|
96
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
97
|
+
followRedirects(res.headers.location, redirectCount + 1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (res.statusCode !== 200) {
|
|
102
|
+
reject(new Error(`Failed to download binary: HTTP ${res.statusCode} from ${currentUrl}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
107
|
+
res.pipe(fileStream);
|
|
108
|
+
|
|
109
|
+
fileStream.on('finish', () => {
|
|
110
|
+
fileStream.close(() => resolve());
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
fileStream.on('error', (err) => {
|
|
114
|
+
fs.unlink(destPath, () => {});
|
|
115
|
+
reject(err);
|
|
116
|
+
});
|
|
117
|
+
}).on('error', (err) => {
|
|
118
|
+
reject(err);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
followRedirects(url);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractArchive(archivePath, targetDir, ext) {
|
|
127
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
if (ext === 'zip') {
|
|
130
|
+
try {
|
|
131
|
+
execSync(`tar -xf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
132
|
+
return;
|
|
133
|
+
} catch (_) {
|
|
134
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${targetDir}' -Force"`, { stdio: 'pipe' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, { stdio: 'pipe' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function findOrDownloadBinary() {
|
|
143
|
+
if (process.env.FIGMA_RUST_MCP_BIN && fs.existsSync(process.env.FIGMA_RUST_MCP_BIN)) {
|
|
144
|
+
return process.env.FIGMA_RUST_MCP_BIN;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const localRelease = path.resolve(__dirname, '../target/release/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
148
|
+
const localDebug = path.resolve(__dirname, '../target/debug/figma-rust-mcp' + (process.platform === 'win32' ? '.exe' : ''));
|
|
149
|
+
|
|
150
|
+
if (fs.existsSync(localRelease)) return localRelease;
|
|
151
|
+
if (fs.existsSync(localDebug)) return localDebug;
|
|
152
|
+
|
|
153
|
+
const info = getPlatformArch();
|
|
154
|
+
const cacheDir = getCacheDir(pkgVersion);
|
|
155
|
+
const cachedBin = path.join(cacheDir, info.binName);
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(cachedBin)) {
|
|
158
|
+
if (process.platform !== 'win32') {
|
|
159
|
+
try { fs.chmodSync(cachedBin, 0o755); } catch (_) {}
|
|
160
|
+
}
|
|
161
|
+
return cachedBin;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
165
|
+
const archivePath = path.join(cacheDir, info.assetName);
|
|
166
|
+
|
|
167
|
+
const releaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/download/v${pkgVersion}/${info.assetName}`;
|
|
168
|
+
const latestUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest/download/${info.assetName}`;
|
|
169
|
+
|
|
170
|
+
process.stderr.write(`\x1b[36m[figma-rust-mcp]\x1b[0m Downloading binary for ${info.osName}-${info.archName} (v${pkgVersion})...\n`);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await downloadFile(releaseUrl, archivePath);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
process.stderr.write(`\x1b[33m[figma-rust-mcp]\x1b[0m Release v${pkgVersion} not found, trying latest release...\n`);
|
|
176
|
+
try {
|
|
177
|
+
await downloadFile(latestUrl, archivePath);
|
|
178
|
+
} catch (fallbackErr) {
|
|
179
|
+
throw new Error(`Failed to download figma-rust-mcp prebuilt binary:\n - ${err.message}\n - ${fallbackErr.message}\n\nPlease verify your internet connection or build from source: cargo build --release`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
extractArchive(archivePath, cacheDir, info.ext);
|
|
185
|
+
} finally {
|
|
186
|
+
try { fs.unlinkSync(archivePath); } catch (_) {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(cachedBin)) {
|
|
190
|
+
const files = fs.readdirSync(cacheDir, { recursive: true });
|
|
191
|
+
const found = files.find(f => path.basename(f) === info.binName);
|
|
192
|
+
if (found) {
|
|
193
|
+
const foundPath = path.join(cacheDir, found);
|
|
194
|
+
fs.renameSync(foundPath, cachedBin);
|
|
195
|
+
} else {
|
|
196
|
+
throw new Error(`Binary ${info.binName} not found inside downloaded archive`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (process.platform !== 'win32') {
|
|
201
|
+
fs.chmodSync(cachedBin, 0o755);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
process.stderr.write(`\x1b[32m[figma-rust-mcp]\x1b[0m Ready! (cached in ${cacheDir})\n\n`);
|
|
205
|
+
return cachedBin;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── SERVICE MANAGEMENT ───────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Install figma-rust-mcp as a background service that auto-starts on login.
|
|
212
|
+
* - macOS: LaunchAgent plist (~/.config/figma-rust-mcp/launchd.plist → ~/Library/LaunchAgents/)
|
|
213
|
+
* - Linux: systemd user service (~/.config/systemd/user/figma-rust-mcp.service)
|
|
214
|
+
* - Windows: Task Scheduler XML via schtasks
|
|
215
|
+
*/
|
|
216
|
+
async function installService(binPath) {
|
|
217
|
+
removeLegacyService();
|
|
218
|
+
const platform = process.platform;
|
|
219
|
+
|
|
220
|
+
if (platform === 'darwin') {
|
|
221
|
+
const plistLabel = 'io.github.figma-rust-mcp.server';
|
|
222
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
223
|
+
const plistPath = path.join(plistDir, `${plistLabel}.plist`);
|
|
224
|
+
|
|
225
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
const logDir = path.join(os.homedir(), 'Library', 'Logs', 'figma-rust-mcp');
|
|
228
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
229
|
+
|
|
230
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
231
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
232
|
+
<plist version="1.0">
|
|
233
|
+
<dict>
|
|
234
|
+
<key>Label</key>
|
|
235
|
+
<string>${plistLabel}</string>
|
|
236
|
+
<key>ProgramArguments</key>
|
|
237
|
+
<array>
|
|
238
|
+
<string>${binPath}</string>
|
|
239
|
+
<string>--server</string>
|
|
240
|
+
</array>
|
|
241
|
+
<key>RunAtLoad</key>
|
|
242
|
+
<true/>
|
|
243
|
+
<key>KeepAlive</key>
|
|
244
|
+
<true/>
|
|
245
|
+
<key>StandardOutPath</key>
|
|
246
|
+
<string>${path.join(logDir, 'stdout.log')}</string>
|
|
247
|
+
<key>StandardErrorPath</key>
|
|
248
|
+
<string>${path.join(logDir, 'stderr.log')}</string>
|
|
249
|
+
<key>ThrottleInterval</key>
|
|
250
|
+
<integer>5</integer>
|
|
251
|
+
</dict>
|
|
252
|
+
</plist>`;
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
255
|
+
|
|
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' });
|
|
259
|
+
|
|
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`);
|
|
265
|
+
|
|
266
|
+
} else if (platform === 'linux') {
|
|
267
|
+
const serviceDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
268
|
+
const servicePath = path.join(serviceDir, 'figma-rust-mcp.service');
|
|
269
|
+
|
|
270
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
271
|
+
|
|
272
|
+
const unit = `[Unit]
|
|
273
|
+
Description=Figma Rust MCP Bridge Server
|
|
274
|
+
After=network.target
|
|
275
|
+
|
|
276
|
+
[Service]
|
|
277
|
+
Type=simple
|
|
278
|
+
ExecStart=${binPath} --server
|
|
279
|
+
Restart=on-failure
|
|
280
|
+
RestartSec=5
|
|
281
|
+
|
|
282
|
+
[Install]
|
|
283
|
+
WantedBy=default.target
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
fs.writeFileSync(servicePath, unit, 'utf8');
|
|
287
|
+
execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' });
|
|
288
|
+
execSync(`systemctl --user enable --now figma-rust-mcp`, { stdio: 'inherit' });
|
|
289
|
+
|
|
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`);
|
|
294
|
+
|
|
295
|
+
} else if (platform === 'win32') {
|
|
296
|
+
const taskName = 'FigmaRustMCPServer';
|
|
297
|
+
// Use schtasks to create a task that runs at login
|
|
298
|
+
// The native launcher detaches the server and redirects output to a log file.
|
|
299
|
+
const taskCommand = `"${binPath}" --background`;
|
|
300
|
+
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
301
|
+
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
|
|
302
|
+
|
|
303
|
+
// Start it immediately too
|
|
304
|
+
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
305
|
+
|
|
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`);
|
|
310
|
+
|
|
311
|
+
} else {
|
|
312
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Uninstall the background service.
|
|
319
|
+
*/
|
|
320
|
+
async function uninstallService() {
|
|
321
|
+
const platform = process.platform;
|
|
322
|
+
|
|
323
|
+
if (platform === 'darwin') {
|
|
324
|
+
const plistLabel = 'io.github.figma-rust-mcp.server';
|
|
325
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${plistLabel}.plist`);
|
|
326
|
+
|
|
327
|
+
if (fs.existsSync(plistPath)) {
|
|
328
|
+
try { execSync(`launchctl unload -w "${plistPath}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
329
|
+
try { execSync(`pkill -f figma-rust-mcp 2>/dev/null`, { stdio: 'pipe' }); } catch (_) {}
|
|
330
|
+
fs.unlinkSync(plistPath);
|
|
331
|
+
}
|
|
332
|
+
removeLegacyService();
|
|
333
|
+
|
|
334
|
+
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
335
|
+
|
|
336
|
+
} else if (platform === 'linux') {
|
|
337
|
+
try { execSync(`systemctl --user disable --now figma-rust-mcp`, { stdio: 'inherit' }); } catch (_) {}
|
|
338
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-rust-mcp.service');
|
|
339
|
+
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
340
|
+
try { execSync(`systemctl --user daemon-reload`, { stdio: 'inherit' }); } catch (_) {}
|
|
341
|
+
removeLegacyService();
|
|
342
|
+
|
|
343
|
+
console.log(`\x1b[32m✓ figma-rust-mcp service uninstalled.\x1b[0m`);
|
|
344
|
+
|
|
345
|
+
} else if (platform === 'win32') {
|
|
346
|
+
const taskName = 'FigmaRustMCPServer';
|
|
347
|
+
try { execSync(`schtasks /Delete /F /TN "${taskName}"`, { stdio: 'inherit' }); } catch (_) {}
|
|
348
|
+
removeLegacyService();
|
|
349
|
+
|
|
350
|
+
console.log(`\x1b[32m✓ figma-rust-mcp Windows Task removed.\x1b[0m`);
|
|
351
|
+
|
|
352
|
+
} else {
|
|
353
|
+
console.error(`\x1b[31mUnsupported platform: ${platform}\x1b[0m`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function removeLegacyService() {
|
|
359
|
+
if (process.platform === 'darwin') {
|
|
360
|
+
const legacyPlist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-mcp.server.plist');
|
|
361
|
+
if (fs.existsSync(legacyPlist)) {
|
|
362
|
+
try { execSync(`launchctl unload -w "${legacyPlist}"`, { stdio: 'pipe' }); } catch (_) {}
|
|
363
|
+
try { execSync('pkill -x figma-mcp', { stdio: 'pipe' }); } catch (_) {}
|
|
364
|
+
fs.unlinkSync(legacyPlist);
|
|
365
|
+
}
|
|
366
|
+
} else if (process.platform === 'linux') {
|
|
367
|
+
const legacyUnit = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service');
|
|
368
|
+
if (fs.existsSync(legacyUnit)) {
|
|
369
|
+
try { execSync('systemctl --user disable --now figma-mcp', { stdio: 'pipe' }); } catch (_) {}
|
|
370
|
+
fs.unlinkSync(legacyUnit);
|
|
371
|
+
try { execSync('systemctl --user daemon-reload', { stdio: 'pipe' }); } catch (_) {}
|
|
372
|
+
}
|
|
373
|
+
} else if (process.platform === 'win32') {
|
|
374
|
+
try { execSync('schtasks /End /TN "FigmaMCPServer"', { stdio: 'pipe' }); } catch (_) {}
|
|
375
|
+
try { execSync('schtasks /Delete /F /TN "FigmaMCPServer"', { stdio: 'pipe' }); } catch (_) {}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check if the service is currently installed.
|
|
381
|
+
*/
|
|
382
|
+
function isServiceInstalled() {
|
|
383
|
+
const platform = process.platform;
|
|
384
|
+
if (platform === 'darwin') {
|
|
385
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-rust-mcp.server.plist');
|
|
386
|
+
return fs.existsSync(plistPath) || fs.existsSync(path.join(os.homedir(), 'Library', 'LaunchAgents', 'io.github.figma-mcp.server.plist'));
|
|
387
|
+
} else if (platform === 'linux') {
|
|
388
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-rust-mcp.service');
|
|
389
|
+
return fs.existsSync(servicePath) || fs.existsSync(path.join(os.homedir(), '.config', 'systemd', 'user', 'figma-mcp.service'));
|
|
390
|
+
} else if (platform === 'win32') {
|
|
391
|
+
try {
|
|
392
|
+
execSync('schtasks /Query /TN "FigmaRustMCPServer"', { stdio: 'pipe' });
|
|
393
|
+
return true;
|
|
394
|
+
} catch (_) {
|
|
395
|
+
try { execSync('schtasks /Query /TN "FigmaMCPServer"', { stdio: 'pipe' }); return true; }
|
|
396
|
+
catch (_) { return false; }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function setupPlugin(customDir) {
|
|
403
|
+
const pluginDir = customDir || path.join(os.homedir(), '.figma-rust-mcp', 'plugin');
|
|
404
|
+
const sourcePluginDir = path.resolve(__dirname, '../plugin');
|
|
405
|
+
|
|
406
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
407
|
+
|
|
408
|
+
const files = ['manifest.json', 'code.js', 'ui.html', 'icon16.png', 'icon32.png'];
|
|
409
|
+
for (const f of files) {
|
|
410
|
+
const src = path.join(sourcePluginDir, f);
|
|
411
|
+
const dest = path.join(pluginDir, f);
|
|
412
|
+
if (fs.existsSync(src)) {
|
|
413
|
+
fs.copyFileSync(src, dest);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
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`);
|
|
425
|
+
return manifestPath;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isNewer(latest, current) {
|
|
429
|
+
const pLatest = latest.split('.').map(n => parseInt(n, 10) || 0);
|
|
430
|
+
const pCurrent = current.split('.').map(n => parseInt(n, 10) || 0);
|
|
431
|
+
for (let i = 0; i < 3; i++) {
|
|
432
|
+
if ((pLatest[i] || 0) > (pCurrent[i] || 0)) return true;
|
|
433
|
+
if ((pLatest[i] || 0) < (pCurrent[i] || 0)) return false;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function checkForUpdatesAsync() {
|
|
439
|
+
const updateStateFile = path.join(os.homedir(), '.figma-rust-mcp', 'update-check.json');
|
|
440
|
+
try {
|
|
441
|
+
if (fs.existsSync(updateStateFile)) {
|
|
442
|
+
const state = JSON.parse(fs.readFileSync(updateStateFile, 'utf8'));
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
if (state.lastChecked && (now - state.lastChecked < 6 * 3600 * 1000)) {
|
|
445
|
+
if (state.latestVersion && isNewer(state.latestVersion, pkgVersion)) {
|
|
446
|
+
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${state.latestVersion} (Run: npx figma-rust-mcp --upgrade)\x1b[0m\n\n`);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch (_) {}
|
|
452
|
+
|
|
453
|
+
const req = https.get('https://registry.npmjs.org/figma-rust-mcp/latest', {
|
|
454
|
+
headers: { 'User-Agent': 'figma-rust-mcp-updater' },
|
|
455
|
+
timeout: 2000
|
|
456
|
+
}, (res) => {
|
|
457
|
+
let data = '';
|
|
458
|
+
res.on('data', chunk => { data += chunk; });
|
|
459
|
+
res.on('end', () => {
|
|
460
|
+
try {
|
|
461
|
+
if (res.statusCode === 200) {
|
|
462
|
+
const json = JSON.parse(data);
|
|
463
|
+
const latest = json.version;
|
|
464
|
+
fs.mkdirSync(path.join(os.homedir(), '.figma-rust-mcp'), { recursive: true });
|
|
465
|
+
fs.writeFileSync(updateStateFile, JSON.stringify({
|
|
466
|
+
lastChecked: Date.now(),
|
|
467
|
+
latestVersion: latest
|
|
468
|
+
}, null, 2));
|
|
469
|
+
if (latest && isNewer(latest, pkgVersion)) {
|
|
470
|
+
process.stderr.write(`\n\x1b[36m⚡ Update available: v${pkgVersion} → v${latest}\x1b[0m\n\x1b[36m Run: npx figma-rust-mcp --upgrade\x1b[0m\n\n`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch (_) {}
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
req.on('error', () => {});
|
|
477
|
+
req.on('timeout', () => req.destroy());
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function upgrade() {
|
|
481
|
+
console.log('\x1b[36m[figma-rust-mcp]\x1b[0m Checking for updates and upgrading figma-rust-mcp...');
|
|
482
|
+
try {
|
|
483
|
+
execSync('npm install -g figma-rust-mcp@latest', { stdio: 'inherit' });
|
|
484
|
+
console.log('\x1b[32m✓ Upgraded figma-rust-mcp to latest version.\x1b[0m');
|
|
485
|
+
setupPlugin();
|
|
486
|
+
if (isServiceInstalled()) {
|
|
487
|
+
console.log('\x1b[36mUpdating background service binary...\x1b[0m');
|
|
488
|
+
const binPath = await findOrDownloadBinary();
|
|
489
|
+
await installService(binPath);
|
|
490
|
+
}
|
|
491
|
+
} catch (err) {
|
|
492
|
+
console.log('\x1b[33mTip: Run directly with latest version:\x1b[0m\n npx -y figma-rust-mcp@latest\n');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function setupMenu() {
|
|
497
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
498
|
+
console.log(`Figma Rust MCP setup options:\n npx -y figma-rust-mcp@latest --install-service Install plugin and background service\n npx -y figma-rust-mcp@latest --setup-plugin Install/update plugin only\n npx -y figma-rust-mcp@latest --service-status Check background service`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
503
|
+
const agents = detectAgents();
|
|
504
|
+
try {
|
|
505
|
+
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`);
|
|
506
|
+
const choice = (await rl.question('Choose an option [1]: ')).trim() || '1';
|
|
507
|
+
switch (choice) {
|
|
508
|
+
case '1': {
|
|
509
|
+
setupPlugin();
|
|
510
|
+
const binPath = await findOrDownloadBinary();
|
|
511
|
+
await installService(binPath);
|
|
512
|
+
await configureDetectedAgents(rl, agents);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case '2': setupPlugin(); break;
|
|
516
|
+
case '3': await runBinary(['--server']); break;
|
|
517
|
+
case '4': await configureDetectedAgents(rl, agents); break;
|
|
518
|
+
case '5': console.log(isServiceInstalled() ? '✓ Background service is installed.' : '✗ Background service is not installed.'); break;
|
|
519
|
+
case '6': await upgrade(); break;
|
|
520
|
+
case '7': await uninstallService(); break;
|
|
521
|
+
case '0': break;
|
|
522
|
+
default: console.log('Unknown option. Run --init to try again.');
|
|
523
|
+
}
|
|
524
|
+
} finally {
|
|
525
|
+
rl.close();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function configureDetectedAgents(rl, agents) {
|
|
530
|
+
if (!agents.length) {
|
|
531
|
+
console.log('No supported AI coding agents were detected. Install an agent and rerun --init.');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log('\nDetected agents:');
|
|
536
|
+
agents.forEach((agent, index) => console.log(` ${index + 1}) ${agent.label} — ${agent.configPath}`));
|
|
537
|
+
const answer = (await rl.question('Configure which agents? Enter numbers separated by commas, a=all, Enter=skip [a]: ')).trim().toLowerCase();
|
|
538
|
+
if (answer === '') return;
|
|
539
|
+
const selected = answer === 'a'
|
|
540
|
+
? agents
|
|
541
|
+
: [...new Set(answer.split(',').map(value => Number(value.trim()) - 1))]
|
|
542
|
+
.filter(index => Number.isInteger(index) && index >= 0 && index < agents.length)
|
|
543
|
+
.map(index => agents[index]);
|
|
544
|
+
if (!selected.length) {
|
|
545
|
+
console.log('No valid agent selection; MCP configuration was not changed.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const agent of selected) {
|
|
550
|
+
try {
|
|
551
|
+
const changed = configureAgent(agent);
|
|
552
|
+
console.log(` ${changed ? '✓ Configured' : '✓ Already configured'} ${agent.label}`);
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error(` ✗ Could not configure ${agent.label}: ${err.message}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
console.log('Restart or reload the selected agent to connect it to Figma Rust MCP.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function runBinary(args) {
|
|
561
|
+
return findOrDownloadBinary().then(binPath => new Promise((resolve, reject) => {
|
|
562
|
+
const child = spawn(binPath, args, { stdio: 'inherit', env: process.env });
|
|
563
|
+
child.on('error', reject);
|
|
564
|
+
child.on('exit', (code, signal) => signal ? process.kill(process.pid, signal) : resolve(code ?? 0));
|
|
565
|
+
for (const sig of ['SIGINT', 'SIGTERM']) process.once(sig, () => child.kill(sig));
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function setupAlias() {
|
|
570
|
+
const platform = process.platform;
|
|
571
|
+
if (platform === 'win32') {
|
|
572
|
+
console.log('\x1b[33mOn Windows, install globally for direct command access:\x1b[0m\n npm install -g figma-rust-mcp\n');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const shellRc = process.env.SHELL && process.env.SHELL.includes('zsh')
|
|
577
|
+
? path.join(os.homedir(), '.zshrc')
|
|
578
|
+
: path.join(os.homedir(), '.bashrc');
|
|
579
|
+
|
|
580
|
+
const aliasLine = "alias figma-rust-mcp='npx figma-rust-mcp'";
|
|
581
|
+
try {
|
|
582
|
+
let content = fs.existsSync(shellRc) ? fs.readFileSync(shellRc, 'utf8') : '';
|
|
583
|
+
if (!content.includes('alias figma-rust-mcp=')) {
|
|
584
|
+
fs.appendFileSync(shellRc, `\n# Figma Rust MCP alias\n${aliasLine}\n`);
|
|
585
|
+
console.log(`\x1b[32m✓ Added alias to ${shellRc}:\x1b[0m`);
|
|
586
|
+
console.log(` ${aliasLine}`);
|
|
587
|
+
console.log(`\nRun \x1b[36msource ${shellRc}\x1b[0m to start using \x1b[1mfigma-rust-mcp\x1b[0m command directly!`);
|
|
588
|
+
} else {
|
|
589
|
+
console.log(`\x1b[32m✓ Alias already exists in ${shellRc}\x1b[0m`);
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
console.error(`Failed to configure alias: ${err.message}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
async function main() {
|
|
599
|
+
const args = process.argv.slice(2);
|
|
600
|
+
|
|
601
|
+
if (args.includes('--init') || args.includes('--configure')) {
|
|
602
|
+
try { await setupMenu(); }
|
|
603
|
+
catch (err) { console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`); process.exit(1); }
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (args.includes('--upgrade') || args.includes('--update')) {
|
|
608
|
+
await upgrade();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (args.includes('--alias')) {
|
|
613
|
+
setupAlias();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (args.includes('--setup-plugin') || args.includes('--export-plugin') || args.includes('--setup')) {
|
|
618
|
+
const customDir = args.find(a => !a.startsWith('-') && a !== '--setup-plugin' && a !== '--export-plugin' && a !== '--setup');
|
|
619
|
+
setupPlugin(customDir);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (args.includes('--install-service')) {
|
|
624
|
+
try {
|
|
625
|
+
setupPlugin();
|
|
626
|
+
const binPath = await findOrDownloadBinary();
|
|
627
|
+
await installService(binPath);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (args.includes('--uninstall-service')) {
|
|
636
|
+
try {
|
|
637
|
+
await uninstallService();
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (args.includes('--service-status')) {
|
|
646
|
+
const installed = isServiceInstalled();
|
|
647
|
+
if (installed) {
|
|
648
|
+
console.log('\x1b[32m✓ figma-rust-mcp service is installed.\x1b[0m');
|
|
649
|
+
console.log(' To uninstall: npx figma-rust-mcp --uninstall-service');
|
|
650
|
+
} else {
|
|
651
|
+
console.log('\x1b[33m✗ figma-rust-mcp service is NOT installed.\x1b[0m');
|
|
652
|
+
console.log(' To install: npx figma-rust-mcp --install-service');
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Non-blocking auto update check
|
|
658
|
+
checkForUpdatesAsync();
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
process.exit(await runBinary(args));
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`\x1b[31m[figma-rust-mcp error]\x1b[0m ${err.message}`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
main();
|