figma-local 1.2.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 +70 -15
- package/package.json +1 -1
- package/src/index.js +159 -5
package/README.md
CHANGED
|
@@ -99,32 +99,66 @@ npm install && npm install -g .
|
|
|
99
99
|
|
|
100
100
|
## Setup (one time only)
|
|
101
101
|
|
|
102
|
-
### 1
|
|
102
|
+
### Step 1 — Install the CLI
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
2. Hamburger menu → **Plugins → Development → Import plugin from manifest...**
|
|
106
|
-
3. Navigate to the `plugin/` folder in this repo (or `$(npm root -g)/figma-local/plugin/`)
|
|
107
|
-
4. Select `manifest.json` → click **Open**
|
|
108
|
-
5. Right-click **Figma Local** in the plugin list → **Add to toolbar**
|
|
104
|
+
Pick one of the install methods above (npm recommended):
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
```bash
|
|
107
|
+
npm install -g figma-local
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Verify it worked:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
fig
|
|
113
|
+
fig --help
|
|
114
|
+
```
|
|
115
|
+
|
|
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
|
|
114
139
|
```
|
|
115
140
|
|
|
116
|
-
|
|
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!
|
|
117
149
|
|
|
118
150
|
---
|
|
119
151
|
|
|
120
152
|
## Every session after that
|
|
121
153
|
|
|
122
|
-
```
|
|
123
|
-
1. Open Figma
|
|
124
|
-
2.
|
|
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
|
|
125
159
|
```
|
|
126
160
|
|
|
127
|
-
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.
|
|
128
162
|
|
|
129
163
|
---
|
|
130
164
|
|
|
@@ -255,13 +289,34 @@ fig prompt "Login" \
|
|
|
255
289
|
|
|
256
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.**
|
|
257
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
|
+
|
|
258
302
|
### Verify & compare
|
|
259
303
|
|
|
260
304
|
```bash
|
|
261
|
-
fig verify
|
|
262
|
-
fig verify --
|
|
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
|
|
263
316
|
```
|
|
264
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
|
+
|
|
265
320
|
### Export
|
|
266
321
|
|
|
267
322
|
```bash
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
5218
|
-
|
|
5219
|
-
if (!node) return { error: 'No
|
|
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
|
-
${
|
|
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
|