figma-local 1.1.0 → 1.3.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/README.md CHANGED
@@ -51,6 +51,12 @@ npm install -g figma-local
51
51
 
52
52
  Gives you `fig` and `fig-start` globally on your PATH.
53
53
 
54
+ **Update to latest:**
55
+
56
+ ```bash
57
+ npm update -g figma-local
58
+ ```
59
+
54
60
  ### curl (one-line)
55
61
 
56
62
  ```bash
@@ -74,7 +80,7 @@ npx figma-local read
74
80
 
75
81
  ```bash
76
82
  git clone https://github.com/thepreakerebi/figma-local.git
77
- cd figma-cli
83
+ cd figma-local
78
84
  npm install && npm install -g .
79
85
  ```
80
86
 
@@ -93,32 +99,66 @@ npm install && npm install -g .
93
99
 
94
100
  ## Setup (one time only)
95
101
 
96
- ### 1. Import the Figma plugin
102
+ ### Step 1 Install the CLI
97
103
 
98
- 1. Open **Figma Desktop**
99
- 2. Hamburger menu → **Plugins → Development → Import plugin from manifest...**
100
- 3. Navigate to the `plugin/` folder in this repo (or `$(npm root -g)/figma-local/plugin/`)
101
- 4. Select `manifest.json` → click **Open**
102
- 5. Right-click **Figma Local** in the plugin list → **Add to toolbar**
104
+ Pick one of the install methods above (npm recommended):
105
+
106
+ ```bash
107
+ npm install -g figma-local
108
+ ```
103
109
 
104
- ### 2. Connect
110
+ Verify it worked:
105
111
 
106
112
  ```bash
107
- fig-start --safe
113
+ fig --help
108
114
  ```
109
115
 
110
- This starts the daemon, waits for you to click Figma Local in Figma, then launches Claude Code.
116
+ ### Step 2 Import the Figma plugin into Figma Desktop
117
+
118
+ The plugin lets figma-local talk to Figma. You only need to do this once per Figma account.
119
+
120
+ 1. Open **Figma Desktop** (not the browser — the desktop app)
121
+ 2. Open any design file
122
+ 3. Click the **hamburger menu** (☰) in the top-left corner
123
+ 4. Go to **Plugins → Development → Import plugin from manifest...**
124
+ 5. In the file picker, navigate to the plugin folder:
125
+ - If you installed via **npm**: run `echo "$(npm root -g)/figma-local/plugin"` in your terminal to get the path, then navigate there
126
+ - If you cloned the **repo**: go to `figma-local/plugin/` in the cloned folder
127
+ 6. Select `manifest.json` → click **Open**
128
+ 7. You should see **"Figma Local"** appear in your plugin list
129
+ 8. *(Optional but recommended)* Right-click **Figma Local** in the plugin list → **Add to toolbar** for one-click access
130
+
131
+ ### Step 3 — Connect and verify
132
+
133
+ 1. In Figma, start the plugin: **Plugins → Development → Figma Local** (or click it in your toolbar)
134
+ 2. You should see a small widget that says **"Figma Local"** with a connecting status
135
+ 3. In your terminal, run:
136
+
137
+ ```bash
138
+ fig connect --safe
139
+ ```
140
+
141
+ 4. The plugin widget should show a **green dot** and say **"Connected"**
142
+ 5. Try reading your canvas:
143
+
144
+ ```bash
145
+ fig read
146
+ ```
147
+
148
+ If you see a list of frames, you're all set!
111
149
 
112
150
  ---
113
151
 
114
152
  ## Every session after that
115
153
 
116
- ```
117
- 1. Open Figma click Figma Local in the toolbar
118
- 2. In terminal: fig-start --safe
154
+ ```bash
155
+ # 1. Open Figma Desktop and your design file
156
+ # 2. Click "Figma Local" in the toolbar (or Plugins → Development → Figma Local)
157
+ # 3. In your terminal:
158
+ fig-start --safe
119
159
  ```
120
160
 
121
- Claude Code reads `CLAUDE.md` and knows every command automatically.
161
+ This connects to Figma and launches Claude Code. Claude reads `CLAUDE.md` and knows every `fig` command automatically.
122
162
 
123
163
  ---
124
164
 
@@ -249,13 +289,34 @@ fig prompt "Login" \
249
289
 
250
290
  Generates ~45 tokens of structured text instead of attaching a Figma frame (300–500+ hidden tokens). **91–97% smaller input, more consistent AI output.**
251
291
 
