design-lazyyy-cli 0.1.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/src/index.js ADDED
@@ -0,0 +1,2528 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { execSync, spawn } from 'child_process';
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import { createInterface } from 'readline';
11
+ import { homedir, platform } from 'os';
12
+ import { FigJamClient } from './figjam-client.js';
13
+
14
+ // Platform detection
15
+ const IS_WINDOWS = platform() === 'win32';
16
+ const IS_MAC = platform() === 'darwin';
17
+ const IS_LINUX = platform() === 'linux';
18
+
19
+ // Platform-specific Figma paths and commands
20
+ function getFigmaPath() {
21
+ if (IS_MAC) {
22
+ return '/Applications/Figma.app/Contents/MacOS/Figma';
23
+ } else if (IS_WINDOWS) {
24
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
25
+ return join(localAppData, 'Figma', 'Figma.exe');
26
+ } else {
27
+ // Linux
28
+ return '/usr/bin/figma';
29
+ }
30
+ }
31
+
32
+ function startFigma() {
33
+ const figmaPath = getFigmaPath();
34
+ if (IS_MAC) {
35
+ execSync('open -a Figma --args --remote-debugging-port=9222', { stdio: 'pipe' });
36
+ } else if (IS_WINDOWS) {
37
+ spawn(figmaPath, ['--remote-debugging-port=9222'], { detached: true, stdio: 'ignore' }).unref();
38
+ } else {
39
+ spawn(figmaPath, ['--remote-debugging-port=9222'], { detached: true, stdio: 'ignore' }).unref();
40
+ }
41
+ }
42
+
43
+ function killFigma() {
44
+ try {
45
+ if (IS_MAC) {
46
+ execSync('pkill -x Figma 2>/dev/null || true', { stdio: 'pipe' });
47
+ } else if (IS_WINDOWS) {
48
+ execSync('taskkill /IM Figma.exe /F 2>nul', { stdio: 'pipe' });
49
+ } else {
50
+ execSync('pkill -x figma 2>/dev/null || true', { stdio: 'pipe' });
51
+ }
52
+ } catch (e) {
53
+ // Ignore errors if Figma wasn't running
54
+ }
55
+ }
56
+
57
+ function getManualStartCommand() {
58
+ if (IS_MAC) {
59
+ return 'open -a Figma --args --remote-debugging-port=9222';
60
+ } else if (IS_WINDOWS) {
61
+ return '"%LOCALAPPDATA%\\Figma\\Figma.exe" --remote-debugging-port=9222';
62
+ } else {
63
+ return 'figma --remote-debugging-port=9222';
64
+ }
65
+ }
66
+
67
+ const __filename = fileURLToPath(import.meta.url);
68
+ const __dirname = dirname(__filename);
69
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
70
+
71
+ const CONFIG_DIR = join(homedir(), '.design-lazyyy-cli');
72
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
73
+
74
+ const program = new Command();
75
+
76
+ // Helper: Prompt user
77
+ function prompt(question) {
78
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
79
+ return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer); }));
80
+ }
81
+
82
+ // Helper: Load config
83
+ function loadConfig() {
84
+ try {
85
+ if (existsSync(CONFIG_FILE)) {
86
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
87
+ }
88
+ } catch {}
89
+ return {};
90
+ }
91
+
92
+ // Helper: Save config
93
+ function saveConfig(config) {
94
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
95
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
96
+ }
97
+
98
+ // Helper: Run figma-use command
99
+ function figmaUse(args, options = {}) {
100
+ try {
101
+ const result = execSync(`figma-use ${args}`, {
102
+ encoding: 'utf8',
103
+ stdio: options.silent ? 'pipe' : 'inherit',
104
+ ...options
105
+ });
106
+ return result;
107
+ } catch (error) {
108
+ if (options.silent) return null;
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ // Helper: Check connection
114
+ function checkConnection() {
115
+ const result = figmaUse('status', { silent: true });
116
+ if (!result || result.includes('Not connected')) {
117
+ console.log(chalk.red('\n✗ Not connected to Figma\n'));
118
+ console.log(chalk.white(' Make sure Figma is running with remote debugging:'));
119
+ console.log(chalk.cyan(' design-lazyyy-cli connect\n'));
120
+ process.exit(1);
121
+ }
122
+ return true;
123
+ }
124
+
125
+ // Helper: Check if figma-use is installed
126
+ function checkDependencies(silent = false) {
127
+ try {
128
+ execSync('which figma-use', { stdio: 'pipe' });
129
+ return true;
130
+ } catch {
131
+ if (!silent) {
132
+ console.log(chalk.yellow(' Installing figma-use...'));
133
+ execSync('npm install -g figma-use', { stdio: 'inherit' });
134
+ }
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // Helper: Check if Figma is patched
140
+ function isFigmaPatched() {
141
+ const config = loadConfig();
142
+ return config.patched === true;
143
+ }
144
+
145
+ // Helper: Hex to Figma RGB
146
+ function hexToRgb(hex) {
147
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
148
+ return {
149
+ r: parseInt(result[1], 16) / 255,
150
+ g: parseInt(result[2], 16) / 255,
151
+ b: parseInt(result[3], 16) / 255
152
+ };
153
+ }
154
+
155
+ // Helper: Smart positioning code (returns JS to get next free X position)
156
+ function smartPosCode(gap = 100) {
157
+ return `
158
+ const children = figma.currentPage.children;
159
+ let smartX = 0;
160
+ if (children.length > 0) {
161
+ children.forEach(n => { smartX = Math.max(smartX, n.x + n.width); });
162
+ smartX += ${gap};
163
+ }
164
+ `;
165
+ }
166
+
167
+ program
168
+ .name('design-lazyyy-cli')
169
+ .description('CLI for managing Figma design systems')
170
+ .version(pkg.version);
171
+
172
+ // Default action when no command is given
173
+ program.action(async () => {
174
+ const config = loadConfig();
175
+
176
+ // First time? Run init
177
+ if (!config.patched || !checkDependencies(true)) {
178
+ showBanner();
179
+ console.log(chalk.white(' Welcome! Let\'s get you set up.\n'));
180
+ console.log(chalk.gray(' This takes about 30 seconds. No API key needed.\n'));
181
+
182
+ // Step 1: Check Node version
183
+ console.log(chalk.blue('Step 1/4: ') + 'Checking Node.js...');
184
+ const nodeVersion = process.version;
185
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
186
+ if (nodeMajor < 18) {
187
+ console.log(chalk.red(` ✗ Node.js ${nodeVersion} is too old. Please upgrade to Node 18+`));
188
+ process.exit(1);
189
+ }
190
+ console.log(chalk.green(` ✓ Node.js ${nodeVersion}`));
191
+
192
+ // Step 2: Install figma-use
193
+ console.log(chalk.blue('\nStep 2/4: ') + 'Installing dependencies...');
194
+ if (checkDependencies(true)) {
195
+ console.log(chalk.green(' ✓ figma-use already installed'));
196
+ } else {
197
+ const spinner = ora(' Installing figma-use...').start();
198
+ try {
199
+ execSync('npm install -g figma-use', { stdio: 'pipe' });
200
+ spinner.succeed('figma-use installed');
201
+ } catch (error) {
202
+ spinner.fail('Failed to install figma-use');
203
+ console.log(chalk.gray(' Try manually: npm install -g figma-use'));
204
+ process.exit(1);
205
+ }
206
+ }
207
+
208
+ // Step 3: Patch Figma
209
+ console.log(chalk.blue('\nStep 3/4: ') + 'Patching Figma Desktop...');
210
+ if (config.patched) {
211
+ console.log(chalk.green(' ✓ Figma already patched'));
212
+ } else {
213
+ console.log(chalk.gray(' (This allows CLI to connect to Figma)'));
214
+ const spinner = ora(' Patching...').start();
215
+ try {
216
+ execSync('figma-use patch', { stdio: 'pipe' });
217
+ config.patched = true;
218
+ saveConfig(config);
219
+ spinner.succeed('Figma patched');
220
+ } catch (error) {
221
+ if (error.message?.includes('already patched') || error.stderr?.includes('already patched')) {
222
+ config.patched = true;
223
+ saveConfig(config);
224
+ spinner.succeed('Figma already patched');
225
+ } else {
226
+ spinner.fail('Patch failed');
227
+ console.log(chalk.gray(' Try manually: figma-use patch'));
228
+ }
229
+ }
230
+ }
231
+
232
+ // Step 4: Start Figma
233
+ console.log(chalk.blue('\nStep 4/4: ') + 'Starting Figma...');
234
+ try {
235
+ killFigma();
236
+ await new Promise(r => setTimeout(r, 1000));
237
+ startFigma();
238
+ console.log(chalk.green(' ✓ Figma started'));
239
+
240
+ // Wait for connection
241
+ const spinner = ora(' Waiting for connection...').start();
242
+ let connected = false;
243
+ for (let i = 0; i < 10; i++) {
244
+ await new Promise(r => setTimeout(r, 1000));
245
+ const result = figmaUse('status', { silent: true });
246
+ if (result && result.includes('Connected')) {
247
+ connected = true;
248
+ break;
249
+ }
250
+ }
251
+
252
+ if (connected) {
253
+ spinner.succeed('Connected to Figma');
254
+ } else {
255
+ spinner.warn('Connection pending - open a file in Figma');
256
+ }
257
+ } catch (error) {
258
+ console.log(chalk.yellow(' ! Could not start Figma automatically'));
259
+ console.log(chalk.gray(' Start manually: ' + getManualStartCommand()));
260
+ }
261
+
262
+ // Done!
263
+ console.log(chalk.green('\n ✓ Setup complete!\n'));
264
+ showQuickStart();
265
+ return;
266
+ }
267
+
268
+ // Already set up - check connection and show status
269
+ showBanner();
270
+
271
+ const result = figmaUse('status', { silent: true });
272
+ if (result && result.includes('Connected')) {
273
+ console.log(chalk.green(' ✓ Connected to Figma\n'));
274
+ console.log(chalk.gray(result.trim().split('\n').map(l => ' ' + l).join('\n')));
275
+ console.log();
276
+ showQuickStart();
277
+ } else {
278
+ console.log(chalk.yellow(' ⚠ Figma not connected\n'));
279
+ console.log(chalk.white(' Starting Figma...'));
280
+ try {
281
+ killFigma();
282
+ await new Promise(r => setTimeout(r, 500));
283
+ startFigma();
284
+ console.log(chalk.green(' ✓ Figma started\n'));
285
+
286
+ const spinner = ora(' Waiting for connection...').start();
287
+ for (let i = 0; i < 8; i++) {
288
+ await new Promise(r => setTimeout(r, 1000));
289
+ const res = figmaUse('status', { silent: true });
290
+ if (res && res.includes('Connected')) {
291
+ spinner.succeed('Connected to Figma\n');
292
+ showQuickStart();
293
+ return;
294
+ }
295
+ }
296
+ spinner.warn('Open a file in Figma to connect\n');
297
+ showQuickStart();
298
+ } catch {
299
+ console.log(chalk.gray(' Start manually: ' + getManualStartCommand() + '\n'));
300
+ }
301
+ }
302
+ });
303
+
304
+ function showQuickStart() {
305
+ console.log(chalk.white(' Quick start:\n'));
306
+ console.log(chalk.gray(' Create design system ') + chalk.cyan('design-lazyyy-cli tokens ds'));
307
+ console.log(chalk.gray(' Create Tailwind colors ') + chalk.cyan('design-lazyyy-cli tokens tailwind'));
308
+ console.log(chalk.gray(' List all variables ') + chalk.cyan('design-lazyyy-cli var list'));
309
+ console.log(chalk.gray(' Render JSX to Figma ') + chalk.cyan('design-lazyyy-cli render \'<Frame>...</Frame>\''));
310
+ console.log(chalk.gray(' See all commands ') + chalk.cyan('design-lazyyy-cli --help'));
311
+ console.log();
312
+ console.log();
313
+ }
314
+
315
+ // ============ WELCOME BANNER ============
316
+
317
+ function showBanner() {
318
+ console.log(chalk.cyan(`
319
+ ██████╗ ███████╗███████╗██╗ ██████╗ ███╗ ██╗ ██╗ █████╗ ███████╗██╗ ██╗██╗ ██╗██╗ ██╗
320
+ ██╔══██╗██╔════╝██╔════╝██║██╔════╝ ████╗ ██║ ██║ ██╔══██╗╚══███╔╝╚██╗ ██╔╝╚██╗ ██╔╝╚██╗ ██╔╝
321
+ ██║ ██║█████╗ ███████╗██║██║ ███╗██╔██╗ ██║█████╗██║ ███████║ ███╔╝ ╚████╔╝ ╚████╔╝ ╚████╔╝
322
+ ██║ ██║██╔══╝ ╚════██║██║██║ ██║██║╚██╗██║╚════╝██║ ██╔══██║ ███╔╝ ╚██╔╝ ╚██╔╝ ╚██╔╝
323
+ ██████╔╝███████╗███████║██║╚██████╔╝██║ ╚████║ ███████╗██║ ██║███████╗ ██║ ██║ ██║
324
+ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝
325
+ `));
326
+ console.log(chalk.white(` Design System CLI for Figma ${chalk.gray('v' + pkg.version)}`));
327
+ console.log(chalk.gray(` by plugin87\n`));
328
+ }
329
+
330
+ // ============ INIT (Interactive Onboarding) ============
331
+
332
+ program
333
+ .command('init')
334
+ .description('Interactive setup wizard')
335
+ .action(async () => {
336
+ showBanner();
337
+
338
+ console.log(chalk.white(' Welcome! Let\'s get you set up.\n'));
339
+ console.log(chalk.gray(' This takes about 30 seconds. No API key needed.\n'));
340
+
341
+ // Step 1: Check Node version
342
+ console.log(chalk.blue('Step 1/4: ') + 'Checking Node.js...');
343
+ const nodeVersion = process.version;
344
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0]);
345
+ if (nodeMajor < 18) {
346
+ console.log(chalk.red(` ✗ Node.js ${nodeVersion} is too old. Please upgrade to Node 18+`));
347
+ process.exit(1);
348
+ }
349
+ console.log(chalk.green(` ✓ Node.js ${nodeVersion}`));
350
+
351
+ // Step 2: Install figma-use
352
+ console.log(chalk.blue('\nStep 2/4: ') + 'Installing dependencies...');
353
+ if (checkDependencies(true)) {
354
+ console.log(chalk.green(' ✓ figma-use already installed'));
355
+ } else {
356
+ const spinner = ora(' Installing figma-use...').start();
357
+ try {
358
+ execSync('npm install -g figma-use', { stdio: 'pipe' });
359
+ spinner.succeed('figma-use installed');
360
+ } catch (error) {
361
+ spinner.fail('Failed to install figma-use');
362
+ console.log(chalk.gray(' Try manually: npm install -g figma-use'));
363
+ process.exit(1);
364
+ }
365
+ }
366
+
367
+ // Step 3: Patch Figma
368
+ console.log(chalk.blue('\nStep 3/4: ') + 'Patching Figma Desktop...');
369
+ const config = loadConfig();
370
+ if (config.patched) {
371
+ console.log(chalk.green(' ✓ Figma already patched'));
372
+ } else {
373
+ console.log(chalk.gray(' (This allows CLI to connect to Figma)'));
374
+ const spinner = ora(' Patching...').start();
375
+ try {
376
+ execSync('figma-use patch', { stdio: 'pipe' });
377
+ config.patched = true;
378
+ saveConfig(config);
379
+ spinner.succeed('Figma patched');
380
+ } catch (error) {
381
+ if (error.message?.includes('already patched')) {
382
+ config.patched = true;
383
+ saveConfig(config);
384
+ spinner.succeed('Figma already patched');
385
+ } else {
386
+ spinner.fail('Patch failed');
387
+ console.log(chalk.gray(' Try manually: figma-use patch'));
388
+ }
389
+ }
390
+ }
391
+
392
+ // Step 4: Start Figma
393
+ console.log(chalk.blue('\nStep 4/4: ') + 'Starting Figma...');
394
+ try {
395
+ killFigma();
396
+ await new Promise(r => setTimeout(r, 1000));
397
+ startFigma();
398
+ console.log(chalk.green(' ✓ Figma started'));
399
+
400
+ // Wait for connection
401
+ const spinner = ora(' Waiting for connection...').start();
402
+ let connected = false;
403
+ for (let i = 0; i < 10; i++) {
404
+ await new Promise(r => setTimeout(r, 1000));
405
+ const result = figmaUse('status', { silent: true });
406
+ if (result && result.includes('Connected')) {
407
+ connected = true;
408
+ break;
409
+ }
410
+ }
411
+
412
+ if (connected) {
413
+ spinner.succeed('Connected to Figma');
414
+ } else {
415
+ spinner.warn('Connection pending - open a file in Figma');
416
+ }
417
+ } catch (error) {
418
+ console.log(chalk.yellow(' ! Could not start Figma automatically'));
419
+ console.log(chalk.gray(' Start manually: ' + getManualStartCommand()));
420
+ }
421
+
422
+ // Done!
423
+ console.log(chalk.green('\n ✓ Setup complete!\n'));
424
+
425
+ console.log(chalk.white(' Quick start:\n'));
426
+ console.log(chalk.gray(' Create Tailwind colors ') + chalk.cyan('design-lazyyy-cli tokens tailwind'));
427
+ console.log(chalk.gray(' Create spacing scale ') + chalk.cyan('design-lazyyy-cli tokens spacing'));
428
+ console.log(chalk.gray(' List all variables ') + chalk.cyan('design-lazyyy-cli var list'));
429
+ console.log(chalk.gray(' Render JSX to Figma ') + chalk.cyan('design-lazyyy-cli render \'<Frame>...</Frame>\''));
430
+ console.log(chalk.gray(' See all commands ') + chalk.cyan('design-lazyyy-cli --help'));
431
+ console.log();
432
+ console.log();
433
+ });
434
+
435
+ // ============ SETUP (alias for init) ============
436
+
437
+ program
438
+ .command('setup')
439
+ .description('Setup Figma for CLI access (alias for init)')
440
+ .action(() => {
441
+ // Redirect to init
442
+ execSync('design-lazyyy-cli init', { stdio: 'inherit' });
443
+ });
444
+
445
+ // ============ STATUS ============
446
+
447
+ program
448
+ .command('status')
449
+ .description('Check connection to Figma')
450
+ .action(() => {
451
+ // Check if first run
452
+ const config = loadConfig();
453
+ if (!config.patched && !checkDependencies(true)) {
454
+ console.log(chalk.yellow('\n⚠ First time? Run the setup wizard:\n'));
455
+ console.log(chalk.cyan(' design-lazyyy-cli init\n'));
456
+ return;
457
+ }
458
+ figmaUse('status');
459
+ });
460
+
461
+ // ============ CONNECT ============
462
+
463
+ program
464
+ .command('connect')
465
+ .description('Start Figma with remote debugging enabled')
466
+ .action(async () => {
467
+ // Check if first run
468
+ const config = loadConfig();
469
+ if (!config.patched) {
470
+ console.log(chalk.yellow('\n⚠ First time? Run the setup wizard:\n'));
471
+ console.log(chalk.cyan(' design-lazyyy-cli init\n'));
472
+ return;
473
+ }
474
+
475
+ console.log(chalk.blue('Starting Figma...'));
476
+ try {
477
+ killFigma();
478
+ await new Promise(r => setTimeout(r, 500));
479
+ } catch {}
480
+
481
+ startFigma();
482
+ console.log(chalk.green('✓ Figma started\n'));
483
+
484
+ // Wait and check connection
485
+ const spinner = ora('Waiting for connection...').start();
486
+ for (let i = 0; i < 8; i++) {
487
+ await new Promise(r => setTimeout(r, 1000));
488
+ const result = figmaUse('status', { silent: true });
489
+ if (result && result.includes('Connected')) {
490
+ spinner.succeed('Connected to Figma');
491
+ console.log(chalk.gray(result.trim()));
492
+ return;
493
+ }
494
+ }
495
+ spinner.warn('Open a file in Figma to connect');
496
+ });
497
+
498
+ // ============ VARIABLES ============
499
+
500
+ const variables = program
501
+ .command('variables')
502
+ .alias('var')
503
+ .description('Manage design tokens/variables');
504
+
505
+ variables
506
+ .command('list')
507
+ .description('List all variables')
508
+ .action(() => {
509
+ checkConnection();
510
+ figmaUse('variable list');
511
+ });
512
+
513
+ variables
514
+ .command('create <name>')
515
+ .description('Create a variable')
516
+ .requiredOption('-c, --collection <id>', 'Collection ID')
517
+ .requiredOption('-t, --type <type>', 'Type: COLOR, FLOAT, STRING, BOOLEAN')
518
+ .option('-v, --value <value>', 'Initial value')
519
+ .action((name, options) => {
520
+ checkConnection();
521
+ let cmd = `variable create "${name}" --collection "${options.collection}" --type ${options.type}`;
522
+ if (options.value) cmd += ` --value "${options.value}"`;
523
+ figmaUse(cmd);
524
+ });
525
+
526
+ variables
527
+ .command('find <pattern>')
528
+ .description('Find variables by name pattern')
529
+ .action((pattern) => {
530
+ checkConnection();
531
+ figmaUse(`variable find "${pattern}"`);
532
+ });
533
+
534
+ // ============ COLLECTIONS ============
535
+
536
+ const collections = program
537
+ .command('collections')
538
+ .alias('col')
539
+ .description('Manage variable collections');
540
+
541
+ collections
542
+ .command('list')
543
+ .description('List all collections')
544
+ .action(() => {
545
+ checkConnection();
546
+ figmaUse('collection list');
547
+ });
548
+
549
+ collections
550
+ .command('create <name>')
551
+ .description('Create a collection')
552
+ .action((name) => {
553
+ checkConnection();
554
+ figmaUse(`collection create "${name}"`);
555
+ });
556
+
557
+ // ============ TOKENS (PRESETS) ============
558
+
559
+ const tokens = program
560
+ .command('tokens')
561
+ .description('Create design token presets');
562
+
563
+ tokens
564
+ .command('tailwind')
565
+ .description('Create Tailwind CSS color palette')
566
+ .option('-c, --collection <name>', 'Collection name', 'Color - Primitive')
567
+ .action((options) => {
568
+ checkConnection();
569
+ const spinner = ora('Creating Tailwind color palette...').start();
570
+
571
+ const tailwindColors = {
572
+ slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' },
573
+ gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' },
574
+ zinc: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
575
+ neutral: { 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' },
576
+ stone: { 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' },
577
+ red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' },
578
+ orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' },
579
+ amber: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' },
580
+ yellow: { 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' },
581
+ lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 300: '#bef264', 400: '#a3e635', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', 900: '#365314', 950: '#1a2e05' },
582
+ green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' },
583
+ emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' },
584
+ teal: { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' },
585
+ cyan: { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' },
586
+ sky: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' },
587
+ blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
588
+ indigo: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' },
589
+ violet: { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' },
590
+ purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' },
591
+ fuchsia: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' },
592
+ pink: { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' },
593
+ rose: { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' }
594
+ };
595
+
596
+ const code = `
597
+ const colors = ${JSON.stringify(tailwindColors)};
598
+ function hexToRgb(hex) {
599
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
600
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
601
+ }
602
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
603
+ if (!col) col = figma.variables.createVariableCollection('${options.collection}');
604
+ const modeId = col.modes[0].modeId;
605
+ let count = 0;
606
+ Object.entries(colors).forEach(([colorName, shades]) => {
607
+ Object.entries(shades).forEach(([shade, hex]) => {
608
+ const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
609
+ if (!existing) {
610
+ const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
611
+ v.setValueForMode(modeId, hexToRgb(hex));
612
+ count++;
613
+ }
614
+ });
615
+ });
616
+ 'Created ' + count + ' color variables in ${options.collection}'
617
+ `;
618
+
619
+ try {
620
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
621
+ spinner.succeed(result?.trim() || 'Created Tailwind palette');
622
+ } catch (error) {
623
+ spinner.fail('Failed to create palette');
624
+ console.error(error.message);
625
+ }
626
+ });
627
+
628
+ tokens
629
+ .command('shadcn')
630
+ .description('Create shadcn/ui color primitives (from v3.shadcn.com/colors)')
631
+ .option('-c, --collection <name>', 'Collection name', 'shadcn/primitives')
632
+ .action((options) => {
633
+ checkConnection();
634
+ const spinner = ora('Creating shadcn color primitives...').start();
635
+
636
+ // All colors from https://v3.shadcn.com/colors
637
+ const shadcnColors = {
638
+ slate: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' },
639
+ gray: { 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' },
640
+ zinc: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
641
+ neutral: { 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' },
642
+ stone: { 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' },
643
+ red: { 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5', 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c', 800: '#991b1b', 900: '#7f1d1d', 950: '#450a0a' },
644
+ orange: { 50: '#fff7ed', 100: '#ffedd5', 200: '#fed7aa', 300: '#fdba74', 400: '#fb923c', 500: '#f97316', 600: '#ea580c', 700: '#c2410c', 800: '#9a3412', 900: '#7c2d12', 950: '#431407' },
645
+ amber: { 50: '#fffbeb', 100: '#fef3c7', 200: '#fde68a', 300: '#fcd34d', 400: '#fbbf24', 500: '#f59e0b', 600: '#d97706', 700: '#b45309', 800: '#92400e', 900: '#78350f', 950: '#451a03' },
646
+ yellow: { 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047', 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207', 800: '#854d0e', 900: '#713f12', 950: '#422006' },
647
+ lime: { 50: '#f7fee7', 100: '#ecfccb', 200: '#d9f99d', 300: '#bef264', 400: '#a3e635', 500: '#84cc16', 600: '#65a30d', 700: '#4d7c0f', 800: '#3f6212', 900: '#365314', 950: '#1a2e05' },
648
+ green: { 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac', 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d', 800: '#166534', 900: '#14532d', 950: '#052e16' },
649
+ emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', 950: '#022c22' },
650
+ teal: { 50: '#f0fdfa', 100: '#ccfbf1', 200: '#99f6e4', 300: '#5eead4', 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 800: '#115e59', 900: '#134e4a', 950: '#042f2e' },
651
+ cyan: { 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9', 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490', 800: '#155e75', 900: '#164e63', 950: '#083344' },
652
+ sky: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e', 950: '#082f49' },
653
+ blue: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
654
+ indigo: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81', 950: '#1e1b4b' },
655
+ violet: { 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd', 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9', 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065' },
656
+ purple: { 50: '#faf5ff', 100: '#f3e8ff', 200: '#e9d5ff', 300: '#d8b4fe', 400: '#c084fc', 500: '#a855f7', 600: '#9333ea', 700: '#7e22ce', 800: '#6b21a8', 900: '#581c87', 950: '#3b0764' },
657
+ fuchsia: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' },
658
+ pink: { 50: '#fdf2f8', 100: '#fce7f3', 200: '#fbcfe8', 300: '#f9a8d4', 400: '#f472b6', 500: '#ec4899', 600: '#db2777', 700: '#be185d', 800: '#9d174d', 900: '#831843', 950: '#500724' },
659
+ rose: { 50: '#fff1f2', 100: '#ffe4e6', 200: '#fecdd3', 300: '#fda4af', 400: '#fb7185', 500: '#f43f5e', 600: '#e11d48', 700: '#be123c', 800: '#9f1239', 900: '#881337', 950: '#4c0519' }
660
+ };
661
+
662
+ const code = `
663
+ const colors = ${JSON.stringify(shadcnColors)};
664
+ function hexToRgb(hex) {
665
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
666
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
667
+ }
668
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
669
+ if (!col) col = figma.variables.createVariableCollection('${options.collection}');
670
+ const modeId = col.modes[0].modeId;
671
+ let count = 0;
672
+ Object.entries(colors).forEach(([colorName, shades]) => {
673
+ Object.entries(shades).forEach(([shade, hex]) => {
674
+ const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
675
+ if (!existing) {
676
+ const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
677
+ v.setValueForMode(modeId, hexToRgb(hex));
678
+ count++;
679
+ }
680
+ });
681
+ });
682
+ 'Created ' + count + ' shadcn color variables in ${options.collection}'
683
+ `;
684
+
685
+ try {
686
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
687
+ spinner.succeed(result?.trim() || 'Created shadcn primitives (231 colors)');
688
+ } catch (error) {
689
+ spinner.fail('Failed to create shadcn colors');
690
+ console.error(error.message);
691
+ }
692
+ });
693
+
694
+ tokens
695
+ .command('spacing')
696
+ .description('Create spacing scale (4px base)')
697
+ .option('-c, --collection <name>', 'Collection name', 'Spacing')
698
+ .action((options) => {
699
+ checkConnection();
700
+ const spinner = ora('Creating spacing scale...').start();
701
+
702
+ const spacings = {
703
+ '0': 0, '0.5': 2, '1': 4, '1.5': 6, '2': 8, '2.5': 10,
704
+ '3': 12, '3.5': 14, '4': 16, '5': 20, '6': 24, '7': 28,
705
+ '8': 32, '9': 36, '10': 40, '11': 44, '12': 48,
706
+ '14': 56, '16': 64, '20': 80, '24': 96, '28': 112,
707
+ '32': 128, '36': 144, '40': 160, '44': 176, '48': 192
708
+ };
709
+
710
+ const code = `
711
+ const spacings = ${JSON.stringify(spacings)};
712
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
713
+ if (!col) col = figma.variables.createVariableCollection('${options.collection}');
714
+ const modeId = col.modes[0].modeId;
715
+ let count = 0;
716
+ Object.entries(spacings).forEach(([name, value]) => {
717
+ const existing = figma.variables.getLocalVariables().find(v => v.name === 'spacing/' + name);
718
+ if (!existing) {
719
+ const v = figma.variables.createVariable('spacing/' + name, col.id, 'FLOAT');
720
+ v.setValueForMode(modeId, value);
721
+ count++;
722
+ }
723
+ });
724
+ 'Created ' + count + ' spacing variables'
725
+ `;
726
+
727
+ try {
728
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
729
+ spinner.succeed(result?.trim() || 'Created spacing scale');
730
+ } catch (error) {
731
+ spinner.fail('Failed to create spacing scale');
732
+ }
733
+ });
734
+
735
+ tokens
736
+ .command('radii')
737
+ .description('Create border radius scale')
738
+ .option('-c, --collection <name>', 'Collection name', 'Radii')
739
+ .action((options) => {
740
+ checkConnection();
741
+ const spinner = ora('Creating border radii...').start();
742
+
743
+ const radii = {
744
+ 'none': 0, 'sm': 2, 'default': 4, 'md': 6, 'lg': 8,
745
+ 'xl': 12, '2xl': 16, '3xl': 24, 'full': 9999
746
+ };
747
+
748
+ const code = `
749
+ const radii = ${JSON.stringify(radii)};
750
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
751
+ if (!col) col = figma.variables.createVariableCollection('${options.collection}');
752
+ const modeId = col.modes[0].modeId;
753
+ let count = 0;
754
+ Object.entries(radii).forEach(([name, value]) => {
755
+ const existing = figma.variables.getLocalVariables().find(v => v.name === 'radius/' + name);
756
+ if (!existing) {
757
+ const v = figma.variables.createVariable('radius/' + name, col.id, 'FLOAT');
758
+ v.setValueForMode(modeId, value);
759
+ count++;
760
+ }
761
+ });
762
+ 'Created ' + count + ' radius variables'
763
+ `;
764
+
765
+ try {
766
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
767
+ spinner.succeed(result?.trim() || 'Created border radii');
768
+ } catch (error) {
769
+ spinner.fail('Failed to create radii');
770
+ }
771
+ });
772
+
773
+ tokens
774
+ .command('import <file>')
775
+ .description('Import tokens from JSON file')
776
+ .option('-c, --collection <name>', 'Collection name')
777
+ .action((file, options) => {
778
+ checkConnection();
779
+
780
+ // Read JSON file
781
+ let tokensData;
782
+ try {
783
+ const content = readFileSync(file, 'utf8');
784
+ tokensData = JSON.parse(content);
785
+ } catch (error) {
786
+ console.log(chalk.red(`✗ Could not read file: ${file}`));
787
+ process.exit(1);
788
+ }
789
+
790
+ const spinner = ora('Importing tokens...').start();
791
+
792
+ // Detect format and convert
793
+ // Support: { "colors": { "primary": "#xxx" } } or { "primary": { "value": "#xxx", "type": "color" } }
794
+ const collectionName = options.collection || 'Imported Tokens';
795
+
796
+ const code = `
797
+ const data = ${JSON.stringify(tokensData)};
798
+ const collectionName = '${collectionName}';
799
+
800
+ function hexToRgb(hex) {
801
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
802
+ if (!r) return null;
803
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
804
+ }
805
+
806
+ function detectType(value) {
807
+ if (typeof value === 'string' && value.startsWith('#')) return 'COLOR';
808
+ if (typeof value === 'number') return 'FLOAT';
809
+ if (typeof value === 'boolean') return 'BOOLEAN';
810
+ return 'STRING';
811
+ }
812
+
813
+ function flattenTokens(obj, prefix = '') {
814
+ const result = [];
815
+ for (const [key, val] of Object.entries(obj)) {
816
+ const name = prefix ? prefix + '/' + key : key;
817
+ if (val && typeof val === 'object' && !val.value && !val.type) {
818
+ result.push(...flattenTokens(val, name));
819
+ } else {
820
+ const value = val?.value ?? val;
821
+ const type = val?.type?.toUpperCase() || detectType(value);
822
+ result.push({ name, value, type });
823
+ }
824
+ }
825
+ return result;
826
+ }
827
+
828
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === collectionName);
829
+ if (!col) col = figma.variables.createVariableCollection(collectionName);
830
+ const modeId = col.modes[0].modeId;
831
+
832
+ const tokens = flattenTokens(data);
833
+ let count = 0;
834
+
835
+ tokens.forEach(({ name, value, type }) => {
836
+ const existing = figma.variables.getLocalVariables().find(v => v.name === name);
837
+ if (!existing) {
838
+ try {
839
+ const figmaType = type === 'COLOR' ? 'COLOR' : type === 'FLOAT' || type === 'NUMBER' ? 'FLOAT' : type === 'BOOLEAN' ? 'BOOLEAN' : 'STRING';
840
+ const v = figma.variables.createVariable(name, col.id, figmaType);
841
+ let figmaValue = value;
842
+ if (figmaType === 'COLOR') figmaValue = hexToRgb(value);
843
+ if (figmaValue !== null) {
844
+ v.setValueForMode(modeId, figmaValue);
845
+ count++;
846
+ }
847
+ } catch (e) {}
848
+ }
849
+ });
850
+
851
+ 'Imported ' + count + ' tokens into ' + collectionName
852
+ `;
853
+
854
+ try {
855
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
856
+ spinner.succeed(result?.trim() || 'Tokens imported');
857
+ } catch (error) {
858
+ spinner.fail('Failed to import tokens');
859
+ console.error(error.message);
860
+ }
861
+ });
862
+
863
+ tokens
864
+ .command('ds')
865
+ .description('Create IDS Base Design System (complete starter kit)')
866
+ .action(async () => {
867
+ checkConnection();
868
+
869
+ console.log(chalk.cyan('\n IDS Base Design System'));
870
+ console.log(chalk.gray(' by Into Design Systems\n'));
871
+
872
+ // IDS Base values
873
+ const idsColors = {
874
+ gray: { 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' },
875
+ primary: { 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a', 950: '#172554' },
876
+ accent: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75', 950: '#4a044e' }
877
+ };
878
+
879
+ const idsSemanticColors = {
880
+ 'background/default': '#ffffff',
881
+ 'background/muted': '#f4f4f5',
882
+ 'background/emphasis': '#18181b',
883
+ 'foreground/default': '#18181b',
884
+ 'foreground/muted': '#71717a',
885
+ 'foreground/emphasis': '#ffffff',
886
+ 'border/default': '#e4e4e7',
887
+ 'border/focus': '#3b82f6',
888
+ 'action/primary': '#3b82f6',
889
+ 'action/primary-hover': '#2563eb',
890
+ 'feedback/success': '#22c55e',
891
+ 'feedback/success-muted': '#dcfce7',
892
+ 'feedback/warning': '#f59e0b',
893
+ 'feedback/warning-muted': '#fef3c7',
894
+ 'feedback/error': '#ef4444',
895
+ 'feedback/error-muted': '#fee2e2'
896
+ };
897
+
898
+ const idsSpacing = {
899
+ 'xs': 4, 'sm': 8, 'md': 16, 'lg': 24, 'xl': 32, '2xl': 48, '3xl': 64
900
+ };
901
+
902
+ const idsTypography = {
903
+ 'size/xs': 12, 'size/sm': 14, 'size/base': 16, 'size/lg': 18,
904
+ 'size/xl': 20, 'size/2xl': 24, 'size/3xl': 30, 'size/4xl': 36,
905
+ 'weight/normal': 400, 'weight/medium': 500, 'weight/semibold': 600, 'weight/bold': 700
906
+ };
907
+
908
+ const idsRadii = {
909
+ 'none': 0, 'sm': 4, 'md': 8, 'lg': 12, 'xl': 16, 'full': 9999
910
+ };
911
+
912
+ // Create Color - Primitives
913
+ let spinner = ora('Creating Color - Primitives...').start();
914
+ const primitivesCode = `
915
+ const colors = ${JSON.stringify(idsColors)};
916
+ function hexToRgb(hex) {
917
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
918
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
919
+ }
920
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Color - Primitives');
921
+ if (!col) col = figma.variables.createVariableCollection('Color - Primitives');
922
+ const modeId = col.modes[0].modeId;
923
+ let count = 0;
924
+ Object.entries(colors).forEach(([colorName, shades]) => {
925
+ Object.entries(shades).forEach(([shade, hex]) => {
926
+ const existing = figma.variables.getLocalVariables().find(v => v.name === colorName + '/' + shade);
927
+ if (!existing) {
928
+ const v = figma.variables.createVariable(colorName + '/' + shade, col.id, 'COLOR');
929
+ v.setValueForMode(modeId, hexToRgb(hex));
930
+ count++;
931
+ }
932
+ });
933
+ });
934
+ count
935
+ `;
936
+ try {
937
+ const result = figmaUse(`eval "${primitivesCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
938
+ spinner.succeed(`Color - Primitives (${result?.trim() || '33'} variables)`);
939
+ } catch { spinner.fail('Color - Primitives failed'); }
940
+
941
+ // Create Color - Semantic
942
+ spinner = ora('Creating Color - Semantic...').start();
943
+ const semanticCode = `
944
+ const colors = ${JSON.stringify(idsSemanticColors)};
945
+ function hexToRgb(hex) {
946
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
947
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
948
+ }
949
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Color - Semantic');
950
+ if (!col) col = figma.variables.createVariableCollection('Color - Semantic');
951
+ const modeId = col.modes[0].modeId;
952
+ let count = 0;
953
+ Object.entries(colors).forEach(([name, hex]) => {
954
+ const existing = figma.variables.getLocalVariables().find(v => v.name === name);
955
+ if (!existing) {
956
+ const v = figma.variables.createVariable(name, col.id, 'COLOR');
957
+ v.setValueForMode(modeId, hexToRgb(hex));
958
+ count++;
959
+ }
960
+ });
961
+ count
962
+ `;
963
+ try {
964
+ const result = figmaUse(`eval "${semanticCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
965
+ spinner.succeed(`Color - Semantic (${result?.trim() || '13'} variables)`);
966
+ } catch { spinner.fail('Color - Semantic failed'); }
967
+
968
+ // Create Spacing
969
+ spinner = ora('Creating Spacing...').start();
970
+ const spacingCode = `
971
+ const spacings = ${JSON.stringify(idsSpacing)};
972
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Spacing');
973
+ if (!col) col = figma.variables.createVariableCollection('Spacing');
974
+ const modeId = col.modes[0].modeId;
975
+ let count = 0;
976
+ Object.entries(spacings).forEach(([name, value]) => {
977
+ const existing = figma.variables.getLocalVariables().find(v => v.name === name);
978
+ if (!existing) {
979
+ const v = figma.variables.createVariable(name, col.id, 'FLOAT');
980
+ v.setValueForMode(modeId, value);
981
+ count++;
982
+ }
983
+ });
984
+ count
985
+ `;
986
+ try {
987
+ const result = figmaUse(`eval "${spacingCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
988
+ spinner.succeed(`Spacing (${result?.trim() || '7'} variables)`);
989
+ } catch { spinner.fail('Spacing failed'); }
990
+
991
+ // Create Typography
992
+ spinner = ora('Creating Typography...').start();
993
+ const typographyCode = `
994
+ const typography = ${JSON.stringify(idsTypography)};
995
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Typography');
996
+ if (!col) col = figma.variables.createVariableCollection('Typography');
997
+ const modeId = col.modes[0].modeId;
998
+ let count = 0;
999
+ Object.entries(typography).forEach(([name, value]) => {
1000
+ const existing = figma.variables.getLocalVariables().find(v => v.name === name);
1001
+ if (!existing) {
1002
+ const v = figma.variables.createVariable(name, col.id, 'FLOAT');
1003
+ v.setValueForMode(modeId, value);
1004
+ count++;
1005
+ }
1006
+ });
1007
+ count
1008
+ `;
1009
+ try {
1010
+ const result = figmaUse(`eval "${typographyCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1011
+ spinner.succeed(`Typography (${result?.trim() || '12'} variables)`);
1012
+ } catch { spinner.fail('Typography failed'); }
1013
+
1014
+ // Create Border Radii
1015
+ spinner = ora('Creating Border Radii...').start();
1016
+ const radiiCode = `
1017
+ const radii = ${JSON.stringify(idsRadii)};
1018
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === 'Border Radii');
1019
+ if (!col) col = figma.variables.createVariableCollection('Border Radii');
1020
+ const modeId = col.modes[0].modeId;
1021
+ let count = 0;
1022
+ Object.entries(radii).forEach(([name, value]) => {
1023
+ const existing = figma.variables.getLocalVariables().find(v => v.name === name);
1024
+ if (!existing) {
1025
+ const v = figma.variables.createVariable(name, col.id, 'FLOAT');
1026
+ v.setValueForMode(modeId, value);
1027
+ count++;
1028
+ }
1029
+ });
1030
+ count
1031
+ `;
1032
+ try {
1033
+ const result = figmaUse(`eval "${radiiCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1034
+ spinner.succeed(`Border Radii (${result?.trim() || '6'} variables)`);
1035
+ } catch { spinner.fail('Border Radii failed'); }
1036
+
1037
+ // Small delay to let spinner render
1038
+ await new Promise(r => setTimeout(r, 100));
1039
+
1040
+ // Summary
1041
+ console.log(chalk.green('\n ✓ IDS Base Design System created!\n'));
1042
+ console.log(chalk.white(' Collections:'));
1043
+ console.log(chalk.gray(' • Color - Primitives (gray, primary, accent)'));
1044
+ console.log(chalk.gray(' • Color - Semantic (background, foreground, border, action, feedback)'));
1045
+ console.log(chalk.gray(' • Spacing (xs to 3xl, 4px base)'));
1046
+ console.log(chalk.gray(' • Typography (sizes + weights)'));
1047
+ console.log(chalk.gray(' • Border Radii (none to full)'));
1048
+ console.log();
1049
+ console.log(chalk.gray(' Total: ~74 variables across 5 collections\n'));
1050
+ console.log(chalk.gray(' Next: ') + chalk.cyan('design-lazyyy-cli tokens components') + chalk.gray(' to add UI components\n'));
1051
+ });
1052
+
1053
+ tokens
1054
+ .command('components')
1055
+ .description('Create IDS Base Components (Button, Input, Card, Badge)')
1056
+ .action(async () => {
1057
+ checkConnection();
1058
+
1059
+ console.log(chalk.cyan('\n IDS Base Components'));
1060
+ console.log(chalk.gray(' by Into Design Systems\n'));
1061
+
1062
+ // Component colors (using IDS Base values)
1063
+ const colors = {
1064
+ primary500: '#3b82f6',
1065
+ primary600: '#2563eb',
1066
+ gray100: '#f4f4f5',
1067
+ gray200: '#e4e4e7',
1068
+ gray500: '#71717a',
1069
+ gray900: '#18181b',
1070
+ white: '#ffffff',
1071
+ success: '#22c55e',
1072
+ warning: '#f59e0b',
1073
+ error: '#ef4444'
1074
+ };
1075
+
1076
+ // First, clean up any existing IDS components
1077
+ let spinner = ora('Cleaning up existing components...').start();
1078
+ const cleanupCode = `
1079
+ const names = ['Button / Primary', 'Button / Secondary', 'Button / Outline', 'Input', 'Card', 'Badge / Default', 'Badge / Success', 'Badge / Warning', 'Badge / Error'];
1080
+ let removed = 0;
1081
+ figma.currentPage.children.forEach(n => {
1082
+ if (names.includes(n.name)) { n.remove(); removed++; }
1083
+ });
1084
+ removed
1085
+ `;
1086
+ try {
1087
+ const removed = figmaUse(`eval "${cleanupCode.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1088
+ spinner.succeed(`Cleaned up ${removed?.trim() || '0'} old elements`);
1089
+ } catch { spinner.succeed('Ready'); }
1090
+
1091
+ // Step 1: Create frames using JSX render (handles fonts)
1092
+ spinner = ora('Creating frames...').start();
1093
+ const jsxComponents = [
1094
+ { jsx: `<Frame name="Button / Primary" bg="${colors.primary500}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="#ffffff">Button</Text></Frame>` },
1095
+ { jsx: `<Frame name="Button / Secondary" bg="${colors.gray100}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="${colors.gray900}">Button</Text></Frame>` },
1096
+ { jsx: `<Frame name="Button / Outline" bg="#ffffff" stroke="${colors.gray200}" px={16} py={10} rounded={8} flex="row"><Text size={14} weight="semibold" color="${colors.gray900}">Button</Text></Frame>` },
1097
+ { jsx: `<Frame name="Input" w={200} bg="#ffffff" stroke="${colors.gray200}" px={12} py={10} rounded={8} flex="row"><Text size={14} color="${colors.gray500}">Placeholder</Text></Frame>` },
1098
+ { jsx: `<Frame name="Card" bg="#ffffff" stroke="${colors.gray200}" p={24} rounded={12} flex="col" gap={8}><Text size={18} weight="semibold" color="${colors.gray900}">Card Title</Text><Text size={14} color="${colors.gray500}">Card description goes here.</Text></Frame>` },
1099
+ { jsx: `<Frame name="Badge / Default" bg="${colors.gray100}" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="${colors.gray900}">Badge</Text></Frame>` },
1100
+ { jsx: `<Frame name="Badge / Success" bg="#dcfce7" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#166534">Success</Text></Frame>` },
1101
+ { jsx: `<Frame name="Badge / Warning" bg="#fef3c7" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#92400e">Warning</Text></Frame>` },
1102
+ { jsx: `<Frame name="Badge / Error" bg="#fee2e2" px={10} py={4} rounded={9999} flex="row"><Text size={12} weight="medium" color="#991b1b">Error</Text></Frame>` }
1103
+ ];
1104
+
1105
+ try {
1106
+ for (const { jsx } of jsxComponents) {
1107
+ execSync(`echo '${jsx}' | figma-use render --stdin`, { stdio: 'pipe' });
1108
+ }
1109
+ spinner.succeed('9 frames created');
1110
+ } catch (e) { spinner.fail('Frame creation failed'); }
1111
+
1112
+ // Step 2: Convert to components one by one with positioning
1113
+ spinner = ora('Converting to components...').start();
1114
+
1115
+ const componentOrder = [
1116
+ { name: 'Button / Primary', row: 0, width: 80, varFill: 'action/primary' },
1117
+ { name: 'Button / Secondary', row: 0, width: 80, varFill: 'background/muted' },
1118
+ { name: 'Button / Outline', row: 0, width: 80, varFill: 'background/default', varStroke: 'border/default' },
1119
+ { name: 'Input', row: 0, width: 200, varFill: 'background/default', varStroke: 'border/default' },
1120
+ { name: 'Card', row: 0, width: 240, varFill: 'background/default', varStroke: 'border/default' },
1121
+ { name: 'Badge / Default', row: 1, width: 60, varFill: 'background/muted' },
1122
+ { name: 'Badge / Success', row: 1, width: 70, varFill: 'feedback/success-muted' },
1123
+ { name: 'Badge / Warning', row: 1, width: 70, varFill: 'feedback/warning-muted' },
1124
+ { name: 'Badge / Error', row: 1, width: 50, varFill: 'feedback/error-muted' }
1125
+ ];
1126
+
1127
+ let row0X = 0, row1X = 0;
1128
+ const gap = 32;
1129
+
1130
+ for (const comp of componentOrder) {
1131
+ const convertSingle = `
1132
+ const f = figma.currentPage.children.find(n => n.name === '${comp.name}' && n.type === 'FRAME');
1133
+ if (f) {
1134
+ const vars = figma.variables.getLocalVariables();
1135
+ const findVar = (name) => vars.find(v => v.name === name);
1136
+ ${comp.varFill ? `
1137
+ const vFill = findVar('${comp.varFill}');
1138
+ if (vFill && f.fills && f.fills.length > 0) {
1139
+ const fills = JSON.parse(JSON.stringify(f.fills));
1140
+ fills[0] = figma.variables.setBoundVariableForPaint(fills[0], 'color', vFill);
1141
+ f.fills = fills;
1142
+ }` : ''}
1143
+ ${comp.varStroke ? `
1144
+ const vStroke = findVar('${comp.varStroke}');
1145
+ if (vStroke && f.strokes && f.strokes.length > 0) {
1146
+ const strokes = JSON.parse(JSON.stringify(f.strokes));
1147
+ strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0], 'color', vStroke);
1148
+ f.strokes = strokes;
1149
+ }` : ''}
1150
+ const c = figma.createComponentFromNode(f);
1151
+ c.x = ${comp.row === 0 ? row0X : row1X};
1152
+ c.y = ${comp.row === 0 ? 0 : 80};
1153
+ }
1154
+ `;
1155
+ try {
1156
+ figmaUse(`eval "${convertSingle.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1157
+ if (comp.row === 0) row0X += comp.width + gap;
1158
+ else row1X += comp.width + 24;
1159
+ } catch {}
1160
+ }
1161
+ spinner.succeed('9 components with variables');
1162
+
1163
+ await new Promise(r => setTimeout(r, 100));
1164
+
1165
+ console.log(chalk.green('\n ✓ IDS Base Components created!\n'));
1166
+ console.log(chalk.white(' Components:'));
1167
+ console.log(chalk.gray(' • Button (Primary, Secondary, Outline)'));
1168
+ console.log(chalk.gray(' • Input'));
1169
+ console.log(chalk.gray(' • Card'));
1170
+ console.log(chalk.gray(' • Badge (Default, Success, Warning, Error)'));
1171
+ console.log();
1172
+ console.log(chalk.gray(' Total: 9 components on canvas\n'));
1173
+ });
1174
+
1175
+ tokens
1176
+ .command('add <name> <value>')
1177
+ .description('Add a single token')
1178
+ .option('-c, --collection <name>', 'Collection name', 'Tokens')
1179
+ .option('-t, --type <type>', 'Type: COLOR, FLOAT, STRING, BOOLEAN (auto-detected if not set)')
1180
+ .action((name, value, options) => {
1181
+ checkConnection();
1182
+
1183
+ const code = `
1184
+ function hexToRgb(hex) {
1185
+ const r = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
1186
+ if (!r) return null;
1187
+ return { r: parseInt(r[1], 16) / 255, g: parseInt(r[2], 16) / 255, b: parseInt(r[3], 16) / 255 };
1188
+ }
1189
+
1190
+ const value = '${value}';
1191
+ let type = '${options.type || ''}';
1192
+ if (!type) {
1193
+ if (value.startsWith('#')) type = 'COLOR';
1194
+ else if (!isNaN(parseFloat(value))) type = 'FLOAT';
1195
+ else if (value === 'true' || value === 'false') type = 'BOOLEAN';
1196
+ else type = 'STRING';
1197
+ }
1198
+
1199
+ let col = figma.variables.getLocalVariableCollections().find(c => c.name === '${options.collection}');
1200
+ if (!col) col = figma.variables.createVariableCollection('${options.collection}');
1201
+ const modeId = col.modes[0].modeId;
1202
+
1203
+ const v = figma.variables.createVariable('${name}', col.id, type);
1204
+ let figmaValue = value;
1205
+ if (type === 'COLOR') figmaValue = hexToRgb(value);
1206
+ else if (type === 'FLOAT') figmaValue = parseFloat(value);
1207
+ else if (type === 'BOOLEAN') figmaValue = value === 'true';
1208
+ v.setValueForMode(modeId, figmaValue);
1209
+
1210
+ 'Created ' + type.toLowerCase() + ' token: ${name}'
1211
+ `;
1212
+
1213
+ try {
1214
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1215
+ console.log(chalk.green(result?.trim() || `✓ Created token: ${name}`));
1216
+ } catch (error) {
1217
+ console.log(chalk.red(`✗ Failed to create token: ${name}`));
1218
+ }
1219
+ });
1220
+
1221
+ // ============ CREATE ============
1222
+
1223
+ const create = program
1224
+ .command('create')
1225
+ .description('Create Figma elements');
1226
+
1227
+ create
1228
+ .command('frame <name>')
1229
+ .description('Create a frame')
1230
+ .option('-w, --width <n>', 'Width', '100')
1231
+ .option('-h, --height <n>', 'Height', '100')
1232
+ .option('-x <n>', 'X position')
1233
+ .option('-y <n>', 'Y position', '0')
1234
+ .option('--fill <color>', 'Fill color')
1235
+ .option('--radius <n>', 'Corner radius')
1236
+ .option('--smart', 'Auto-position to avoid overlaps (default if no -x)')
1237
+ .option('-g, --gap <n>', 'Gap for smart positioning', '100')
1238
+ .action((name, options) => {
1239
+ checkConnection();
1240
+ // Smart positioning: if no X specified, auto-position
1241
+ const useSmartPos = options.smart || options.x === undefined;
1242
+ if (useSmartPos) {
1243
+ const { r, g, b } = options.fill ? hexToRgb(options.fill) : { r: 1, g: 1, b: 1 };
1244
+ let code = `
1245
+ ${smartPosCode(options.gap)}
1246
+ const frame = figma.createFrame();
1247
+ frame.name = '${name}';
1248
+ frame.x = smartX;
1249
+ frame.y = ${options.y};
1250
+ frame.resize(${options.width}, ${options.height});
1251
+ ${options.fill ? `frame.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];` : ''}
1252
+ ${options.radius ? `frame.cornerRadius = ${options.radius};` : ''}
1253
+ figma.currentPage.selection = [frame];
1254
+ '${name} created at (' + smartX + ', ${options.y})'
1255
+ `;
1256
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1257
+ } else {
1258
+ let cmd = `create frame --name "${name}" --x ${options.x} --y ${options.y} --width ${options.width} --height ${options.height}`;
1259
+ if (options.fill) cmd += ` --fill "${options.fill}"`;
1260
+ if (options.radius) cmd += ` --radius ${options.radius}`;
1261
+ figmaUse(cmd);
1262
+ }
1263
+ });
1264
+
1265
+ create
1266
+ .command('icon <name>')
1267
+ .description('Create an icon from Iconify (e.g., lucide:star, mdi:home) - auto-positions')
1268
+ .option('-s, --size <n>', 'Size', '24')
1269
+ .option('-c, --color <color>', 'Color', '#000000')
1270
+ .option('-x <n>', 'X position (auto if not set)')
1271
+ .option('-y <n>', 'Y position', '0')
1272
+ .option('--spacing <n>', 'Gap from other elements', '100')
1273
+ .action((name, options) => {
1274
+ checkConnection();
1275
+ const useSmartPos = options.x === undefined;
1276
+ if (useSmartPos) {
1277
+ // First create icon at 0,0, then move it to smart position
1278
+ const code = `
1279
+ ${smartPosCode(options.spacing)}
1280
+ const icon = figma.currentPage.selection[0];
1281
+ if (icon) {
1282
+ icon.x = smartX;
1283
+ icon.y = ${options.y};
1284
+ 'Icon moved to (' + smartX + ', ${options.y})';
1285
+ } else {
1286
+ 'No icon selected';
1287
+ }
1288
+ `;
1289
+ figmaUse(`create icon ${name} --size ${options.size} --color "${options.color}"`);
1290
+ // Move to smart position after creation
1291
+ setTimeout(() => {
1292
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
1293
+ }, 500);
1294
+ } else {
1295
+ figmaUse(`create icon ${name} --size ${options.size} --color "${options.color}" -x ${options.x} -y ${options.y}`);
1296
+ }
1297
+ });
1298
+
1299
+ create
1300
+ .command('rect [name]')
1301
+ .alias('rectangle')
1302
+ .description('Create a rectangle (auto-positions to avoid overlap)')
1303
+ .option('-w, --width <n>', 'Width', '100')
1304
+ .option('-h, --height <n>', 'Height', '100')
1305
+ .option('-x <n>', 'X position (auto if not set)')
1306
+ .option('-y <n>', 'Y position', '0')
1307
+ .option('--fill <color>', 'Fill color', '#D9D9D9')
1308
+ .option('--stroke <color>', 'Stroke color')
1309
+ .option('--radius <n>', 'Corner radius')
1310
+ .option('--opacity <n>', 'Opacity 0-1')
1311
+ .action((name, options) => {
1312
+ checkConnection();
1313
+ const rectName = name || 'Rectangle';
1314
+ const { r, g, b } = hexToRgb(options.fill);
1315
+ const useSmartPos = options.x === undefined;
1316
+ let code = `
1317
+ ${useSmartPos ? smartPosCode(100) : `const smartX = ${options.x};`}
1318
+ const rect = figma.createRectangle();
1319
+ rect.name = '${rectName}';
1320
+ rect.x = smartX;
1321
+ rect.y = ${options.y};
1322
+ rect.resize(${options.width}, ${options.height});
1323
+ rect.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
1324
+ ${options.radius ? `rect.cornerRadius = ${options.radius};` : ''}
1325
+ ${options.opacity ? `rect.opacity = ${options.opacity};` : ''}
1326
+ ${options.stroke ? `rect.strokes = [{ type: 'SOLID', color: { r: ${hexToRgb(options.stroke).r}, g: ${hexToRgb(options.stroke).g}, b: ${hexToRgb(options.stroke).b} } }]; rect.strokeWeight = 1;` : ''}
1327
+ figma.currentPage.selection = [rect];
1328
+ '${rectName} created at (' + smartX + ', ${options.y})'
1329
+ `;
1330
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1331
+ });
1332
+
1333
+ create
1334
+ .command('ellipse [name]')
1335
+ .alias('circle')
1336
+ .description('Create an ellipse/circle (auto-positions to avoid overlap)')
1337
+ .option('-w, --width <n>', 'Width (diameter)', '100')
1338
+ .option('-h, --height <n>', 'Height (same as width for circle)')
1339
+ .option('-x <n>', 'X position (auto if not set)')
1340
+ .option('-y <n>', 'Y position', '0')
1341
+ .option('--fill <color>', 'Fill color', '#D9D9D9')
1342
+ .option('--stroke <color>', 'Stroke color')
1343
+ .action((name, options) => {
1344
+ checkConnection();
1345
+ const ellipseName = name || 'Ellipse';
1346
+ const height = options.height || options.width;
1347
+ const { r, g, b } = hexToRgb(options.fill);
1348
+ const useSmartPos = options.x === undefined;
1349
+ let code = `
1350
+ ${useSmartPos ? smartPosCode(100) : `const smartX = ${options.x};`}
1351
+ const ellipse = figma.createEllipse();
1352
+ ellipse.name = '${ellipseName}';
1353
+ ellipse.x = smartX;
1354
+ ellipse.y = ${options.y};
1355
+ ellipse.resize(${options.width}, ${height});
1356
+ ellipse.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
1357
+ ${options.stroke ? `ellipse.strokes = [{ type: 'SOLID', color: { r: ${hexToRgb(options.stroke).r}, g: ${hexToRgb(options.stroke).g}, b: ${hexToRgb(options.stroke).b} } }]; ellipse.strokeWeight = 1;` : ''}
1358
+ figma.currentPage.selection = [ellipse];
1359
+ '${ellipseName} created at (' + smartX + ', ${options.y})'
1360
+ `;
1361
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1362
+ });
1363
+
1364
+ create
1365
+ .command('text <content>')
1366
+ .description('Create a text layer (smart positions by default)')
1367
+ .option('-x <n>', 'X position (auto if not set)')
1368
+ .option('-y <n>', 'Y position', '0')
1369
+ .option('-s, --size <n>', 'Font size', '16')
1370
+ .option('-c, --color <color>', 'Text color', '#000000')
1371
+ .option('-w, --weight <weight>', 'Font weight: regular, medium, semibold, bold', 'regular')
1372
+ .option('--font <family>', 'Font family', 'Inter')
1373
+ .option('--width <n>', 'Text box width (auto-width if not set)')
1374
+ .option('--spacing <n>', 'Gap from other elements', '100')
1375
+ .action((content, options) => {
1376
+ checkConnection();
1377
+ const { r, g, b } = hexToRgb(options.color);
1378
+ const weightMap = { regular: 'Regular', medium: 'Medium', semibold: 'Semi Bold', bold: 'Bold' };
1379
+ const fontStyle = weightMap[options.weight.toLowerCase()] || 'Regular';
1380
+ const useSmartPos = options.x === undefined;
1381
+ let code = `
1382
+ (async function() {
1383
+ ${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x};`}
1384
+ await figma.loadFontAsync({ family: '${options.font}', style: '${fontStyle}' });
1385
+ const text = figma.createText();
1386
+ text.fontName = { family: '${options.font}', style: '${fontStyle}' };
1387
+ text.characters = '${content.replace(/'/g, "\\'")}';
1388
+ text.fontSize = ${options.size};
1389
+ text.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
1390
+ text.x = smartX;
1391
+ text.y = ${options.y};
1392
+ ${options.width ? `text.resize(${options.width}, text.height); text.textAutoResize = 'HEIGHT';` : ''}
1393
+ figma.currentPage.selection = [text];
1394
+ return 'Text created at (' + smartX + ', ${options.y})';
1395
+ })()
1396
+ `;
1397
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1398
+ });
1399
+
1400
+ create
1401
+ .command('line')
1402
+ .description('Create a line (smart positions by default)')
1403
+ .option('--x1 <n>', 'Start X (auto if not set)')
1404
+ .option('--y1 <n>', 'Start Y', '0')
1405
+ .option('--x2 <n>', 'End X (auto + length if x1 not set)')
1406
+ .option('--y2 <n>', 'End Y', '0')
1407
+ .option('-l, --length <n>', 'Line length', '100')
1408
+ .option('-c, --color <color>', 'Line color', '#000000')
1409
+ .option('-w, --weight <n>', 'Stroke weight', '1')
1410
+ .option('--spacing <n>', 'Gap from other elements', '100')
1411
+ .action((options) => {
1412
+ checkConnection();
1413
+ const { r, g, b } = hexToRgb(options.color);
1414
+ const useSmartPos = options.x1 === undefined;
1415
+ const lineLength = parseFloat(options.length);
1416
+ let code = `
1417
+ ${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x1};`}
1418
+ const line = figma.createLine();
1419
+ line.x = smartX;
1420
+ line.y = ${options.y1};
1421
+ line.resize(${useSmartPos ? lineLength : `Math.abs(${options.x2 || options.x1 + '+' + lineLength} - ${options.x1}) || ${lineLength}`}, 0);
1422
+ ${options.x2 && options.x1 ? `line.rotation = Math.atan2(${options.y2} - ${options.y1}, ${options.x2} - ${options.x1}) * 180 / Math.PI;` : ''}
1423
+ line.strokes = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }];
1424
+ line.strokeWeight = ${options.weight};
1425
+ figma.currentPage.selection = [line];
1426
+ 'Line created at (' + smartX + ', ${options.y1}) with length ${lineLength}'
1427
+ `;
1428
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1429
+ });
1430
+
1431
+ create
1432
+ .command('component [name]')
1433
+ .description('Convert selection to component')
1434
+ .action((name) => {
1435
+ checkConnection();
1436
+ const compName = name || 'Component';
1437
+ let code = `
1438
+ const sel = figma.currentPage.selection;
1439
+ if (sel.length === 0) 'No selection';
1440
+ else if (sel.length === 1) {
1441
+ const comp = figma.createComponentFromNode(sel[0]);
1442
+ comp.name = '${compName}';
1443
+ figma.currentPage.selection = [comp];
1444
+ 'Component created: ' + comp.name;
1445
+ } else {
1446
+ const group = figma.group(sel, figma.currentPage);
1447
+ const comp = figma.createComponentFromNode(group);
1448
+ comp.name = '${compName}';
1449
+ figma.currentPage.selection = [comp];
1450
+ 'Component created from ' + sel.length + ' elements: ' + comp.name;
1451
+ }
1452
+ `;
1453
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1454
+ });
1455
+
1456
+ create
1457
+ .command('group [name]')
1458
+ .description('Group current selection')
1459
+ .action((name) => {
1460
+ checkConnection();
1461
+ const groupName = name || 'Group';
1462
+ let code = `
1463
+ const sel = figma.currentPage.selection;
1464
+ if (sel.length < 2) 'Select 2+ elements to group';
1465
+ else {
1466
+ const group = figma.group(sel, figma.currentPage);
1467
+ group.name = '${groupName}';
1468
+ figma.currentPage.selection = [group];
1469
+ 'Grouped ' + sel.length + ' elements';
1470
+ }
1471
+ `;
1472
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1473
+ });
1474
+
1475
+ create
1476
+ .command('autolayout [name]')
1477
+ .alias('al')
1478
+ .description('Create an auto-layout frame (smart positions by default)')
1479
+ .option('-d, --direction <dir>', 'Direction: row, col', 'row')
1480
+ .option('-g, --gap <n>', 'Gap between items', '8')
1481
+ .option('-p, --padding <n>', 'Padding', '16')
1482
+ .option('-x <n>', 'X position (auto if not set)')
1483
+ .option('-y <n>', 'Y position', '0')
1484
+ .option('--fill <color>', 'Fill color')
1485
+ .option('--radius <n>', 'Corner radius')
1486
+ .option('--spacing <n>', 'Gap from other elements', '100')
1487
+ .action((name, options) => {
1488
+ checkConnection();
1489
+ const frameName = name || 'Auto Layout';
1490
+ const layoutMode = options.direction === 'col' ? 'VERTICAL' : 'HORIZONTAL';
1491
+ const useSmartPos = options.x === undefined;
1492
+ let code = `
1493
+ ${useSmartPos ? smartPosCode(options.spacing) : `const smartX = ${options.x};`}
1494
+ const frame = figma.createFrame();
1495
+ frame.name = '${frameName}';
1496
+ frame.x = smartX;
1497
+ frame.y = ${options.y};
1498
+ frame.layoutMode = '${layoutMode}';
1499
+ frame.primaryAxisSizingMode = 'AUTO';
1500
+ frame.counterAxisSizingMode = 'AUTO';
1501
+ frame.itemSpacing = ${options.gap};
1502
+ frame.paddingTop = ${options.padding};
1503
+ frame.paddingRight = ${options.padding};
1504
+ frame.paddingBottom = ${options.padding};
1505
+ frame.paddingLeft = ${options.padding};
1506
+ ${options.fill ? `frame.fills = [{ type: 'SOLID', color: { r: ${hexToRgb(options.fill).r}, g: ${hexToRgb(options.fill).g}, b: ${hexToRgb(options.fill).b} } }];` : 'frame.fills = [];'}
1507
+ ${options.radius ? `frame.cornerRadius = ${options.radius};` : ''}
1508
+ figma.currentPage.selection = [frame];
1509
+ 'Auto-layout frame created at (' + smartX + ', ${options.y})'
1510
+ `;
1511
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1512
+ });
1513
+
1514
+ // ============ CANVAS ============
1515
+
1516
+ const canvas = program
1517
+ .command('canvas')
1518
+ .description('Canvas awareness and smart positioning');
1519
+
1520
+ canvas
1521
+ .command('info')
1522
+ .description('Show canvas info (bounds, element count, free space)')
1523
+ .action(() => {
1524
+ checkConnection();
1525
+ let code = `
1526
+ const children = figma.currentPage.children;
1527
+ if (children.length === 0) {
1528
+ JSON.stringify({ empty: true, message: 'Canvas is empty', nextX: 0, nextY: 0 });
1529
+ } else {
1530
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1531
+ children.forEach(n => {
1532
+ minX = Math.min(minX, n.x);
1533
+ minY = Math.min(minY, n.y);
1534
+ maxX = Math.max(maxX, n.x + n.width);
1535
+ maxY = Math.max(maxY, n.y + n.height);
1536
+ });
1537
+ JSON.stringify({
1538
+ elements: children.length,
1539
+ bounds: { x: Math.round(minX), y: Math.round(minY), width: Math.round(maxX - minX), height: Math.round(maxY - minY) },
1540
+ nextX: Math.round(maxX + 100),
1541
+ nextY: 0,
1542
+ frames: children.filter(n => n.type === 'FRAME').length,
1543
+ components: children.filter(n => n.type === 'COMPONENT').length
1544
+ }, null, 2);
1545
+ }
1546
+ `;
1547
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1548
+ });
1549
+
1550
+ canvas
1551
+ .command('next')
1552
+ .description('Get next free position on canvas (no overlap)')
1553
+ .option('-g, --gap <n>', 'Gap from existing elements', '100')
1554
+ .option('-d, --direction <dir>', 'Direction: right, below', 'right')
1555
+ .action((options) => {
1556
+ checkConnection();
1557
+ let code = `
1558
+ const children = figma.currentPage.children;
1559
+ const gap = ${options.gap};
1560
+ if (children.length === 0) {
1561
+ JSON.stringify({ x: 0, y: 0 });
1562
+ } else {
1563
+ ${options.direction === 'below' ? `
1564
+ let maxY = -Infinity;
1565
+ children.forEach(n => { maxY = Math.max(maxY, n.y + n.height); });
1566
+ JSON.stringify({ x: 0, y: Math.round(maxY + gap) });
1567
+ ` : `
1568
+ let maxX = -Infinity;
1569
+ children.forEach(n => { maxX = Math.max(maxX, n.x + n.width); });
1570
+ JSON.stringify({ x: Math.round(maxX + gap), y: 0 });
1571
+ `}
1572
+ }
1573
+ `;
1574
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1575
+ });
1576
+
1577
+ // ============ BIND (Variables) ============
1578
+
1579
+ const bind = program
1580
+ .command('bind')
1581
+ .description('Bind variables to node properties');
1582
+
1583
+ bind
1584
+ .command('fill <varName>')
1585
+ .description('Bind color variable to fill')
1586
+ .option('-n, --node <id>', 'Node ID (uses selection if not set)')
1587
+ .action((varName, options) => {
1588
+ checkConnection();
1589
+ const nodeSelector = options.node
1590
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1591
+ : `const nodes = figma.currentPage.selection;`;
1592
+ let code = `
1593
+ ${nodeSelector}
1594
+ const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
1595
+ if (!v) 'Variable not found: ${varName}';
1596
+ else if (nodes.length === 0) 'No node selected';
1597
+ else {
1598
+ nodes.forEach(n => {
1599
+ if ('fills' in n && n.fills.length > 0) {
1600
+ const newFill = figma.variables.setBoundVariableForPaint(n.fills[0], 'color', v);
1601
+ n.fills = [newFill];
1602
+ }
1603
+ });
1604
+ 'Bound ' + v.name + ' to fill on ' + nodes.length + ' elements';
1605
+ }
1606
+ `;
1607
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1608
+ });
1609
+
1610
+ bind
1611
+ .command('stroke <varName>')
1612
+ .description('Bind color variable to stroke')
1613
+ .option('-n, --node <id>', 'Node ID')
1614
+ .action((varName, options) => {
1615
+ checkConnection();
1616
+ const nodeSelector = options.node
1617
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1618
+ : `const nodes = figma.currentPage.selection;`;
1619
+ let code = `
1620
+ ${nodeSelector}
1621
+ const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
1622
+ if (!v) 'Variable not found: ${varName}';
1623
+ else if (nodes.length === 0) 'No node selected';
1624
+ else {
1625
+ nodes.forEach(n => {
1626
+ if ('strokes' in n) {
1627
+ const stroke = n.strokes[0] || { type: 'SOLID', color: {r:0,g:0,b:0} };
1628
+ const newStroke = figma.variables.setBoundVariableForPaint(stroke, 'color', v);
1629
+ n.strokes = [newStroke];
1630
+ }
1631
+ });
1632
+ 'Bound ' + v.name + ' to stroke on ' + nodes.length + ' elements';
1633
+ }
1634
+ `;
1635
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1636
+ });
1637
+
1638
+ bind
1639
+ .command('radius <varName>')
1640
+ .description('Bind number variable to corner radius')
1641
+ .option('-n, --node <id>', 'Node ID')
1642
+ .action((varName, options) => {
1643
+ checkConnection();
1644
+ const nodeSelector = options.node
1645
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1646
+ : `const nodes = figma.currentPage.selection;`;
1647
+ let code = `
1648
+ ${nodeSelector}
1649
+ const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
1650
+ if (!v) 'Variable not found: ${varName}';
1651
+ else if (nodes.length === 0) 'No node selected';
1652
+ else {
1653
+ nodes.forEach(n => {
1654
+ if ('cornerRadius' in n) n.setBoundVariable('cornerRadius', v);
1655
+ });
1656
+ 'Bound ' + v.name + ' to radius on ' + nodes.length + ' elements';
1657
+ }
1658
+ `;
1659
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1660
+ });
1661
+
1662
+ bind
1663
+ .command('gap <varName>')
1664
+ .description('Bind number variable to auto-layout gap')
1665
+ .option('-n, --node <id>', 'Node ID')
1666
+ .action((varName, options) => {
1667
+ checkConnection();
1668
+ const nodeSelector = options.node
1669
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1670
+ : `const nodes = figma.currentPage.selection;`;
1671
+ let code = `
1672
+ ${nodeSelector}
1673
+ const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
1674
+ if (!v) 'Variable not found: ${varName}';
1675
+ else if (nodes.length === 0) 'No node selected';
1676
+ else {
1677
+ nodes.forEach(n => {
1678
+ if ('itemSpacing' in n) n.setBoundVariable('itemSpacing', v);
1679
+ });
1680
+ 'Bound ' + v.name + ' to gap on ' + nodes.length + ' elements';
1681
+ }
1682
+ `;
1683
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1684
+ });
1685
+
1686
+ bind
1687
+ .command('padding <varName>')
1688
+ .description('Bind number variable to padding')
1689
+ .option('-n, --node <id>', 'Node ID')
1690
+ .option('-s, --side <side>', 'Side: top, right, bottom, left, all', 'all')
1691
+ .action((varName, options) => {
1692
+ checkConnection();
1693
+ const nodeSelector = options.node
1694
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1695
+ : `const nodes = figma.currentPage.selection;`;
1696
+ const sides = options.side === 'all'
1697
+ ? ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
1698
+ : [`padding${options.side.charAt(0).toUpperCase() + options.side.slice(1)}`];
1699
+ let code = `
1700
+ ${nodeSelector}
1701
+ const v = figma.variables.getLocalVariables().find(v => v.name === '${varName}' || v.name.endsWith('/${varName}'));
1702
+ if (!v) 'Variable not found: ${varName}';
1703
+ else if (nodes.length === 0) 'No node selected';
1704
+ else {
1705
+ const sides = ${JSON.stringify(sides)};
1706
+ nodes.forEach(n => {
1707
+ sides.forEach(side => { if (side in n) n.setBoundVariable(side, v); });
1708
+ });
1709
+ 'Bound ' + v.name + ' to padding on ' + nodes.length + ' elements';
1710
+ }
1711
+ `;
1712
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1713
+ });
1714
+
1715
+ bind
1716
+ .command('list')
1717
+ .description('List available variables for binding')
1718
+ .option('-t, --type <type>', 'Filter: COLOR, FLOAT')
1719
+ .action((options) => {
1720
+ checkConnection();
1721
+ let code = `
1722
+ const vars = figma.variables.getLocalVariables();
1723
+ const filtered = vars${options.type ? `.filter(v => v.resolvedType === '${options.type.toUpperCase()}')` : ''};
1724
+ filtered.map(v => v.resolvedType.padEnd(8) + ' ' + v.name).join('\\n') || 'No variables';
1725
+ `;
1726
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1727
+ });
1728
+
1729
+ // ============ SIZING ============
1730
+
1731
+ const sizing = program
1732
+ .command('sizing')
1733
+ .description('Control sizing in auto-layout');
1734
+
1735
+ sizing
1736
+ .command('hug')
1737
+ .description('Set to hug contents')
1738
+ .option('-a, --axis <axis>', 'Axis: both, h, v', 'both')
1739
+ .action((options) => {
1740
+ checkConnection();
1741
+ let code = `
1742
+ const nodes = figma.currentPage.selection;
1743
+ if (nodes.length === 0) 'No selection';
1744
+ else {
1745
+ nodes.forEach(n => {
1746
+ ${options.axis === 'h' || options.axis === 'both' ? `if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'HUG';` : ''}
1747
+ ${options.axis === 'v' || options.axis === 'both' ? `if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'HUG';` : ''}
1748
+ if (n.layoutMode) { n.primaryAxisSizingMode = 'AUTO'; n.counterAxisSizingMode = 'AUTO'; }
1749
+ });
1750
+ 'Set hug on ' + nodes.length + ' elements';
1751
+ }
1752
+ `;
1753
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1754
+ });
1755
+
1756
+ sizing
1757
+ .command('fill')
1758
+ .description('Set to fill container')
1759
+ .option('-a, --axis <axis>', 'Axis: both, h, v', 'both')
1760
+ .action((options) => {
1761
+ checkConnection();
1762
+ let code = `
1763
+ const nodes = figma.currentPage.selection;
1764
+ if (nodes.length === 0) 'No selection';
1765
+ else {
1766
+ nodes.forEach(n => {
1767
+ ${options.axis === 'h' || options.axis === 'both' ? `if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'FILL';` : ''}
1768
+ ${options.axis === 'v' || options.axis === 'both' ? `if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'FILL';` : ''}
1769
+ });
1770
+ 'Set fill on ' + nodes.length + ' elements';
1771
+ }
1772
+ `;
1773
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1774
+ });
1775
+
1776
+ sizing
1777
+ .command('fixed <width> [height]')
1778
+ .description('Set to fixed size')
1779
+ .action((width, height) => {
1780
+ checkConnection();
1781
+ const h = height || width;
1782
+ let code = `
1783
+ const nodes = figma.currentPage.selection;
1784
+ if (nodes.length === 0) 'No selection';
1785
+ else {
1786
+ nodes.forEach(n => {
1787
+ if ('layoutSizingHorizontal' in n) n.layoutSizingHorizontal = 'FIXED';
1788
+ if ('layoutSizingVertical' in n) n.layoutSizingVertical = 'FIXED';
1789
+ if ('resize' in n) n.resize(${width}, ${h});
1790
+ });
1791
+ 'Set fixed ${width}x${h} on ' + nodes.length + ' elements';
1792
+ }
1793
+ `;
1794
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1795
+ });
1796
+
1797
+ // ============ LAYOUT SHORTCUTS ============
1798
+
1799
+ program
1800
+ .command('padding <value> [r] [b] [l]')
1801
+ .alias('pad')
1802
+ .description('Set padding (CSS-style: 1-4 values)')
1803
+ .action((value, r, b, l) => {
1804
+ checkConnection();
1805
+ let top = value, right = r || value, bottom = b || value, left = l || r || value;
1806
+ if (!r) { right = value; bottom = value; left = value; }
1807
+ else if (!b) { bottom = value; left = r; }
1808
+ else if (!l) { left = r; }
1809
+ let code = `
1810
+ const nodes = figma.currentPage.selection;
1811
+ if (nodes.length === 0) 'No selection';
1812
+ else {
1813
+ nodes.forEach(n => {
1814
+ if ('paddingTop' in n) {
1815
+ n.paddingTop = ${top}; n.paddingRight = ${right};
1816
+ n.paddingBottom = ${bottom}; n.paddingLeft = ${left};
1817
+ }
1818
+ });
1819
+ 'Set padding on ' + nodes.length + ' elements';
1820
+ }
1821
+ `;
1822
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1823
+ });
1824
+
1825
+ program
1826
+ .command('gap <value>')
1827
+ .description('Set auto-layout gap')
1828
+ .action((value) => {
1829
+ checkConnection();
1830
+ let code = `
1831
+ const nodes = figma.currentPage.selection;
1832
+ if (nodes.length === 0) 'No selection';
1833
+ else {
1834
+ nodes.forEach(n => { if ('itemSpacing' in n) n.itemSpacing = ${value}; });
1835
+ 'Set gap ${value} on ' + nodes.length + ' elements';
1836
+ }
1837
+ `;
1838
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1839
+ });
1840
+
1841
+ program
1842
+ .command('align <alignment>')
1843
+ .description('Align items: start, center, end, stretch')
1844
+ .action((alignment) => {
1845
+ checkConnection();
1846
+ const map = { start: 'MIN', center: 'CENTER', end: 'MAX', stretch: 'STRETCH' };
1847
+ const val = map[alignment.toLowerCase()] || 'CENTER';
1848
+ let code = `
1849
+ const nodes = figma.currentPage.selection;
1850
+ if (nodes.length === 0) 'No selection';
1851
+ else {
1852
+ nodes.forEach(n => {
1853
+ if ('primaryAxisAlignItems' in n) n.primaryAxisAlignItems = '${val}';
1854
+ if ('counterAxisAlignItems' in n) n.counterAxisAlignItems = '${val}';
1855
+ });
1856
+ 'Aligned ' + nodes.length + ' elements to ${alignment}';
1857
+ }
1858
+ `;
1859
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1860
+ });
1861
+
1862
+ // ============ SELECT ============
1863
+
1864
+ program
1865
+ .command('select <nodeId>')
1866
+ .description('Select a node by ID')
1867
+ .action((nodeId) => {
1868
+ checkConnection();
1869
+ figmaUse(`select "${nodeId}"`);
1870
+ });
1871
+
1872
+ // ============ DELETE ============
1873
+
1874
+ program
1875
+ .command('delete [nodeId]')
1876
+ .alias('remove')
1877
+ .description('Delete node by ID or current selection')
1878
+ .action((nodeId) => {
1879
+ checkConnection();
1880
+ if (nodeId) {
1881
+ let code = `
1882
+ const node = figma.getNodeById('${nodeId}');
1883
+ if (node) { node.remove(); 'Deleted: ${nodeId}'; } else { 'Node not found: ${nodeId}'; }
1884
+ `;
1885
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1886
+ } else {
1887
+ let code = `
1888
+ const sel = figma.currentPage.selection;
1889
+ if (sel.length === 0) 'No selection';
1890
+ else { const count = sel.length; sel.forEach(n => n.remove()); 'Deleted ' + count + ' elements'; }
1891
+ `;
1892
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1893
+ }
1894
+ });
1895
+
1896
+ // ============ DUPLICATE ============
1897
+
1898
+ program
1899
+ .command('duplicate [nodeId]')
1900
+ .alias('dup')
1901
+ .description('Duplicate node by ID or current selection')
1902
+ .option('--offset <n>', 'Offset from original', '20')
1903
+ .action((nodeId, options) => {
1904
+ checkConnection();
1905
+ if (nodeId) {
1906
+ let code = `
1907
+ const node = figma.getNodeById('${nodeId}');
1908
+ if (node) { const clone = node.clone(); clone.x += ${options.offset}; clone.y += ${options.offset}; figma.currentPage.selection = [clone]; 'Duplicated: ' + clone.id; } else { 'Node not found'; }
1909
+ `;
1910
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1911
+ } else {
1912
+ let code = `
1913
+ const sel = figma.currentPage.selection;
1914
+ if (sel.length === 0) 'No selection';
1915
+ else { const clones = sel.map(n => { const c = n.clone(); c.x += ${options.offset}; c.y += ${options.offset}; return c; }); figma.currentPage.selection = clones; 'Duplicated ' + clones.length + ' elements'; }
1916
+ `;
1917
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1918
+ }
1919
+ });
1920
+
1921
+ // ============ SET ============
1922
+
1923
+ const set = program
1924
+ .command('set')
1925
+ .description('Set properties on selection or node');
1926
+
1927
+ set
1928
+ .command('fill <color>')
1929
+ .description('Set fill color')
1930
+ .option('-n, --node <id>', 'Node ID (uses selection if not set)')
1931
+ .action((color, options) => {
1932
+ checkConnection();
1933
+ const { r, g, b } = hexToRgb(color);
1934
+ const nodeSelector = options.node
1935
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1936
+ : `const nodes = figma.currentPage.selection;`;
1937
+ let code = `
1938
+ ${nodeSelector}
1939
+ if (nodes.length === 0) 'No node found';
1940
+ else { nodes.forEach(n => { if ('fills' in n) n.fills = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }]; }); 'Fill set on ' + nodes.length + ' elements'; }
1941
+ `;
1942
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1943
+ });
1944
+
1945
+ set
1946
+ .command('stroke <color>')
1947
+ .description('Set stroke color')
1948
+ .option('-n, --node <id>', 'Node ID')
1949
+ .option('-w, --weight <n>', 'Stroke weight', '1')
1950
+ .action((color, options) => {
1951
+ checkConnection();
1952
+ const { r, g, b } = hexToRgb(color);
1953
+ const nodeSelector = options.node
1954
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1955
+ : `const nodes = figma.currentPage.selection;`;
1956
+ let code = `
1957
+ ${nodeSelector}
1958
+ if (nodes.length === 0) 'No node found';
1959
+ else { nodes.forEach(n => { if ('strokes' in n) { n.strokes = [{ type: 'SOLID', color: { r: ${r}, g: ${g}, b: ${b} } }]; n.strokeWeight = ${options.weight}; } }); 'Stroke set on ' + nodes.length + ' elements'; }
1960
+ `;
1961
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1962
+ });
1963
+
1964
+ set
1965
+ .command('radius <value>')
1966
+ .description('Set corner radius')
1967
+ .option('-n, --node <id>', 'Node ID')
1968
+ .action((value, options) => {
1969
+ checkConnection();
1970
+ const nodeSelector = options.node
1971
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1972
+ : `const nodes = figma.currentPage.selection;`;
1973
+ let code = `
1974
+ ${nodeSelector}
1975
+ if (nodes.length === 0) 'No node found';
1976
+ else { nodes.forEach(n => { if ('cornerRadius' in n) n.cornerRadius = ${value}; }); 'Radius set on ' + nodes.length + ' elements'; }
1977
+ `;
1978
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1979
+ });
1980
+
1981
+ set
1982
+ .command('size <width> <height>')
1983
+ .description('Set size')
1984
+ .option('-n, --node <id>', 'Node ID')
1985
+ .action((width, height, options) => {
1986
+ checkConnection();
1987
+ const nodeSelector = options.node
1988
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
1989
+ : `const nodes = figma.currentPage.selection;`;
1990
+ let code = `
1991
+ ${nodeSelector}
1992
+ if (nodes.length === 0) 'No node found';
1993
+ else { nodes.forEach(n => { if ('resize' in n) n.resize(${width}, ${height}); }); 'Size set on ' + nodes.length + ' elements'; }
1994
+ `;
1995
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
1996
+ });
1997
+
1998
+ set
1999
+ .command('pos <x> <y>')
2000
+ .alias('position')
2001
+ .description('Set position')
2002
+ .option('-n, --node <id>', 'Node ID')
2003
+ .action((x, y, options) => {
2004
+ checkConnection();
2005
+ const nodeSelector = options.node
2006
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
2007
+ : `const nodes = figma.currentPage.selection;`;
2008
+ let code = `
2009
+ ${nodeSelector}
2010
+ if (nodes.length === 0) 'No node found';
2011
+ else { nodes.forEach(n => { n.x = ${x}; n.y = ${y}; }); 'Position set on ' + nodes.length + ' elements'; }
2012
+ `;
2013
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2014
+ });
2015
+
2016
+ set
2017
+ .command('opacity <value>')
2018
+ .description('Set opacity (0-1)')
2019
+ .option('-n, --node <id>', 'Node ID')
2020
+ .action((value, options) => {
2021
+ checkConnection();
2022
+ const nodeSelector = options.node
2023
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
2024
+ : `const nodes = figma.currentPage.selection;`;
2025
+ let code = `
2026
+ ${nodeSelector}
2027
+ if (nodes.length === 0) 'No node found';
2028
+ else { nodes.forEach(n => { if ('opacity' in n) n.opacity = ${value}; }); 'Opacity set on ' + nodes.length + ' elements'; }
2029
+ `;
2030
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2031
+ });
2032
+
2033
+ set
2034
+ .command('name <name>')
2035
+ .description('Rename node')
2036
+ .option('-n, --node <id>', 'Node ID')
2037
+ .action((name, options) => {
2038
+ checkConnection();
2039
+ const nodeSelector = options.node
2040
+ ? `const nodes = [figma.getNodeById('${options.node}')].filter(Boolean);`
2041
+ : `const nodes = figma.currentPage.selection;`;
2042
+ let code = `
2043
+ ${nodeSelector}
2044
+ if (nodes.length === 0) 'No node found';
2045
+ else { nodes.forEach(n => { n.name = '${name}'; }); 'Renamed ' + nodes.length + ' elements to ${name}'; }
2046
+ `;
2047
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2048
+ });
2049
+
2050
+ set
2051
+ .command('autolayout <direction>')
2052
+ .alias('al')
2053
+ .description('Apply auto-layout to selection (row/col)')
2054
+ .option('-g, --gap <n>', 'Gap between items', '8')
2055
+ .option('-p, --padding <n>', 'Padding')
2056
+ .action((direction, options) => {
2057
+ checkConnection();
2058
+ const layoutMode = direction === 'col' || direction === 'vertical' ? 'VERTICAL' : 'HORIZONTAL';
2059
+ let code = `
2060
+ const sel = figma.currentPage.selection;
2061
+ if (sel.length === 0) 'No selection';
2062
+ else {
2063
+ sel.forEach(n => {
2064
+ if (n.type === 'FRAME' || n.type === 'COMPONENT') {
2065
+ n.layoutMode = '${layoutMode}';
2066
+ n.primaryAxisSizingMode = 'AUTO';
2067
+ n.counterAxisSizingMode = 'AUTO';
2068
+ n.itemSpacing = ${options.gap};
2069
+ ${options.padding ? `n.paddingTop = n.paddingRight = n.paddingBottom = n.paddingLeft = ${options.padding};` : ''}
2070
+ }
2071
+ });
2072
+ 'Auto-layout applied to ' + sel.length + ' frames';
2073
+ }
2074
+ `;
2075
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2076
+ });
2077
+
2078
+ // ============ ARRANGE ============
2079
+
2080
+ program
2081
+ .command('arrange')
2082
+ .description('Arrange frames on canvas')
2083
+ .option('-g, --gap <n>', 'Gap between frames', '100')
2084
+ .option('-c, --cols <n>', 'Number of columns (0 = single row)', '0')
2085
+ .action((options) => {
2086
+ checkConnection();
2087
+ let code = `
2088
+ const frames = figma.currentPage.children.filter(n => n.type === 'FRAME' || n.type === 'COMPONENT');
2089
+ if (frames.length === 0) 'No frames to arrange';
2090
+ else {
2091
+ frames.sort((a, b) => a.name.localeCompare(b.name));
2092
+ let x = 0, y = 0, rowHeight = 0, col = 0;
2093
+ const gap = ${options.gap};
2094
+ const cols = ${options.cols};
2095
+ frames.forEach((f, i) => {
2096
+ f.x = x;
2097
+ f.y = y;
2098
+ rowHeight = Math.max(rowHeight, f.height);
2099
+ if (cols > 0 && ++col >= cols) {
2100
+ col = 0;
2101
+ x = 0;
2102
+ y += rowHeight + gap;
2103
+ rowHeight = 0;
2104
+ } else {
2105
+ x += f.width + gap;
2106
+ }
2107
+ });
2108
+ 'Arranged ' + frames.length + ' frames';
2109
+ }
2110
+ `;
2111
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2112
+ });
2113
+
2114
+ // ============ GET ============
2115
+
2116
+ program
2117
+ .command('get [nodeId]')
2118
+ .description('Get properties of node or selection')
2119
+ .action((nodeId) => {
2120
+ checkConnection();
2121
+ const nodeSelector = nodeId
2122
+ ? `const node = figma.getNodeById('${nodeId}');`
2123
+ : `const node = figma.currentPage.selection[0];`;
2124
+ let code = `
2125
+ ${nodeSelector}
2126
+ if (!node) 'No node found';
2127
+ else JSON.stringify({
2128
+ id: node.id,
2129
+ name: node.name,
2130
+ type: node.type,
2131
+ x: node.x,
2132
+ y: node.y,
2133
+ width: node.width,
2134
+ height: node.height,
2135
+ visible: node.visible,
2136
+ locked: node.locked,
2137
+ opacity: node.opacity,
2138
+ rotation: node.rotation,
2139
+ cornerRadius: node.cornerRadius,
2140
+ layoutMode: node.layoutMode,
2141
+ fills: node.fills?.length,
2142
+ strokes: node.strokes?.length,
2143
+ children: node.children?.length
2144
+ }, null, 2)
2145
+ `;
2146
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2147
+ });
2148
+
2149
+ // ============ FIND ============
2150
+
2151
+ program
2152
+ .command('find <name>')
2153
+ .description('Find nodes by name (partial match)')
2154
+ .option('-t, --type <type>', 'Filter by type (FRAME, TEXT, RECTANGLE, etc.)')
2155
+ .option('-l, --limit <n>', 'Limit results', '20')
2156
+ .action((name, options) => {
2157
+ checkConnection();
2158
+ let code = `
2159
+ const results = [];
2160
+ function search(node) {
2161
+ if (node.name && node.name.toLowerCase().includes('${name.toLowerCase()}')) {
2162
+ ${options.type ? `if (node.type === '${options.type.toUpperCase()}')` : ''}
2163
+ results.push({ id: node.id, name: node.name, type: node.type });
2164
+ }
2165
+ if (node.children && results.length < ${options.limit}) {
2166
+ node.children.forEach(search);
2167
+ }
2168
+ }
2169
+ search(figma.currentPage);
2170
+ results.length === 0 ? 'No nodes found matching "${name}"' : results.slice(0, ${options.limit}).map(r => r.id + ' [' + r.type + '] ' + r.name).join('\\n')
2171
+ `;
2172
+ figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: false });
2173
+ });
2174
+
2175
+ // ============ RENDER ============
2176
+
2177
+ program
2178
+ .command('render <jsx>')
2179
+ .description('Render JSX to Figma')
2180
+ .action((jsx) => {
2181
+ checkConnection();
2182
+ execSync(`echo '${jsx}' | figma-use render --stdin`, { stdio: 'inherit' });
2183
+ });
2184
+
2185
+ // ============ EXPORT ============
2186
+
2187
+ const exp = program
2188
+ .command('export')
2189
+ .description('Export from Figma');
2190
+
2191
+ exp
2192
+ .command('screenshot')
2193
+ .description('Take a screenshot')
2194
+ .option('-o, --output <file>', 'Output file', 'screenshot.png')
2195
+ .action((options) => {
2196
+ checkConnection();
2197
+ figmaUse(`export screenshot --output "${options.output}"`);
2198
+ });
2199
+
2200
+ exp
2201
+ .command('css')
2202
+ .description('Export variables as CSS custom properties')
2203
+ .action(() => {
2204
+ checkConnection();
2205
+ const code = `
2206
+ const vars = figma.variables.getLocalVariables();
2207
+ const css = vars.map(v => {
2208
+ const val = Object.values(v.valuesByMode)[0];
2209
+ if (v.resolvedType === 'COLOR') {
2210
+ const hex = '#' + [val.r, val.g, val.b].map(n => Math.round(n*255).toString(16).padStart(2,'0')).join('');
2211
+ return ' --' + v.name.replace(/\\//g, '-') + ': ' + hex + ';';
2212
+ }
2213
+ return ' --' + v.name.replace(/\\//g, '-') + ': ' + val + (v.resolvedType === 'FLOAT' ? 'px' : '') + ';';
2214
+ }).join('\\n');
2215
+ ':root {\\n' + css + '\\n}'
2216
+ `;
2217
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
2218
+ console.log(result);
2219
+ });
2220
+
2221
+ exp
2222
+ .command('tailwind')
2223
+ .description('Export color variables as Tailwind config')
2224
+ .action(() => {
2225
+ checkConnection();
2226
+ const code = `
2227
+ const vars = figma.variables.getLocalVariables().filter(v => v.resolvedType === 'COLOR');
2228
+ const colors = {};
2229
+ vars.forEach(v => {
2230
+ const val = Object.values(v.valuesByMode)[0];
2231
+ const hex = '#' + [val.r, val.g, val.b].map(n => Math.round(n*255).toString(16).padStart(2,'0')).join('');
2232
+ const parts = v.name.split('/');
2233
+ if (parts.length === 2) {
2234
+ if (!colors[parts[0]]) colors[parts[0]] = {};
2235
+ colors[parts[0]][parts[1]] = hex;
2236
+ } else {
2237
+ colors[v.name.replace(/\\//g, '-')] = hex;
2238
+ }
2239
+ });
2240
+ JSON.stringify({ theme: { extend: { colors } } }, null, 2)
2241
+ `;
2242
+ const result = figmaUse(`eval "${code.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, { silent: true });
2243
+ console.log(result);
2244
+ });
2245
+
2246
+ // ============ EVAL ============
2247
+
2248
+ program
2249
+ .command('eval <code>')
2250
+ .description('Execute JavaScript in Figma plugin context')
2251
+ .action((code) => {
2252
+ checkConnection();
2253
+ figmaUse(`eval "${code.replace(/"/g, '\\"')}"`);
2254
+ });
2255
+
2256
+ // ============ PASSTHROUGH ============
2257
+
2258
+ program
2259
+ .command('raw <command...>')
2260
+ .description('Run raw figma-use command')
2261
+ .action((command) => {
2262
+ checkConnection();
2263
+ figmaUse(command.join(' '));
2264
+ });
2265
+
2266
+ // ============ FIGJAM ============
2267
+
2268
+ const figjam = program
2269
+ .command('figjam')
2270
+ .alias('fj')
2271
+ .description('FigJam commands (sticky notes, shapes, connectors)');
2272
+
2273
+ // Helper: Get FigJam client
2274
+ async function getFigJamClient(pageTitle) {
2275
+ const client = new FigJamClient();
2276
+ try {
2277
+ const pages = await FigJamClient.listPages();
2278
+ if (pages.length === 0) {
2279
+ console.log(chalk.red('\n✗ No FigJam pages open\n'));
2280
+ console.log(chalk.gray(' Open a FigJam file in Figma Desktop first.\n'));
2281
+ process.exit(1);
2282
+ }
2283
+
2284
+ const targetPage = pageTitle || pages[0].title;
2285
+ await client.connect(targetPage);
2286
+ return client;
2287
+ } catch (error) {
2288
+ console.log(chalk.red('\n✗ ' + error.message + '\n'));
2289
+ process.exit(1);
2290
+ }
2291
+ }
2292
+
2293
+ figjam
2294
+ .command('list')
2295
+ .description('List open FigJam pages')
2296
+ .action(async () => {
2297
+ try {
2298
+ const pages = await FigJamClient.listPages();
2299
+ if (pages.length === 0) {
2300
+ console.log(chalk.yellow('\n No FigJam pages open\n'));
2301
+ return;
2302
+ }
2303
+ console.log(chalk.cyan('\n Open FigJam Pages:\n'));
2304
+ pages.forEach((p, i) => {
2305
+ console.log(chalk.white(` ${i + 1}. ${p.title}`));
2306
+ });
2307
+ console.log();
2308
+ } catch (error) {
2309
+ console.log(chalk.red('\n✗ Could not connect to Figma\n'));
2310
+ console.log(chalk.gray(' Make sure Figma is running with: design-lazyyy-cli connect\n'));
2311
+ }
2312
+ });
2313
+
2314
+ figjam
2315
+ .command('info')
2316
+ .description('Show current FigJam page info')
2317
+ .option('-p, --page <title>', 'Page title (partial match)')
2318
+ .action(async (options) => {
2319
+ const client = await getFigJamClient(options.page);
2320
+ try {
2321
+ const info = await client.getPageInfo();
2322
+ console.log(chalk.cyan('\n FigJam Page Info:\n'));
2323
+ console.log(chalk.white(` Name: ${info.name}`));
2324
+ console.log(chalk.white(` ID: ${info.id}`));
2325
+ console.log(chalk.white(` Elements: ${info.childCount}`));
2326
+ console.log();
2327
+ } finally {
2328
+ client.close();
2329
+ }
2330
+ });
2331
+
2332
+ figjam
2333
+ .command('nodes')
2334
+ .description('List nodes on current FigJam page')
2335
+ .option('-p, --page <title>', 'Page title (partial match)')
2336
+ .option('-l, --limit <n>', 'Limit number of nodes', '20')
2337
+ .action(async (options) => {
2338
+ const client = await getFigJamClient(options.page);
2339
+ try {
2340
+ const nodes = await client.listNodes(parseInt(options.limit));
2341
+ if (nodes.length === 0) {
2342
+ console.log(chalk.yellow('\n No elements on this page\n'));
2343
+ return;
2344
+ }
2345
+ console.log(chalk.cyan('\n FigJam Elements:\n'));
2346
+ nodes.forEach(n => {
2347
+ const type = n.type.padEnd(16);
2348
+ const name = (n.name || '(unnamed)').substring(0, 30);
2349
+ console.log(chalk.gray(` ${n.id.padEnd(8)}`), chalk.white(type), chalk.gray(name), chalk.gray(`(${n.x}, ${n.y})`));
2350
+ });
2351
+ console.log();
2352
+ } finally {
2353
+ client.close();
2354
+ }
2355
+ });
2356
+
2357
+ figjam
2358
+ .command('sticky <text>')
2359
+ .description('Create a sticky note')
2360
+ .option('-p, --page <title>', 'Page title (partial match)')
2361
+ .option('-x <n>', 'X position', '0')
2362
+ .option('-y <n>', 'Y position', '0')
2363
+ .option('-c, --color <hex>', 'Background color')
2364
+ .action(async (text, options) => {
2365
+ const client = await getFigJamClient(options.page);
2366
+ const spinner = ora('Creating sticky note...').start();
2367
+ try {
2368
+ const result = await client.createSticky(text, parseFloat(options.x), parseFloat(options.y), options.color);
2369
+ spinner.succeed(`Sticky created: ${result.id} at (${result.x}, ${result.y})`);
2370
+ } catch (error) {
2371
+ spinner.fail('Failed to create sticky: ' + error.message);
2372
+ } finally {
2373
+ client.close();
2374
+ }
2375
+ });
2376
+
2377
+ figjam
2378
+ .command('shape <text>')
2379
+ .description('Create a shape with text')
2380
+ .option('-p, --page <title>', 'Page title (partial match)')
2381
+ .option('-x <n>', 'X position', '0')
2382
+ .option('-y <n>', 'Y position', '0')
2383
+ .option('-w, --width <n>', 'Width', '200')
2384
+ .option('-h, --height <n>', 'Height', '100')
2385
+ .option('-t, --type <type>', 'Shape type (ROUNDED_RECTANGLE, RECTANGLE, ELLIPSE, DIAMOND)', 'ROUNDED_RECTANGLE')
2386
+ .action(async (text, options) => {
2387
+ const client = await getFigJamClient(options.page);
2388
+ const spinner = ora('Creating shape...').start();
2389
+ try {
2390
+ const result = await client.createShape(
2391
+ text,
2392
+ parseFloat(options.x),
2393
+ parseFloat(options.y),
2394
+ parseFloat(options.width),
2395
+ parseFloat(options.height),
2396
+ options.type
2397
+ );
2398
+ spinner.succeed(`Shape created: ${result.id} at (${result.x}, ${result.y})`);
2399
+ } catch (error) {
2400
+ spinner.fail('Failed to create shape: ' + error.message);
2401
+ } finally {
2402
+ client.close();
2403
+ }
2404
+ });
2405
+
2406
+ figjam
2407
+ .command('text <content>')
2408
+ .description('Create a text node')
2409
+ .option('-p, --page <title>', 'Page title (partial match)')
2410
+ .option('-x <n>', 'X position', '0')
2411
+ .option('-y <n>', 'Y position', '0')
2412
+ .option('-s, --size <n>', 'Font size', '16')
2413
+ .action(async (content, options) => {
2414
+ const client = await getFigJamClient(options.page);
2415
+ const spinner = ora('Creating text...').start();
2416
+ try {
2417
+ const result = await client.createText(content, parseFloat(options.x), parseFloat(options.y), parseFloat(options.size));
2418
+ spinner.succeed(`Text created: ${result.id} at (${result.x}, ${result.y})`);
2419
+ } catch (error) {
2420
+ spinner.fail('Failed to create text: ' + error.message);
2421
+ } finally {
2422
+ client.close();
2423
+ }
2424
+ });
2425
+
2426
+ figjam
2427
+ .command('connect <startId> <endId>')
2428
+ .description('Create a connector between two nodes')
2429
+ .option('-p, --page <title>', 'Page title (partial match)')
2430
+ .action(async (startId, endId, options) => {
2431
+ const client = await getFigJamClient(options.page);
2432
+ const spinner = ora('Creating connector...').start();
2433
+ try {
2434
+ const result = await client.createConnector(startId, endId);
2435
+ if (result.error) {
2436
+ spinner.fail(result.error);
2437
+ } else {
2438
+ spinner.succeed(`Connector created: ${result.id}`);
2439
+ }
2440
+ } catch (error) {
2441
+ spinner.fail('Failed to create connector: ' + error.message);
2442
+ } finally {
2443
+ client.close();
2444
+ }
2445
+ });
2446
+
2447
+ figjam
2448
+ .command('delete <nodeId>')
2449
+ .description('Delete a node by ID')
2450
+ .option('-p, --page <title>', 'Page title (partial match)')
2451
+ .action(async (nodeId, options) => {
2452
+ const client = await getFigJamClient(options.page);
2453
+ const spinner = ora('Deleting node...').start();
2454
+ try {
2455
+ const result = await client.deleteNode(nodeId);
2456
+ if (result.deleted) {
2457
+ spinner.succeed(`Node ${nodeId} deleted`);
2458
+ } else {
2459
+ spinner.fail(result.error || 'Node not found');
2460
+ }
2461
+ } catch (error) {
2462
+ spinner.fail('Failed to delete node: ' + error.message);
2463
+ } finally {
2464
+ client.close();
2465
+ }
2466
+ });
2467
+
2468
+ figjam
2469
+ .command('move <nodeId> <x> <y>')
2470
+ .description('Move a node to a new position')
2471
+ .option('-p, --page <title>', 'Page title (partial match)')
2472
+ .action(async (nodeId, x, y, options) => {
2473
+ const client = await getFigJamClient(options.page);
2474
+ const spinner = ora('Moving node...').start();
2475
+ try {
2476
+ const result = await client.moveNode(nodeId, parseFloat(x), parseFloat(y));
2477
+ if (result.error) {
2478
+ spinner.fail(result.error);
2479
+ } else {
2480
+ spinner.succeed(`Node ${result.id} moved to (${result.x}, ${result.y})`);
2481
+ }
2482
+ } catch (error) {
2483
+ spinner.fail('Failed to move node: ' + error.message);
2484
+ } finally {
2485
+ client.close();
2486
+ }
2487
+ });
2488
+
2489
+ figjam
2490
+ .command('update <nodeId> <text>')
2491
+ .description('Update text content of a node')
2492
+ .option('-p, --page <title>', 'Page title (partial match)')
2493
+ .action(async (nodeId, text, options) => {
2494
+ const client = await getFigJamClient(options.page);
2495
+ const spinner = ora('Updating text...').start();
2496
+ try {
2497
+ const result = await client.updateText(nodeId, text);
2498
+ if (result.error) {
2499
+ spinner.fail(result.error);
2500
+ } else {
2501
+ spinner.succeed(`Node ${result.id} text updated`);
2502
+ }
2503
+ } catch (error) {
2504
+ spinner.fail('Failed to update text: ' + error.message);
2505
+ } finally {
2506
+ client.close();
2507
+ }
2508
+ });
2509
+
2510
+ figjam
2511
+ .command('eval <code>')
2512
+ .description('Execute JavaScript in FigJam context')
2513
+ .option('-p, --page <title>', 'Page title (partial match)')
2514
+ .action(async (code, options) => {
2515
+ const client = await getFigJamClient(options.page);
2516
+ try {
2517
+ const result = await client.eval(code);
2518
+ if (result !== undefined) {
2519
+ console.log(typeof result === 'object' ? JSON.stringify(result, null, 2) : result);
2520
+ }
2521
+ } catch (error) {
2522
+ console.log(chalk.red('Error: ' + error.message));
2523
+ } finally {
2524
+ client.close();
2525
+ }
2526
+ });
2527
+
2528
+ program.parse();