figma-local 1.0.0 → 1.2.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 +95 -18
- package/bin/fig-start +28 -12
- package/package.json +8 -6
- package/plugin/code.js +178 -0
- package/plugin/manifest.json +14 -0
- package/plugin/ui.html +285 -0
- package/skills/figma-css/SKILL.md +119 -0
- package/skills/figma-document/SKILL.md +129 -0
- package/skills/figma-inspect/SKILL.md +98 -0
- package/skills/figma-local/SKILL.md +170 -0
- package/skills/figma-measure/SKILL.md +59 -0
- package/skills/figma-styles/SKILL.md +114 -0
- package/src/daemon.js +1 -1
- package/src/index.js +1772 -25
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# figma-
|
|
1
|
+
# figma-local
|
|
2
2
|
|
|
3
3
|
> Control Figma Desktop with Claude Code. Design tokens, shadcn/ui components, AI prompt export, lint, and more — no API key required.
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
15
|
-
**figma-
|
|
15
|
+
**figma-local** connects directly to Figma Desktop and lets you — or Claude Code — control it with natural language and `fig` commands.
|
|
16
16
|
|
|
17
17
|
- **Write** — Create frames, components, design tokens, icons, and full UI kits
|
|
18
18
|
- **Read** — Extract design context in a lean staged format (91–97% fewer tokens than raw data dumps)
|
|
@@ -51,16 +51,22 @@ 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
|
|
57
|
-
curl -fsSL https://raw.githubusercontent.com/thepreakerebi/figma-
|
|
63
|
+
curl -fsSL https://raw.githubusercontent.com/thepreakerebi/figma-local/main/install.sh | bash
|
|
58
64
|
```
|
|
59
65
|
|
|
60
66
|
### Homebrew
|
|
61
67
|
|
|
62
68
|
```bash
|
|
63
|
-
brew tap thepreakerebi/figma-
|
|
69
|
+
brew tap thepreakerebi/figma-local
|
|
64
70
|
brew install figma-cli
|
|
65
71
|
```
|
|
66
72
|
|
|
@@ -73,8 +79,8 @@ npx figma-local read
|
|
|
73
79
|
### From source
|
|
74
80
|
|
|
75
81
|
```bash
|
|
76
|
-
git clone https://github.com/thepreakerebi/figma-
|
|
77
|
-
cd figma-
|
|
82
|
+
git clone https://github.com/thepreakerebi/figma-local.git
|
|
83
|
+
cd figma-local
|
|
78
84
|
npm install && npm install -g .
|
|
79
85
|
```
|
|
80
86
|
|
|
@@ -99,7 +105,7 @@ npm install && npm install -g .
|
|
|
99
105
|
2. Hamburger menu → **Plugins → Development → Import plugin from manifest...**
|
|
100
106
|
3. Navigate to the `plugin/` folder in this repo (or `$(npm root -g)/figma-local/plugin/`)
|
|
101
107
|
4. Select `manifest.json` → click **Open**
|
|
102
|
-
5. Right-click **
|
|
108
|
+
5. Right-click **Figma Local** in the plugin list → **Add to toolbar**
|
|
103
109
|
|
|
104
110
|
### 2. Connect
|
|
105
111
|
|
|
@@ -107,14 +113,14 @@ npm install && npm install -g .
|
|
|
107
113
|
fig-start --safe
|
|
108
114
|
```
|
|
109
115
|
|
|
110
|
-
This starts the daemon, waits for you to click
|
|
116
|
+
This starts the daemon, waits for you to click Figma Local in Figma, then launches Claude Code.
|
|
111
117
|
|
|
112
118
|
---
|
|
113
119
|
|
|
114
120
|
## Every session after that
|
|
115
121
|
|
|
116
122
|
```
|
|
117
|
-
1. Open Figma → click
|
|
123
|
+
1. Open Figma → click Figma Local in the toolbar
|
|
118
124
|
2. In terminal: fig-start --safe
|
|
119
125
|
```
|
|
120
126
|
|
|
@@ -127,14 +133,60 @@ Claude Code reads `CLAUDE.md` and knows every command automatically.
|
|
|
127
133
|
### Read & understand your canvas
|
|
128
134
|
|
|
129
135
|
```bash
|
|
130
|
-
fig read # List all frames
|
|
131
|
-
fig read "Login Screen" #
|
|
132
|
-
fig read "Login Screen" --tokens #
|
|
136
|
+
fig read # List all frames on the current page
|
|
137
|
+
fig read "Login Screen" # Get the layout tree, components, and design tokens for that frame
|
|
138
|
+
fig read "Login Screen" --tokens # Show only the design tokens (colors, spacing) that frame uses
|
|
139
|
+
fig read --selection # Read whatever you have selected right now in Figma
|
|
140
|
+
fig read --link "https://..." # Read a specific node from a Figma selection link
|
|
133
141
|
fig find "Button" # Find nodes by name
|
|
134
142
|
fig node tree # Layer hierarchy
|
|
135
143
|
fig canvas info # Raw canvas info
|
|
136
144
|
```
|
|
137
145
|
|
|
146
|
+
### Inspect design specs
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
fig inspect # Full specs for the selected element (spacing, colors, fonts, effects)
|
|
150
|
+
fig inspect --node "123:456" # Inspect a specific node by ID
|
|
151
|
+
fig inspect --json # Raw JSON output
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Returns dimensions, padding, gap, colors (hex + rgba), typography (font family, size, weight, line-height, letter-spacing), border radius, strokes, shadows, opacity, and variable bindings (name + resolved value + per-mode values for Light/Dark) — all in **px and rem**.
|
|
155
|
+
|
|
156
|
+
### Generate CSS / Tailwind from design
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
fig css # CSS for current selection (rem units)
|
|
160
|
+
fig css --px # CSS in px units
|
|
161
|
+
fig css --tailwind # Tailwind utility classes
|
|
162
|
+
fig css --link "https://..." # CSS from a Figma link
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Measure spacing
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
fig measure # Select 2 elements in Figma, get the spacing between them
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Extract style guide from a frame
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
fig styles "Login Screen" # All text styles, colors, spacing values, and radii used
|
|
175
|
+
fig styles --selection # Styles from current selection
|
|
176
|
+
fig styles --json # Raw JSON
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Document a component (full recursive spec)
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
fig document # Document selected component — all children, all specs
|
|
183
|
+
fig document --json # Structured JSON for coding agents
|
|
184
|
+
fig document --link "https://..." # Document from a Figma link
|
|
185
|
+
fig document --tokens-only # Just the design tokens used
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Returns a complete breakdown: summary, all unique design tokens (colors, typography, spacing, radii, shadows), and a recursive tree of every element with full specs. One command gives a coding agent everything it needs to replicate the component.
|
|
189
|
+
|
|
138
190
|
### Design tokens
|
|
139
191
|
|
|
140
192
|
```bash
|
|
@@ -247,7 +299,7 @@ fig fj connect "ID1" "ID2"
|
|
|
247
299
|
| | Safe Mode | Yolo Mode |
|
|
248
300
|
|-|-----------|-----------|
|
|
249
301
|
| How | Plugin bridge | Direct CDP |
|
|
250
|
-
| Setup per session | Start
|
|
302
|
+
| Setup per session | Start Figma Local plugin | Nothing (after one-time patch) |
|
|
251
303
|
| Speed | Standard | ~10× faster |
|
|
252
304
|
| Extra permissions | None | macOS: Full Disk Access / Windows: Admin |
|
|
253
305
|
| Command | `fig connect --safe` | `fig connect` |
|
|
@@ -271,7 +323,7 @@ The daemon runs only on `127.0.0.1` (never exposed to the network) and is protec
|
|
|
271
323
|
| 1 MB request/message body cap | Memory exhaustion from oversized payloads |
|
|
272
324
|
| Plugin input validation | Code size cap (512 KB), batch size cap (50), strict field types |
|
|
273
325
|
| Rate limiting | Max 30 evals per 10 s per connection |
|
|
274
|
-
| Idle auto-shutdown | Daemon stops after
|
|
326
|
+
| Idle auto-shutdown | Daemon stops after 1 hour of inactivity |
|
|
275
327
|
|
|
276
328
|
The session token is generated fresh on every `fig connect`, stored at `~/.figma-ds-cli/.daemon-token` with `chmod 600` (owner-read only).
|
|
277
329
|
|
|
@@ -286,6 +338,31 @@ fig-start --safe --here # launch from your project dir; Claude sees both you
|
|
|
286
338
|
|
|
287
339
|
---
|
|
288
340
|
|
|
341
|
+
## Claude Code Plugin (Skills)
|
|
342
|
+
|
|
343
|
+
figma-local ships as a Claude Code plugin with 6 skills that teach coding agents how to use it automatically:
|
|
344
|
+
|
|
345
|
+
| Skill | Triggers on |
|
|
346
|
+
|-------|------------|
|
|
347
|
+
| **figma-local** | "read the design", "what's on the canvas", "Figma to code" |
|
|
348
|
+
| **figma-inspect** | "get specs", "what font/color/spacing", "design specs" |
|
|
349
|
+
| **figma-css** | "generate CSS", "Tailwind classes", "convert to CSS" |
|
|
350
|
+
| **figma-styles** | "style guide", "extract colors/fonts", "spacing scale" |
|
|
351
|
+
| **figma-measure** | "measure spacing", "gap between elements" |
|
|
352
|
+
| **figma-document** | "document this component", "full spec sheet", "deep breakdown" |
|
|
353
|
+
|
|
354
|
+
### Install the skills
|
|
355
|
+
|
|
356
|
+
Install via [skills.sh](https://skills.sh):
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
npx skills add thepreakerebi/figma-local
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Once installed, restart Claude Code. It automatically knows all `fig` commands and uses them when your tasks involve Figma designs.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
289
366
|
## For teams
|
|
290
367
|
|
|
291
368
|
```bash
|
|
@@ -299,7 +376,7 @@ npm install -g figma-local
|
|
|
299
376
|
Pin a version for consistency:
|
|
300
377
|
|
|
301
378
|
```bash
|
|
302
|
-
npm install -g
|
|
379
|
+
npm install -g figma-local@1.0.0
|
|
303
380
|
```
|
|
304
381
|
|
|
305
382
|
---
|
|
@@ -311,7 +388,7 @@ npm uninstall -g figma-local
|
|
|
311
388
|
rm -rf ~/.figma-cli ~/.figma-ds-cli
|
|
312
389
|
```
|
|
313
390
|
|
|
314
|
-
Remove the plugin in Figma: Plugins → Development → right-click
|
|
391
|
+
Remove the plugin in Figma: Plugins → Development → right-click Figma Local → Remove.
|
|
315
392
|
|
|
316
393
|
---
|
|
317
394
|
|
|
@@ -320,8 +397,8 @@ Remove the plugin in Figma: Plugins → Development → right-click FigCli → R
|
|
|
320
397
|
Issues and PRs welcome. For major changes, open an issue first to discuss.
|
|
321
398
|
|
|
322
399
|
```bash
|
|
323
|
-
git clone https://github.com/thepreakerebi/figma-
|
|
324
|
-
cd figma-
|
|
400
|
+
git clone https://github.com/thepreakerebi/figma-local.git
|
|
401
|
+
cd figma-local
|
|
325
402
|
npm install
|
|
326
403
|
node src/index.js --help
|
|
327
404
|
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-
|
|
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 [
|
|
130
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-local",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
-
"bin"
|
|
15
|
+
"bin",
|
|
16
|
+
"plugin",
|
|
17
|
+
"skills"
|
|
16
18
|
],
|
|
17
19
|
"keywords": [
|
|
18
20
|
"figma",
|
|
@@ -25,11 +27,11 @@
|
|
|
25
27
|
],
|
|
26
28
|
"repository": {
|
|
27
29
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/thepreakerebi/figma-
|
|
30
|
+
"url": "git+https://github.com/thepreakerebi/figma-local.git"
|
|
29
31
|
},
|
|
30
|
-
"homepage": "https://github.com/thepreakerebi/figma-
|
|
32
|
+
"homepage": "https://github.com/thepreakerebi/figma-local",
|
|
31
33
|
"bugs": {
|
|
32
|
-
"url": "https://github.com/thepreakerebi/figma-
|
|
34
|
+
"url": "https://github.com/thepreakerebi/figma-local/issues"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
37
|
"setup-alias": "bash bin/setup-alias.sh",
|
package/plugin/code.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma CLI Bridge Plugin — code.js
|
|
3
|
+
*
|
|
4
|
+
* Safe Mode: Connects to CLI daemon via WebSocket.
|
|
5
|
+
* No debug port, no patching required.
|
|
6
|
+
*
|
|
7
|
+
* Security hardening:
|
|
8
|
+
* - All incoming messages from the UI are validated before execution
|
|
9
|
+
* - Code and batch payloads are size-capped
|
|
10
|
+
* - Batch arrays are length-capped
|
|
11
|
+
* - Notify text is sanitised (no arbitrary HTML/script injection)
|
|
12
|
+
* - Rate limiting: max 30 evals per 10-second window
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Constants ──────────────────────────────────────────────────
|
|
16
|
+
const MAX_CODE_BYTES = 512 * 1024; // 512 KB max per eval code string
|
|
17
|
+
const MAX_BATCH_COUNT = 50; // Max codes in a single eval-batch
|
|
18
|
+
const MAX_NOTIFY_CHARS = 200; // Max chars shown in Figma notification
|
|
19
|
+
|
|
20
|
+
// ── Rate limiter ───────────────────────────────────────────────
|
|
21
|
+
// Prevents a rogue daemon (or compromised WebSocket) from flooding
|
|
22
|
+
// the plugin with eval calls.
|
|
23
|
+
const RATE_WINDOW_MS = 10000;
|
|
24
|
+
const RATE_MAX = 30;
|
|
25
|
+
let rateCount = 0;
|
|
26
|
+
let rateWindowStart = Date.now();
|
|
27
|
+
|
|
28
|
+
function isRateAllowed() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (now - rateWindowStart > RATE_WINDOW_MS) {
|
|
31
|
+
rateCount = 0;
|
|
32
|
+
rateWindowStart = now;
|
|
33
|
+
}
|
|
34
|
+
if (rateCount >= RATE_MAX) return false;
|
|
35
|
+
rateCount++;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Input validators ───────────────────────────────────────────
|
|
40
|
+
function isValidId(id) {
|
|
41
|
+
return typeof id === 'number' && Number.isFinite(id) && id >= 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isValidCode(code) {
|
|
45
|
+
return typeof code === 'string' && code.length > 0 && code.length <= MAX_CODE_BYTES;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitiseNotify(text) {
|
|
49
|
+
if (typeof text !== 'string') return 'Unknown error';
|
|
50
|
+
// Strip anything that looks like HTML tags, keep plain text only
|
|
51
|
+
return text.replace(/<[^>]*>/g, '').slice(0, MAX_NOTIFY_CHARS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Show minimal UI (needed for WebSocket connection)
|
|
55
|
+
figma.showUI(__html__, {
|
|
56
|
+
width: 200,
|
|
57
|
+
height: 92
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Execute code with auto-return and timeout protection
|
|
61
|
+
async function executeCode(code, timeoutMs = 25000) {
|
|
62
|
+
let trimmed = code.trim();
|
|
63
|
+
|
|
64
|
+
// Don't add return if code already starts with return
|
|
65
|
+
if (!trimmed.startsWith('return ')) {
|
|
66
|
+
const isSimpleExpr = !trimmed.includes(';');
|
|
67
|
+
const isIIFE = trimmed.startsWith('(function') || trimmed.startsWith('(async function');
|
|
68
|
+
const isArrowIIFE = trimmed.startsWith('(() =>') || trimmed.startsWith('(async () =>');
|
|
69
|
+
|
|
70
|
+
if (isSimpleExpr || isIIFE || isArrowIIFE) {
|
|
71
|
+
trimmed = `return ${trimmed}`;
|
|
72
|
+
} else {
|
|
73
|
+
const lastSemicolon = trimmed.lastIndexOf(';');
|
|
74
|
+
if (lastSemicolon !== -1) {
|
|
75
|
+
const beforeLast = trimmed.substring(0, lastSemicolon + 1);
|
|
76
|
+
const lastStmt = trimmed.substring(lastSemicolon + 1).trim();
|
|
77
|
+
if (lastStmt && !lastStmt.startsWith('return ')) {
|
|
78
|
+
trimmed = beforeLast + ' return ' + lastStmt;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
85
|
+
const fn = new AsyncFunction('figma', `return (async () => { ${trimmed} })()`);
|
|
86
|
+
|
|
87
|
+
// Execute with timeout protection
|
|
88
|
+
const execPromise = fn(figma);
|
|
89
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
90
|
+
setTimeout(() => reject(new Error(`Execution timeout (${timeoutMs/1000}s)`)), timeoutMs)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return Promise.race([execPromise, timeoutPromise]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle messages from UI (WebSocket bridge)
|
|
97
|
+
figma.ui.onmessage = async (msg) => {
|
|
98
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
99
|
+
|
|
100
|
+
// ── Single eval ─────────────────────────────────────────────
|
|
101
|
+
if (msg.type === 'eval') {
|
|
102
|
+
// Validate id and code before doing anything
|
|
103
|
+
if (!isValidId(msg.id)) return;
|
|
104
|
+
if (!isValidCode(msg.code)) {
|
|
105
|
+
figma.ui.postMessage({ type: 'result', id: msg.id, error: 'Invalid or oversized code payload' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isRateAllowed()) {
|
|
109
|
+
figma.ui.postMessage({ type: 'result', id: msg.id, error: 'Rate limit exceeded — slow down' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = await executeCode(msg.code);
|
|
114
|
+
figma.ui.postMessage({ type: 'result', id: msg.id, result });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
figma.ui.postMessage({ type: 'result', id: msg.id, error: error.message });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Batch eval ──────────────────────────────────────────────
|
|
122
|
+
if (msg.type === 'eval-batch') {
|
|
123
|
+
if (!isValidId(msg.id)) return;
|
|
124
|
+
if (!Array.isArray(msg.codes)) {
|
|
125
|
+
figma.ui.postMessage({ type: 'batch-result', id: msg.id, results: [{ success: false, error: 'codes must be an array' }] });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Cap array length
|
|
129
|
+
if (msg.codes.length > MAX_BATCH_COUNT) {
|
|
130
|
+
figma.ui.postMessage({ type: 'batch-result', id: msg.id, results: [{ success: false, error: `Batch too large (max ${MAX_BATCH_COUNT})` }] });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Validate each code string
|
|
134
|
+
for (const c of msg.codes) {
|
|
135
|
+
if (!isValidCode(c)) {
|
|
136
|
+
figma.ui.postMessage({ type: 'batch-result', id: msg.id, results: [{ success: false, error: 'Invalid or oversized code in batch' }] });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!isRateAllowed()) {
|
|
141
|
+
figma.ui.postMessage({ type: 'batch-result', id: msg.id, results: [{ success: false, error: 'Rate limit exceeded' }] });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const results = [];
|
|
145
|
+
for (const code of msg.codes) {
|
|
146
|
+
try {
|
|
147
|
+
const result = await executeCode(code);
|
|
148
|
+
results.push({ success: true, result });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
results.push({ success: false, error: error.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
figma.ui.postMessage({ type: 'batch-result', id: msg.id, results });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Connection lifecycle (no eval, safe to handle directly) ──
|
|
158
|
+
if (msg.type === 'connected') {
|
|
159
|
+
figma.notify('✓ Figma Local connected', { timeout: 2000 });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (msg.type === 'disconnected') {
|
|
163
|
+
figma.notify('Figma Local disconnected', { timeout: 2000 });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (msg.type === 'error') {
|
|
167
|
+
// Sanitise before displaying in Figma UI
|
|
168
|
+
figma.notify('Figma Local: ' + sanitiseNotify(msg.message), { error: true });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Keep plugin alive
|
|
174
|
+
figma.on('close', () => {
|
|
175
|
+
// Plugin closed
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log('Figma DS CLI plugin started');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Figma Local",
|
|
3
|
+
"id": "figma-cli-bridge",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"main": "code.js",
|
|
6
|
+
"ui": "ui.html",
|
|
7
|
+
"capabilities": [],
|
|
8
|
+
"enableProposedApi": false,
|
|
9
|
+
"editorType": ["figma", "figjam"],
|
|
10
|
+
"networkAccess": {
|
|
11
|
+
"allowedDomains": ["*"],
|
|
12
|
+
"reasoning": "Connects to local CLI daemon for command execution"
|
|
13
|
+
}
|
|
14
|
+
}
|