292
+ ### Screenshots
293
+
294
+ ```bash
295
+ fig screenshot # Screenshot current selection
296
+ fig screenshot --node "123:456" # Screenshot a specific node
297
+ fig screenshot --link "https://..." # Screenshot from a Figma link
298
+ fig screenshot -o design.png -s 2 # Save to file at 2x scale
299
+ fig screenshot -f svg -o icon.svg # Export as SVG
300
+ ```
301
+
252
302
  ### Verify & compare
253
303
 
254
304
  ```bash
255
- fig verify # Screenshot of selection for AI review
256
- fig verify --compare "https://..." # Diff prototype vs Figma design → correction prompts
305
+ fig verify # Screenshot of selection for AI review
306
+ fig verify --link "https://..." # Verify from a Figma link
307
+ fig verify --node "123:456" # Verify a specific node
308
+ fig verify --compare "https://..." # Diff prototype vs Figma design → correction prompts
309
+
310
+ # Visual comparison between any two sources
311
+ fig compare --a selection --b "123:456" # Compare selection vs a node
312
+ fig compare --a design.png --b "123:456" # Compare a screenshot file vs a Figma node
313
+ fig compare --a design.png --b coded.png # Compare two screenshot files
314
+ fig compare --a-link "https://..." --b-link "https://..." # Compare two Figma links
315
+ fig compare --a "123:456" --b "789:012" # Compare two nodes by ID
257
316
  ```
258
317
 
318
+ Sources for `--a` and `--b` can be: `selection`, a node ID (`123:456`), or a file path (`screenshot.png`). Use `--a-link` / `--b-link` for Figma selection URLs.
319
+
259
320
  ### Export
260
321
 
261
322
  ```bash
@@ -392,7 +453,7 @@ Issues and PRs welcome. For major changes, open an issue first to discuss.
392
453
 
393
454
  ```bash
394
455
  git clone https://github.com/thepreakerebi/figma-local.git
395
- cd figma-cli
456
+ cd figma-local
396
457
  npm install
397
458
  node src/index.js --help
398
459
  npm test
package/bin/fig-start CHANGED
@@ -123,11 +123,14 @@ for arg in "$@"; do
123
123
  done
124
124
 
125
125
  if [ "$1" = "--setup" ]; then
126
- printf "${BOLD}Enter the path to your figma-cli repo:${NC} "
126
+ printf "${BOLD}Enter the absolute path to your figma-local folder:${NC} "
127
127
  read -r repo_path < /dev/tty
128
128
  repo_path="${repo_path/#\~/$HOME}"
