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 CHANGED
@@ -1,4 +1,4 @@
1
- # figma-cli
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-cli** connects directly to Figma Desktop and lets you — or Claude Code — control it with natural language and `fig` commands.
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-cli/main/install.sh | bash
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-cli
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-cli.git
77
- cd figma-cli
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 **FigCli** in the plugin list → **Add to toolbar**
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 FigCli in Figma, then launches Claude Code.
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 FigCli in the toolbar
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 (fast)
131
- fig read "Login Screen" # Layout + components + used tokens only
132
- fig read "Login Screen" --tokens # Just the design tokens that frame uses
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 FigCli plugin | Nothing (after one-time patch) |
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 10 minutes of inactivity |
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 @jetro/figma-local@1.0.0
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 FigCli → Remove.
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-cli.git
324
- cd figma-cli
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-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,7 +1,7 @@
1
1
  {
2
2
  "name": "figma-local",
3
- "version": "1.0.0",
4
- "description": "Figma CLI — control Figma Desktop with Claude Code. Smart read, write, and AI-prompt export. No API key required.",
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-cli.git"
30
+ "url": "git+https://github.com/thepreakerebi/figma-local.git"
29
31
  },
30
- "homepage": "https://github.com/thepreakerebi/figma-cli",
32
+ "homepage": "https://github.com/thepreakerebi/figma-local",
31
33
  "bugs": {
32
- "url": "https://github.com/thepreakerebi/figma-cli/issues"
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
+ }