129
- if [ ! -f "$repo_path/src/index.js" ]; then
130
- echo -e "${RED}Not a valid figma-cli repo:${NC} $repo_path"
129
+ if [[ "$repo_path" != /* ]]; then
130
+ repo_path="$(cd "$repo_path" 2>/dev/null && pwd)"
131
+ fi
132
+ if [ -z "$repo_path" ] || [ ! -f "$repo_path/src/index.js" ]; then
133
+ echo -e "${RED}Not a valid figma-local folder.${NC} Expected src/index.js inside it."
131
134
  exit 1
132
135
  fi
133
136
  save_repo_path "$repo_path"
@@ -139,21 +142,34 @@ fi
139
142
  REPO_PATH=$(get_repo_path)
140
143
 
141
144
  if [ -z "$REPO_PATH" ] || [ ! -d "$REPO_PATH" ]; then
142
- # Try to find it relative to this script
145
+ # Try to find it relative to this script (works for both git clone and npm -g installs)
143
146
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
144
147
  if [ -f "$SCRIPT_DIR/src/index.js" ]; then
145
148
  REPO_PATH="$SCRIPT_DIR"
146
149
  save_repo_path "$REPO_PATH"
147
150
  else
148
- echo -e "${YELLOW}figma-cli repo not configured.${NC}"
149
- printf "${BOLD}Enter the path to your figma-cli repo:${NC} "
150
- read -r REPO_PATH < /dev/tty
151
- REPO_PATH="${REPO_PATH/#\~/$HOME}"
152
- if [ ! -f "$REPO_PATH/src/index.js" ]; then
153
- echo -e "${RED}Not a valid figma-cli repo:${NC} $REPO_PATH"
154
- exit 1
151
+ # Try npm global install location
152
+ NPM_GLOBAL="$(npm root -g 2>/dev/null)/figma-local"
153
+ if [ -f "$NPM_GLOBAL/src/index.js" ]; then
154
+ REPO_PATH="$NPM_GLOBAL"
155
+ save_repo_path "$REPO_PATH"
156
+ else
157
+ echo -e "${YELLOW}figma-local repo not configured.${NC}"
158
+ echo -e "${DIM}If you installed via npm, run: fig-start --setup${NC}"
159
+ printf "${BOLD}Enter the absolute path to your figma-local folder:${NC} "
160
+ read -r REPO_PATH < /dev/tty
161
+ REPO_PATH="${REPO_PATH/#\~/$HOME}"
162
+ # Convert relative paths to absolute
163
+ if [[ "$REPO_PATH" != /* ]]; then
164
+ REPO_PATH="$(cd "$REPO_PATH" 2>/dev/null && pwd)"
165
+ fi
166
+ if [ -z "$REPO_PATH" ] || [ ! -f "$REPO_PATH/src/index.js" ]; then
167
+ echo -e "${RED}Not a valid figma-local folder.${NC} Expected src/index.js inside it."
168
+ echo -e "${DIM}Tip: use the full path, e.g. /Users/yourname/figma-local${NC}"
169
+ exit 1
170
+ fi
171
+ save_repo_path "$REPO_PATH"
155
172
  fi
156
- save_repo_path "$REPO_PATH"
157
173
  fi
158
174
  fi
159
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
5
5
  "author": "elvke",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -5205,18 +5205,30 @@ const exp = program
5205
5205
 
5206
5206
  exp
5207
5207
  .command('screenshot')
5208
- .description('Take a screenshot of selected node or current page')
5208
+ .description('Take a screenshot of selected node, specific node, or from a Figma link')
5209
5209
  .option('-o, --output <file>', 'Output file', 'screenshot.png')
5210
5210
  .option('-s, --scale <number>', 'Export scale (1-4)', '2')
5211
5211
  .option('-f, --format <format>', 'Format: png, jpg, svg, pdf', 'png')
5212
+ .option('--node <id>', 'Screenshot a specific node by ID')
5213
+ .option('--link <url>', 'Screenshot a node from a Figma selection link')
5212
5214
  .action((options) => {
5213
5215
  checkConnection();
5214
5216
  const format = options.format.toUpperCase();
5215
5217
  const scale = parseFloat(options.scale);
5218
+ let nodeResolver;
5219
+ if (options.link) {
5220
+ const nodeId = parseNodeIdFromLink(options.link);
5221
+ if (!nodeId) { console.error(chalk.red('✗'), 'Could not parse node ID from link'); process.exit(1); }
5222
+ nodeResolver = `node = await figma.getNodeByIdAsync('${nodeId}');`;
5223
+ } else if (options.node) {
5224
+ nodeResolver = `node = await figma.getNodeByIdAsync('${options.node}');`;
5225
+ } else {
5226
+ nodeResolver = `const sel = figma.currentPage.selection; node = sel.length > 0 ? sel[0] : figma.currentPage;`;
5227
+ }
5216
5228
  const code = `(async () => {
5217
- const sel = figma.currentPage.selection;
5218
- const node = sel.length > 0 ? sel[0] : figma.currentPage;
5219
- if (!node) return { error: 'No page or selection' };
5229
+ let node;
5230
+ ${nodeResolver}
5231
+ if (!node) return { error: 'No node found' };
5220
5232
  if (!('exportAsync' in node)) return { error: 'Node cannot be exported' };
5221
5233
  const bytes = await node.exportAsync({ format: '${format}', constraint: { type: 'SCALE', value: ${scale} } });
5222
5234
  return {
@@ -5333,14 +5345,25 @@ program
5333
5345
  .option('--save [path]', 'Save as PNG file (default: /tmp/figma-verify-{id}.png)')
5334
5346
  .option('--compare <url>', 'Compare against a prototype/preview URL and generate correction prompts')
5335
5347
  .option('--compare-save <path>', 'Save prototype screenshot to this path when using --compare')
5348
+ .option('--link <url>', 'Verify a node from a Figma selection link')
5349
+ .option('--node <id>', 'Verify a specific node by ID')
5336
5350
  .action(async (nodeId, options) => {
5337
5351
  checkConnection();
5338
5352
  const scale = parseFloat(options.scale);
5339
5353
  const maxDim = parseInt(options.max);
5340
5354
 
5355
+ // Resolve node: --link > --node > positional nodeId > selection
5356
+ let resolvedNodeId = nodeId;
5357
+ if (options.link) {
5358
+ resolvedNodeId = parseNodeIdFromLink(options.link);
5359
+ if (!resolvedNodeId) { console.error(chalk.red('✗'), 'Could not parse node ID from link'); process.exit(1); }
5360
+ } else if (options.node) {
5361
+ resolvedNodeId = options.node;
5362
+ }
5363
+
5341
5364
  const code = `(async () => {
5342
5365
  let node;
5343
- ${nodeId ? `node = await figma.getNodeByIdAsync('${nodeId}');` : `
5366
+ ${resolvedNodeId ? `node = await figma.getNodeByIdAsync('${resolvedNodeId}');` : `
5344
5367
  const sel = figma.currentPage.selection;
5345
5368
  node = sel.length > 0 ? sel[0] : null;
5346
5369
  `}
@@ -5441,6 +5464,137 @@ program
5441
5464
  }
5442
5465
  });
5443
5466
 
5467
+ // ============ COMPARE (Visual Comparison) ============
5468
+
5469
+ function exportNodeScreenshot(nodeResolver, scale, maxDim) {
5470
+ const code = `(async () => {
5471
+ let node;
5472
+ ${nodeResolver}
5473
+ if (!node) return { error: 'No node found' };
5474
+ if (!('exportAsync' in node)) return { error: 'Node cannot be exported' };
5475
+ const nodeWidth = node.width || 100;
5476
+ const nodeHeight = node.height || 100;
5477
+ let finalScale = ${scale};
5478
+ const maxNodeDim = Math.max(nodeWidth, nodeHeight);
5479
+ if (maxNodeDim * finalScale > ${maxDim}) finalScale = ${maxDim} / maxNodeDim;
5480
+ if (maxNodeDim * finalScale > 7500) finalScale = 7500 / maxNodeDim;
5481
+ const bytes = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: finalScale } });
5482
+ return {
5483
+ name: node.name,
5484
+ id: node.id,
5485
+ type: node.type,
5486
+ width: Math.round(nodeWidth * finalScale),
5487
+ height: Math.round(nodeHeight * finalScale),
5488
+ originalWidth: Math.round(nodeWidth),
5489
+ originalHeight: Math.round(nodeHeight),
5490
+ bytes: Array.from(bytes)
5491
+ };
5492
+ })()`;
5493
+ return figmaEvalSync(code);
5494
+ }
5495
+
5496
+ program
5497
+ .command('compare')
5498
+ .description('Compare two things visually: screenshots, Figma nodes, or a mix. Outputs both images for AI analysis.')
5499
+ .option('--a <source>', 'First source: file path, node ID, or "selection"', 'selection')
5500
+ .option('--b <source>', 'Second source: file path, node ID, or Figma link')
5501
+ .option('--a-link <url>', 'First source from a Figma selection link')
5502
+ .option('--b-link <url>', 'Second source from a Figma selection link')
5503
+ .option('-s, --scale <number>', 'Export scale for Figma nodes', '1')
5504
+ .option('--max <pixels>', 'Max dimension for exports', '2000')
5505
+ .option('--save-dir <dir>', 'Directory to save comparison images', '/tmp')
5506
+ .addHelpText('after', `
5507
+ Examples:
5508
+ fig compare --a selection --b "123:456" Compare selection vs a node
5509
+ fig compare --a design.png --b "123:456" Compare a screenshot file vs a Figma node
5510
+ fig compare --a design.png --b coded.png Compare two screenshot files
5511
+ fig compare --a-link "https://..." --b-link "https://..." Compare two Figma links
5512
+ fig compare --a selection --b-link "https://..." Compare selection vs a Figma link
5513
+ fig compare --a "123:456" --b "789:012" Compare two nodes by ID
5514
+ `)
5515
+ .action((options) => {
5516
+ checkConnection();
5517
+ const scale = parseFloat(options.scale);
5518
+ const maxDim = parseInt(options.max);
5519
+ const saveDir = options.saveDir;
5520
+ const timestamp = Date.now();
5521
+
5522
+ function resolveSource(source, linkOpt, label) {
5523
+ // If it's a --link option
5524
+ if (linkOpt) {
5525
+ const nodeId = parseNodeIdFromLink(linkOpt);
5526
+ if (!nodeId) { console.error(chalk.red('✗'), `Could not parse node ID from ${label} link`); process.exit(1); }
5527
+ const result = exportNodeScreenshot(`node = await figma.getNodeByIdAsync('${nodeId}');`, scale, maxDim);
5528
+ if (result.error) { console.error(chalk.red('✗'), `${label}: ${result.error}`); process.exit(1); }
5529
+ const filePath = `${saveDir}/figma-compare-${label}-${timestamp}.png`;
5530
+ writeFileSync(filePath, Buffer.from(result.bytes));
5531
+ return { type: 'figma-node', name: result.name, id: result.id, nodeType: result.type, width: result.width, height: result.height, originalWidth: result.originalWidth, originalHeight: result.originalHeight, path: filePath };
5532
+ }
5533
+
5534
+ // File path (existing screenshot)
5535
+ if (source && existsSync(source)) {
5536
+ return { type: 'file', name: source.split('/').pop(), path: source };
5537
+ }
5538
+
5539
+ // "selection"
5540
+ if (source === 'selection') {
5541
+ const result = exportNodeScreenshot(`const sel = figma.currentPage.selection; node = sel.length > 0 ? sel[0] : null;`, scale, maxDim);
5542
+ if (result.error) { console.error(chalk.red('✗'), `${label}: ${result.error}`); process.exit(1); }
5543
+ const filePath = `${saveDir}/figma-compare-${label}-${timestamp}.png`;
5544
+ writeFileSync(filePath, Buffer.from(result.bytes));
5545
+ return { type: 'figma-node', name: result.name, id: result.id, nodeType: result.type, width: result.width, height: result.height, originalWidth: result.originalWidth, originalHeight: result.originalHeight, path: filePath };
5546
+ }
5547
+
5548
+ // Node ID (contains ":")
5549
+ if (source && source.includes(':')) {
5550
+ const result = exportNodeScreenshot(`node = await figma.getNodeByIdAsync('${source}');`, scale, maxDim);
5551
+ if (result.error) { console.error(chalk.red('✗'), `${label}: ${result.error}`); process.exit(1); }
5552
+ const filePath = `${saveDir}/figma-compare-${label}-${timestamp}.png`;
5553
+ writeFileSync(filePath, Buffer.from(result.bytes));
5554
+ return { type: 'figma-node', name: result.name, id: result.id, nodeType: result.type, width: result.width, height: result.height, originalWidth: result.originalWidth, originalHeight: result.originalHeight, path: filePath };
5555
+ }
5556
+
5557
+ console.error(chalk.red('✗'), `${label}: "${source}" is not a valid file path, node ID, or "selection"`);
5558
+ process.exit(1);
5559
+ }
5560
+
5561
+ const sourceA = resolveSource(options.a, options.aLink, 'a');
5562
+ const sourceB = resolveSource(options.b, options.bLink, 'b');
5563
+
5564
+ console.log(chalk.bold('\n## Visual Comparison\n'));
5565
+ console.log(chalk.cyan('Source A:'), sourceA.name, sourceA.type === 'figma-node' ? `(${sourceA.nodeType} ${sourceA.id}, ${sourceA.originalWidth}x${sourceA.originalHeight})` : '(file)');
5566
+ console.log(chalk.cyan('Source B:'), sourceB.name, sourceB.type === 'figma-node' ? `(${sourceB.nodeType} ${sourceB.id}, ${sourceB.originalWidth}x${sourceB.originalHeight})` : '(file)');
5567
+ console.log('');
5568
+ console.log(chalk.green('Image A:'), sourceA.path);
5569
+ console.log(chalk.green('Image B:'), sourceB.path);
5570
+ console.log('');
5571
+
5572
+ // Output structured data for AI agents
5573
+ console.log(JSON.stringify({
5574
+ mode: 'visual-comparison',
5575
+ sourceA: { ...sourceA, bytes: undefined },
5576
+ sourceB: { ...sourceB, bytes: undefined },
5577
+ instructions: [
5578
+ `1. Open and examine Image A: ${sourceA.path}`,
5579
+ `2. Open and examine Image B: ${sourceB.path}`,
5580
+ '3. Compare them visually for differences in:',
5581
+ ' - Layout and spacing (padding, margins, gaps)',
5582
+ ' - Colors (backgrounds, text, borders)',
5583
+ ' - Typography (font size, weight, line-height)',
5584
+ ' - Border radius and shadows',
5585
+ ' - Missing or extra elements',
5586
+ ' - Alignment and positioning',
5587
+ '4. Output a structured gap report with specific differences',
5588
+ '5. For each difference, provide the exact values from both sources',
5589
+ ],
5590
+ gapReportTemplate: {
5591
+ matches: '(list elements that match between A and B)',
5592
+ differences: '(table: element | property | value_in_A | value_in_B | severity)',
5593
+ summary: '(brief overall assessment: how closely do they match)',
5594
+ }
5595
+ }, null, 2));
5596
+ });
5597
+
5444
5598
  // ============ EVAL ============
5445
5599
 
5446
5600
  program