figrelay 0.8.0 → 0.9.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
@@ -2,62 +2,18 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/figrelay?color=cb3837&logo=npm)](https://www.npmjs.com/package/figrelay)
4
4
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/quannh287/figma-mcp-console/blob/main/LICENSE)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/quannh287/figma-relay/blob/main/LICENSE)
6
6
 
7
- > **Let your AI assistant design in Figma — no API token, no rate limits.**
8
- > Works with Claude CLI · Claude Desktop · Codex · VS Code · Cursor · Antigravity · any MCP client.
7
+ > **Let your AI assistant read and write Figma — no API token, no rate limits.**
8
+ > An MCP server that talks to a Figma Desktop plugin, so the AI can implement a frame in your codebase, draw a screen from a requirement, sync design tokens or bulk-edit layers. Works with any MCP client.
9
9
 
10
- > **Fork of [figma-mcp-console](https://github.com/hoangann2000/figma-mcp-console)** by [@hoangann2000](https://github.com/hoangann2000), forked at v0.5.4. Same setup, published on npm as `figrelay`; the plugin now shows as **Figma Relay**.
11
-
12
- ## 🆕 What's new since the fork
13
-
14
- - **Prototype tools** — `set_prototype_links`, `get_prototype_links` and `set_flow_starting_point` wire and check prototype flows.
15
- - **Safer `run_script`** — resending the same `token` returns the original run instead of running the script twice; timeout and disconnect errors say the script may still be writing.
16
- - **Batch edits, variants, text restyling, page targeting** — fewer calls for common edits, plus a scripting escape hatch.
17
- - **Many sessions, one plugin** — a timeout reports how many commands are queued, screenshots get 150s, and a busy plugin is no longer dropped as dead.
18
- - **Smaller tool list** — `create_frame`/`create_rectangle`/`create_ellipse` merged into `create_shape`; hide whole groups with `FIGMA_MCP_DISABLE`.
19
- - **Server version in the plugin panel**, so a stale binary is easy to spot.
20
-
21
- **Coming from `figma-mcp-console`?** Swap the package name to `figrelay@latest`, re-import the plugin from this release, and replace calls to `create_frame`/`create_rectangle`/`create_ellipse` with `create_shape`.
22
-
23
- ## ✨ What can it do?
24
-
25
- - 🖥️ **Figma → Code** — paste a link to a Figma frame, and the AI implements that exact screen in your codebase, exporting the design's icons and images into your project instead of inventing its own
26
- - 🎨 **Requirement → Figma** — paste a requirement, and the AI draws the screen in Figma, then screenshots its own work and fixes what's off
27
- - 🔁 **Token sync** — pull design tokens into your theme config, or build a design system in Figma from your code
28
- - ✏️ **Bulk editing** — rename layers, replace text across a page, reorganize frames
29
-
30
- ---
10
+ Fork of [figma-mcp-console](https://github.com/hoangann2000/figma-mcp-console), published on npm as [`figrelay`](https://www.npmjs.com/package/figrelay); the plugin shows as **Figma Relay**. **Migrating?** Swap the package name to `figrelay@latest`, re-import the plugin from this release, and replace `create_frame`/`create_rectangle`/`create_ellipse` with `create_shape`.
31
11
 
32
12
  ## ⚡ Quick start
33
13
 
34
- **Prerequisites**
35
-
36
- - [ ] [Node.js](https://nodejs.org) 18+ — check with `node --version`
37
- - [ ] **Figma Desktop** app (the web app can't run development plugins)
14
+ Needs [Node.js](https://nodejs.org) 18+ and the **Figma Desktop** app (the web app cannot run development plugins).
38
15
 
39
- ### Step 1 — Add the server to your AI tool
40
-
41
- No install needed — `npx` downloads and runs everything automatically.
42
-
43
- <details>
44
- <summary><b>Claude CLI</b></summary>
45
-
46
- For one project — run inside that project (registers it there only):
47
-
48
- ```sh
49
- claude mcp add figma-console -- npx -y figrelay@latest
50
- ```
51
-
52
- For all your projects at once — add it globally instead:
53
-
54
- ```sh
55
- claude mcp add --scope user figma-console -- npx -y figrelay@latest
56
- ```
57
-
58
- Both work the same at runtime: each Claude session starts its own server instance in that project's directory (so exported assets land in the right project), and all instances share one local bridge to Figma automatically.
59
-
60
- Or add to the project's `.mcp.json`:
16
+ **1. Add the server to your AI tool.** Most clients take this block; [docs/mcp-client-setup.md](docs/mcp-client-setup.md) has the config-file path and the one-line CLI command for each.
61
17
 
62
18
  ```json
63
19
  {
@@ -70,134 +26,19 @@ Or add to the project's `.mcp.json`:
70
26
  }
71
27
  ```
72
28
 
73
- </details>
74
-
75
- <details>
76
- <summary><b>Claude Desktop</b></summary>
77
-
78
- **Settings → Developer → Edit Config** (`claude_desktop_config.json`):
79
-
80
- ```json
81
- {
82
- "mcpServers": {
83
- "figma-console": {
84
- "command": "npx",
85
- "args": ["-y", "figrelay@latest"]
86
- }
87
- }
88
- }
89
- ```
90
-
91
- Restart Claude Desktop. On Windows use `"command": "cmd"`, `"args": ["/c", "npx", "-y", "figrelay@latest"]`.
92
-
93
- </details>
94
-
95
- <details>
96
- <summary><b>Codex CLI</b></summary>
97
-
98
- ```sh
99
- codex mcp add figma-console -- npx -y figrelay@latest
100
- ```
101
-
102
- Or in `~/.codex/config.toml`:
103
-
104
- ```toml
105
- [mcp_servers.figma-console]
106
- command = "npx"
107
- args = ["-y", "figrelay@latest"]
108
- ```
109
-
110
- </details>
111
-
112
- <details>
113
- <summary><b>VS Code (GitHub Copilot)</b></summary>
114
-
115
- Create `.vscode/mcp.json` (or **Command Palette → "MCP: Add Server"**):
116
-
117
- ```json
118
- {
119
- "servers": {
120
- "figma-console": {
121
- "type": "stdio",
122
- "command": "npx",
123
- "args": ["-y", "figrelay@latest"],
124
- "cwd": "${workspaceFolder}"
125
- }
126
- }
127
- }
128
- ```
129
-
130
- Use it from Copilot Chat in **Agent** mode.
131
-
132
- </details>
133
-
134
- <details>
135
- <summary><b>Cursor</b></summary>
136
-
137
- Create `.cursor/mcp.json` (or **Settings → MCP → Add new MCP server**):
138
-
139
- ```json
140
- {
141
- "mcpServers": {
142
- "figma-console": {
143
- "command": "npx",
144
- "args": ["-y", "figrelay@latest"]
145
- }
146
- }
147
- }
148
- ```
149
-
150
- </details>
151
-
152
- <details>
153
- <summary><b>Antigravity</b></summary>
154
-
155
- Agent panel → **MCP Servers → Manage MCP servers → View raw config** (`mcp_config.json`):
156
-
157
- ```json
158
- {
159
- "mcpServers": {
160
- "figma-console": {
161
- "command": "npx",
162
- "args": ["-y", "figrelay@latest"]
163
- }
164
- }
165
- }
166
- ```
167
-
168
- Click **Refresh** after saving.
169
-
170
- </details>
171
-
172
- <details>
173
- <summary><b>Other MCP clients (opencode, Windsurf, Cline, Gemini CLI, Zed, …)</b></summary>
174
-
175
- Standard MCP stdio transport — any client works. Set the command to `npx` with args `["-y", "figrelay@latest"]`.
176
-
177
- </details>
178
-
179
- ### Step 2 — Install the Figma plugin
180
-
181
- First, get the plugin folder onto your machine — **pick either option:**
182
-
183
- **📦 Option A — Download from GitHub (recommended)**
184
-
185
- > 1. Download **[plugin.zip](https://github.com/quannh287/figma-mcp-console/releases/latest)** → **unzip it**
186
-
187
- **⌨️ Option B — Get it via command** *(if the download doesn't work)*
188
-
189
- > 1. Run **`npx figrelay install-plugin`**
190
- > 2. It saves the plugin to a folder and **opens it automatically** so you can see `manifest.json`
191
-
192
- Then, in both cases:
193
-
194
- 2. In **Figma Desktop**: **Plugins → Development → Import plugin from manifest…** → select `manifest.json` *(one-time step)*
195
- 3. Run it: **Plugins → Development → Figma Relay** — and **keep the plugin window open** while you work. 🟢 Green dot = connected
196
- 4. Working with several Figma files at once? Run the plugin in **each** file — every window connects to the same local bridge, and the AI picks the right file per call
29
+ | Client | Where the config lives |
30
+ |---|---|
31
+ | Claude CLI | `claude mcp add figma-console -- npx -y figrelay@latest`, or `.mcp.json` |
32
+ | Claude Desktop | Settings → Developer → Edit Config (`claude_desktop_config.json`) |
33
+ | Codex CLI | `codex mcp add figma-console -- npx -y figrelay@latest`, or `~/.codex/config.toml` (TOML) |
34
+ | VS Code (Copilot) | `.vscode/mcp.json` — **different shape**, see the doc |
35
+ | Cursor | `.cursor/mcp.json` |
36
+ | Antigravity | MCP Servers → Manage → View raw config (`mcp_config.json`) |
37
+ | Others | Standard MCP stdio: command `npx`, args `["-y", "figrelay@latest"]` |
197
38
 
198
- ### Step 3 — Try it
39
+ **2. Install the Figma plugin.** Download [plugin.zip](https://github.com/quannh287/figma-relay/releases/latest) and unzip it, or run `npx figrelay@latest install-plugin` (saves it to a folder and opens it). Then in Figma Desktop: **Plugins → Development → Import plugin from manifest…** → pick `manifest.json` (one-time), and run **Plugins → Development → Figma Relay**. Keep the plugin window open — 🟢 green dot means connected. Working across several Figma files? Run the plugin in each one.
199
40
 
200
- **Figma → code** — right-click a frame → **Copy link to selection**, then:
41
+ **3. Try it.** Right-click a frame → **Copy link to selection**, then ask:
201
42
 
202
43
  ```text
203
44
  Implement this Figma page in our project: <paste link>. Match layout, spacing,
@@ -205,28 +46,22 @@ type and colors; export its icons/images into the project; screenshot the frame
205
46
  and compare when done.
206
47
  ```
207
48
 
208
- **Requirement → Figma:**
209
-
210
- ```text
211
- Design a SaaS landing page in Figma (desktop, 1440px): hero + CTA, three feature
212
- cards, pricing with three plans, footer. Light theme, use auto-layout, then
213
- screenshot and fix anything off.
214
- ```
49
+ Or the other direction: *"Design a SaaS landing page in Figma (desktop, 1440px): hero + CTA, three feature cards, pricing, footer. Light theme, auto-layout, then screenshot and fix anything off."* Asking for a **screenshot and compare** at the end makes the AI check its own work.
215
50
 
216
- > 🎯 Asking the AI to **screenshot and compare** at the end makes it check its own work instead of guessing.
51
+ ## 🎓 Agent skills
217
52
 
218
- ---
53
+ The tools tell the AI *what it can call*; the skills tell it *how to do the job well* — orient before writing, auto-layout over absolute positioning, reuse components and tokens, screenshot and verify. `npx figrelay@latest install-skills` writes them to `.claude/skills/<name>/SKILL.md`; other clients take the same files in their own skills folder. Sources live in [`skills/`](skills/) — edit them to match your conventions.
219
54
 
220
- ## 🗂 Multiple files, multiple projects
221
-
222
- All AI sessions and all Figma files share **one local bridge** on port 2000 — the first MCP server to start owns it, later ones join it automatically, and if the owner exits another session takes over within a second. Nothing to configure.
223
-
224
- - **Several Figma files**: run the plugin in each file (its window shows *Connected · \<file name\>*). With more than one file connected, every tool accepts a `file` parameter (file name, case-insensitive), and `list_files` shows what's connected — so you can say things like *"copy the primary color from **Design System** into the CTA button in **Landing Page**"* in a single session. With one file connected, nothing changes.
225
- - **Several code projects**: each AI session (one per project) reaches every connected file through the shared bridge. State which file you mean in the prompt and each session works with the right one.
55
+ | Skill | When it fires |
56
+ |---|---|
57
+ | `figrelay-design` | Anything that writes to a canvas — frames, text, components, variants, variables, prototype links |
58
+ | `figrelay-design-to-code` | "Implement this design" — map a frame onto the repo's components and tokens, export assets |
59
+ | `figrelay-generate-design` | New screens built from the file's existing components and styles |
60
+ | `figrelay-generate-library` | Build or update a Figma design system library from the codebase, in reviewed phases |
226
61
 
227
62
  ## ✂️ Hide tools you don't use
228
63
 
229
- Every listed tool takes up room in the AI's context, even in sessions that never touch Figma. Hide whole groups with `FIGMA_MCP_DISABLE` (or `-disable`): `components`, `styles`, `variables`, `prototype`. Hiding all four cuts the tool list by about a third.
64
+ Every listed tool costs context, even in sessions that never touch Figma. `FIGMA_MCP_DISABLE` (or the `-disable` flag) hides whole groups: `components`, `styles`, `variables`, `prototype`. Hiding all four cuts the tool list by about a third.
230
65
 
231
66
  ```json
232
67
  "figma-console": {
@@ -238,17 +73,7 @@ Every listed tool takes up room in the AI's context, even in sessions that never
238
73
 
239
74
  ## 🧠 Teach your AI what to export, and where
240
75
 
241
- Exports follow the file extension the AI picks (`.svg` for vector, `.png`/`.jpg` for bitmap, with an optional 0.5–4x scale). It decides based on what you ask — so put your preferences in the project rules file once, and stop repeating yourself:
242
-
243
- | AI tool | Rules file |
244
- |---|---|
245
- | Claude CLI / Claude Desktop | `CLAUDE.md` |
246
- | Codex / opencode | `AGENTS.md` |
247
- | Cursor | `.cursorrules` |
248
- | VS Code Copilot | `.github/copilot-instructions.md` |
249
- | Windsurf | `.windsurfrules` |
250
-
251
- > **📋 Copy-paste rules** (adjust paths — this fits Next.js):
76
+ Exports follow the extension the AI picks (`.svg` for vector, `.png`/`.jpg` for bitmap, optional 0.5–4x scale), so put your preferences in the project rules file once — `CLAUDE.md`, `AGENTS.md` (Codex/opencode), `.cursorrules`, `.github/copilot-instructions.md` or `.windsurfrules`:
252
77
 
253
78
  ```markdown
254
79
  ## Figma asset rules
@@ -262,19 +87,13 @@ Exports follow the file extension the AI picks (`.svg` for vector, `.png`/`.jpg`
262
87
 
263
88
  | Problem | Fix |
264
89
  |---|---|
265
- | ❌ "no Figma plugin is connected" | Open the plugin in Figma Desktop (**Plugins → Development → Figma Relay**) and keep its window open — in every file you want the AI to touch |
266
- | 🕐 Plugin says "Searching for MCP server…" | Normal before your AI client connects — the server starts automatically with it. If it never connects, click the status line to retry, or restart your AI client |
267
- | 🗂 "N Figma files are connected; pass the file parameter" | More than one plugin window is open — tell the AI which file you mean (or close the extra plugin windows) |
268
- | 🚫 The MCP server doesn't appear in your AI tool | Check Node.js 18+ is installed (`node -v`) |
269
- | 📁 Exported files end up in the wrong folder | Exports are written relative to where the server runs. Register it in the project-level config, or tell the AI an explicit export path |
270
- | 🔤 Font errors when creating text | The Figma file uses a font not installed on your machine — the error message names it |
271
-
272
- ## 🛠 Contributing
273
-
274
- Setup, how to add a tool, and the gotchas that cost time: **[CONTRIBUTING.md](CONTRIBUTING.md)**. Why the non-obvious parts work the way they do: **[docs/design-notes.md](docs/design-notes.md)**.
275
-
276
- ## 📄 License
90
+ | ❌ "no Figma plugin is connected" | Open the plugin (**Plugins → Development → Figma Relay**) and keep its window open — in every file the AI should touch |
91
+ | 🕐 Plugin says "Searching for MCP server…" | Normal until your AI client connects. If it never does, click the status line to retry, or restart the client |
92
+ | 🗂 "N Figma files are connected; pass the file parameter" | More than one plugin window is open — name the file you mean, or close the extras |
93
+ | 🚫 The server doesn't appear in your AI tool | Check Node.js 18+ (`node -v`) |
94
+ | 📁 Exported files land in the wrong folder | Exports are relative to where the server runs. Register it in the project-level config, or give the AI an explicit path |
95
+ | 🔤 Font errors when creating text | The file uses a font not installed on your machine — the error names it |
277
96
 
278
- [MIT](https://github.com/quannh287/figma-mcp-console/blob/main/LICENSE) — © [Hoang Le Thien An](https://github.com/hoangann2000) (original project), fork maintained by [@quannh287](https://github.com/quannh287)
97
+ ## 🛠 More
279
98
 
280
- > This is an independent community project, not affiliated with or endorsed by Figma, Inc. "Figma" is a trademark of Figma, Inc.
99
+ Several Figma files or projects at once: [docs/mcp-client-setup.md](docs/mcp-client-setup.md). Setup, adding a tool and the gotchas that cost time: [CONTRIBUTING.md](CONTRIBUTING.md). Why the non-obvious parts work that way: [docs/design-notes.md](docs/design-notes.md). License: [MIT](https://github.com/quannh287/figma-relay/blob/main/LICENSE) — © [Hoang Le Thien An](https://github.com/hoangann2000) (original project), fork maintained by [@quannh287](https://github.com/quannh287). Independent community project, not affiliated with or endorsed by Figma, Inc.; "Figma" is a trademark of Figma, Inc.
Binary file
Binary file
Binary file
Binary file
@@ -4,6 +4,10 @@
4
4
 
5
5
  figma.showUI(__html__, { width: 280, height: 104 });
6
6
 
7
+ // The UI can collapse to a dot; selection changes must not resize it back.
8
+ let collapsed = false;
9
+ let fullHeight = 104;
10
+
7
11
  // Tell the UI which file this plugin window runs in; the UI forwards it to
8
12
  // the bridge on every (re)connect so calls can be routed per file when
9
13
  // several files run the plugin at once. figma.fileKey is undefined for
@@ -1326,10 +1330,10 @@ async function postSelection() {
1326
1330
  // the paste-URL prompt when a frame needs a key (64) + up to 6 visible rows
1327
1331
  // (the list scrolls beyond that).
1328
1332
  const rows = Math.min(items.length, 6);
1329
- const h = items.length
1333
+ fullHeight = items.length
1330
1334
  ? 134 + (items.length > 1 ? 22 : 0) + (needsKey ? 64 : 0) + rows * 30
1331
1335
  : 104;
1332
- figma.ui.resize(280, h);
1336
+ if (!collapsed) figma.ui.resize(280, fullHeight);
1333
1337
  }
1334
1338
  figma.on("selectionchange", postSelection);
1335
1339
  postSelection();
@@ -1338,6 +1342,14 @@ postSelection();
1338
1342
 
1339
1343
  figma.ui.onmessage = async (msg) => {
1340
1344
  if (!msg) return;
1345
+ // Control message from the UI: collapse to a dot. The window has to stay
1346
+ // open for the bridge, so shrinking it is the only way to get the screen
1347
+ // space back. 70 is Figma's minimum width.
1348
+ if (typeof msg.collapsed === "boolean") {
1349
+ collapsed = msg.collapsed;
1350
+ figma.ui.resize(collapsed ? 70 : 280, collapsed ? 36 : fullHeight);
1351
+ return;
1352
+ }
1341
1353
  // Control message from the UI: the user pasted the file URL to enable links.
1342
1354
  if (typeof msg.setFileKey === "string") {
1343
1355
  setFileKey(msg.setFileKey);
@@ -57,6 +57,44 @@
57
57
  box-shadow: 0 0 0 3px rgba(34, 160, 107, 0.15);
58
58
  }
59
59
 
60
+ #collapse {
61
+ flex: none;
62
+ margin-left: auto;
63
+ width: 18px;
64
+ height: 18px;
65
+ border: 0;
66
+ border-radius: 5px;
67
+ background: transparent;
68
+ color: #8288a0;
69
+ font-size: 13px;
70
+ line-height: 1;
71
+ cursor: pointer;
72
+ }
73
+
74
+ #collapse:hover {
75
+ background: #eef0f7;
76
+ color: #2c2c34;
77
+ }
78
+
79
+ /* Collapsed: the dot only, at Figma's 70px minimum width. */
80
+ body.mini {
81
+ padding: 4px;
82
+ }
83
+
84
+ body.mini .card {
85
+ padding: 4px;
86
+ display: flex;
87
+ justify-content: center;
88
+ }
89
+
90
+ body.mini #status,
91
+ body.mini #collapse,
92
+ body.mini #hint,
93
+ body.mini #result,
94
+ body.mini #credit {
95
+ display: none;
96
+ }
97
+
60
98
  #hint {
61
99
  color: #6b6f7d;
62
100
  margin-top: 5px;
@@ -252,13 +290,14 @@
252
290
  <div class="card">
253
291
  <div id="status-row">
254
292
  <span id="dot"></span><span id="status">Connecting…</span>
293
+ <button id="collapse" title="Collapse to a dot">&minus;</button>
255
294
  </div>
256
295
  <div id="hint">Keep open while AI works · run it in every file.</div>
257
296
  <div id="result"></div>
258
297
  </div>
259
298
  <div id="credit">
260
299
  <span id="version"></span>
261
- <a href="https://github.com/quannh287/figma-mcp-console" target="_blank">@quannh287</a>
300
+ <a href="https://github.com/quannh287/figma-relay" target="_blank">@quannh287</a>
262
301
  </div>
263
302
 
264
303
  <script>
@@ -313,19 +352,20 @@
313
352
  sock.onmessage = (e) => {
314
353
  const msg = JSON.parse(e.data);
315
354
  if (msg.hello) {
316
- // Name the side that is behind: a stale server is just as common as
317
- // a stale plugin, and blaming the plugin sends people re-importing
318
- // a plugin that was already up to date.
319
355
  // Show which server build we are talking to; "dev" means a local
320
356
  // go build rather than a release.
321
357
  if (msg.hello.version) {
322
358
  document.getElementById("version").textContent = "v" + msg.hello.version;
323
359
  }
360
+ // Name the side that is behind: a stale server is just as common as
361
+ // a stale plugin, and blaming the plugin sends people re-importing
362
+ // a plugin that was already up to date.
324
363
  protoWarning = null;
325
364
  if (msg.hello.protocol && msg.hello.protocol !== PROTOCOL) {
365
+ // npx figrelay@latest only fetches the new server when the AI client restarts it.
326
366
  protoWarning =
327
367
  msg.hello.protocol < PROTOCOL
328
- ? "⚠ Server outdated — update figrelay"
368
+ ? "⚠ Server outdated — restart your AI client"
329
369
  : "⚠ Plugin outdated — re-import from the latest release";
330
370
  }
331
371
  setStatus(true, label());
@@ -549,7 +589,22 @@
549
589
  }
550
590
  };
551
591
 
552
- document.getElementById("status-row").onclick = () => connect();
592
+ // Collapsed, the whole card is the expand target; expanded, the status row
593
+ // still retries the connection.
594
+ let mini = false;
595
+ function setMini(on) {
596
+ mini = on;
597
+ document.body.classList.toggle("mini", on);
598
+ parent.postMessage({ pluginMessage: { collapsed: on } }, "*");
599
+ }
600
+ document.getElementById("collapse").onclick = (e) => {
601
+ e.stopPropagation();
602
+ setMini(true);
603
+ };
604
+ document.getElementById("status-row").onclick = () => {
605
+ if (mini) return setMini(false);
606
+ connect();
607
+ };
553
608
  document.addEventListener("visibilitychange", () => {
554
609
  if (!document.hidden && !ws) connect();
555
610
  });
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: figrelay-design
3
+ description: Create or modify content on a Figma Design canvas through the Figma Relay MCP server — frames, text, auto layout, components, variants, styles, variables and prototype links. Use whenever the user asks to draw, build, edit, restyle, rename, rearrange or wire anything in a Figma file. Triggers on "in Figma", "add a frame/button/screen", "change this component", "set up tokens/variables", "wire the prototype".
4
+ ---
5
+
6
+ # Writing to a Figma file
7
+
8
+ The plugin edits **the page that is open in Figma**, which the user can change at
9
+ any moment. Never assume where your work lands.
10
+
11
+ ## 1. Orient before writing
12
+
13
+ - `list_files` — only when several Figma files are connected; pass `file` to every
14
+ later call.
15
+ - `list_pages` then `set_current_page` — put the right page in front before creating.
16
+ - `get_metadata` for the page's top-level layers, `get_selection` for what the user
17
+ is pointing at. In a large file use `find_nodes` (by name, text, type, fill, font)
18
+ instead of walking the tree with `get_design_context`.
19
+ - `get_design_context` with `compact: true`. The full form costs several thousand
20
+ tokens for one frame; only drop `compact` when you need raw paint objects.
21
+
22
+ ## 2. Build top-down
23
+
24
+ 1. A root `create_shape` FRAME at an empty spot, sized for the target
25
+ (390x844 mobile, 1440x900 desktop). Everything else goes inside it via `parent_id`.
26
+ 2. `set_auto_layout` on every container — direction, `item_spacing`, padding,
27
+ sizing modes. Only position with x/y when auto layout genuinely does not fit:
28
+ absolute children do not reflow when content changes.
29
+ 3. `create_text` for copy, `create_shape` / `create_line` for structure.
30
+ Name every layer the way a designer would ("Header", "CTA / Primary"), never
31
+ "Rectangle 12".
32
+ 4. Style: `set_fills`, `set_strokes`, `set_corner_radius`, `set_effects`,
33
+ `set_text_properties`.
34
+
35
+ Reuse before you create: `get_local_components` + `create_instance`,
36
+ `get_local_styles` + `apply_style`, `get_variable_defs` + `set_bound_variable`.
37
+ A hard-coded hex where a token exists is a bug.
38
+
39
+ ## 3. Verify visually
40
+
41
+ `get_screenshot` on the root frame after each meaningful chunk — not once at the
42
+ end. Compare against the brief, fix with `move_nodes`, `resize_nodes`,
43
+ `set_characters`, `set_fills`, `remove_nodes`, screenshot again. A tall node comes
44
+ back scaled with a warning; screenshot a child instead of reading a thumbnail.
45
+
46
+ Finish with `set_selection` on the root frame so the user sees the result.
47
+
48
+ ## 4. Components, variables, prototypes
49
+
50
+ - **Component**: build the frame, then `create_component`. For variants, name each
51
+ component `Property=Value` (`Size=Large`) *before* `combine_as_variants`.
52
+ `add_component_property` returns the key `set_instance_properties` needs.
53
+ - **Variables**: `create_variable_collection` → `add_variable_mode` (light/dark) →
54
+ `create_variable` → `set_variable_value` per mode → `set_bound_variable` on nodes.
55
+ These are for single edits; a whole token set goes through `run_script`
56
+ (see below). **Check the mode limit before designing around modes**: on a
57
+ free/starter plan `add_variable_mode` fails with *"Limited to 1 modes only"* and
58
+ the light/dark-via-modes pattern is simply unavailable — the fallback is two
59
+ sibling collections (`tokens · Light`, `tokens · Dark`) and no mode switching.
60
+ Name variables with slashes (`primary/500`, not `color-primary-500`) — that is
61
+ what groups them in the Variables panel; flat names are unusable past ~50 tokens.
62
+ - **Prototype**: `set_prototype_links` only joins **top-level frames on the same
63
+ page**; a nested frame is rejected. Each call replaces that node's reactions, so
64
+ pass all of a node's links together. Check with `get_prototype_links`,
65
+ mark the entry with `set_flow_starting_point`.
66
+
67
+ ## Gotchas that cost time
68
+
69
+ - **Fonts**: `create_text` fails if the family is not installed locally.
70
+ `list_available_fonts` first when using anything but the file's existing fonts.
71
+ - **Rename, don't rebuild**: `rename_nodes`. Deleting and recreating a component
72
+ breaks every existing instance.
73
+ - **`resize_nodes` resets auto-layout sizing modes to FIXED.** Resize *before*
74
+ `set_auto_layout`, or re-apply the sizing modes afterwards — otherwise a frame
75
+ set to hug ends up locked at the resized height with its content spilling out.
76
+ It also does not move children unless they have constraints or auto layout.
77
+ - **Instances**: children have derived ids `I<instance-id>;<child-id>` — accepted
78
+ by every tool, returned by `find_nodes`.
79
+ - **Batch**: most edit tools take `node_ids`; feed `find_nodes` output straight in.
80
+
81
+ ## `run_script` — the bulk path
82
+
83
+ Default for anything token-shaped (importing 150 variables as 150 calls is absurd)
84
+ and for bulk rebinding. Single edits stay on the atomic tools: they fail with a
85
+ precise error, while a half-failed script leaves the document half-changed.
86
+
87
+ - **A thrown script keeps everything it already created.** Write find-or-create
88
+ scripts keyed by name so a retry adopts the orphan instead of duplicating it.
89
+ - **Read the line number before retrying.** The throw is often on the *return*
90
+ line — e.g. `return { n: figma.getLocalTextStyles().length }` — meaning the work
91
+ completed and only the summary failed. Inspect the document, then decide.
92
+ - **dynamic-page access**: sync accessors throw. Async-only —
93
+ `getLocalVariableCollectionsAsync`, `getVariableByIdAsync`,
94
+ `getLocalTextStylesAsync`, `getLocalEffectStylesAsync`, `getNodeByIdAsync`,
95
+ `setCurrentPageAsync`, `loadAllPagesAsync`, `node.getMainComponentAsync`,
96
+ `setTextStyleIdAsync`, `setEffectStyleIdAsync`, `setReactionsAsync`.
97
+ Still sync: `variables.createVariable`, `createVariableCollection`,
98
+ `setValueForMode`, `valuesByMode`, `createTextStyle`, `createEffectStyle`,
99
+ `combineAsVariants`.
100
+ - **Never call an async-only getter inside a `findAll`/`filter` predicate** — it
101
+ crashes the whole traversal (*"findAll callback crashed"*). Collect the nodes
102
+ first, then `for...of` with `await`.
103
+ - **Seed a bound paint from the variable's own value** — free insurance, not a
104
+ cure. Figma normalises the stored colour to the variable's resolved value at
105
+ bind time, so an in-file binding seeded black still renders correctly
106
+ (measured). The seed only ever matters where the variable *cannot* resolve —
107
+ a cross-file or library binding — which is untested. Do it anyway; it costs
108
+ one property read.
109
+
110
+ ```js
111
+ const col = (await figma.variables.getLocalVariableCollectionsAsync()).find(c => c.name === 'tokens · Light');
112
+ const mode = col.modes[0].modeId;
113
+ const v = await figma.variables.getVariableByIdAsync(id);
114
+ const paint = { type: 'SOLID', color: v.valuesByMode[mode], opacity: 1 };
115
+ node.fills = [figma.variables.setBoundVariableForPaint(paint, 'color', v)];
116
+ ```
117
+
118
+ ### Rebinding and deleting variables
119
+
120
+ Measured, not guessed:
121
+
122
+ - **Rebinding is safe and needs no reseed.** `setBoundVariableForPaint(paint, 'color', newVar)`
123
+ with the stale colour lands on the new variable's value. Mutating
124
+ `paint.boundVariables.color = {type:'VARIABLE_ALIAS', id}` directly works too.
125
+ - **Renaming never breaks a binding.** Bindings are by id; a renamed collection
126
+ or variable keeps every consumer. Rename freely.
127
+ - **Deleting a collection leaves silent zombies.** Nodes still bound keep the
128
+ alias and the last literal colour — they look right and are dead. Worse,
129
+ `getVariableByIdAsync(deadId)` and `getVariableCollectionByIdAsync` **still
130
+ return live-looking objects with correct names**, so a null check detects
131
+ nothing. The only reliable test is list membership:
132
+ `!(await figma.variables.getLocalVariablesAsync()).some(v => v.id === aliasId)`.
133
+ - **There is no consumer API.** `variable.consumers` is `undefined`, and
134
+ `Object.keys()` on a variable or collection returns `["id"]` — they are
135
+ proxies, so don't introspect them. To find consumers, scan the tree via
136
+ `node.boundVariables?.fills?.some(a => ids.includes(a.id))` (cheaper than
137
+ digging through each paint's `boundVariables`). A plain property read like
138
+ this is fine inside a `findAll` predicate; an async getter is not.
139
+ - **`collection.remove()` is immediate with no API undo.** Re-scan and assert
140
+ zero remaining consumers *before* removing — you get no error if you miss some.
141
+
142
+ - Split by **logical group**, not node count — the point is error isolation.
143
+ Scripts creating 300+ variables, or several hundred nodes, run without timing out.
144
+ - Pass a `token` anyway (it costs nothing). A timed-out script **keeps running and
145
+ keeps writing**, so the token is what lets a retry join the original run instead
146
+ of duplicating it. Never redo a timed-out script's work blind.
147
+
148
+ Raw-API-only gotchas (the atomic tools already handle these):
149
+ `figma.combineAsVariants` neither names nor lays out the set — set `name`,
150
+ `layoutMode`, `layoutWrap`, `primaryAxisSizingMode = 'FIXED'`, `resize`, padding
151
+ and spacing yourself, or every variant stacks at 0,0. `layoutAlign = 'STRETCH'`
152
+ does not stretch a child unless `primaryAxisSizingMode` is also `FIXED`.
153
+ `layoutPositioning = 'ABSOLUTE'` must be set *after* `appendChild`.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: figrelay-design-to-code
3
+ description: Turn a Figma frame, component or selection into code in this repository — read the design through the Figma Relay MCP server, map it onto existing components and design tokens, export the assets it needs. Use when the user says "implement this design", "build this screen", "code this component", "match the Figma", or pastes/points at a Figma selection.
4
+ ---
5
+
6
+ # Figma → code
7
+
8
+ Goal: code that matches the design **using this codebase's own components and
9
+ tokens**. A pixel-perfect one-off built from raw divs and hex codes is a failure.
10
+
11
+ ## 1. Read the design
12
+
13
+ - `get_selection` when the user says "this" / "the selected frame"; otherwise
14
+ `find_nodes` by layer name. `get_metadata` / `list_pages` if you are lost.
15
+ - `get_design_context` with `compact: true` and a sensible `depth` (2–3 first,
16
+ deeper only where needed). Full depth on a whole screen buries the real
17
+ structure in thousands of tokens.
18
+ - `get_screenshot` on the frame. Read the picture and the tree together — the
19
+ tree gives names and numbers, the picture gives intent.
20
+
21
+ ## 2. Map before you write
22
+
23
+ - `get_variable_defs` and `get_local_styles` → the design's tokens. Match them to
24
+ the repo's theme/tokens by **value and name**; write the repo's token, never the
25
+ raw hex. Report any design token that has no counterpart instead of inventing one.
26
+ Read the repo's token **source** (theme config, tokens JSON, Tailwind config) —
27
+ never a built CSS bundle, which is utility output and gets truncated before the
28
+ `:root` block.
29
+ - `get_local_components` → named components in the design. Grep this repo for the
30
+ same concept (Button, Card, Modal) and reuse it. Only build new when nothing fits.
31
+ - Auto layout maps onto flex: `direction` → `flex-direction`, `item_spacing` →
32
+ `gap`, padding → padding, sizing modes → `fixed` / `hug` (fit-content) /
33
+ `fill` (flex: 1). Absolutely positioned children are the exception, not the rule.
34
+ - Fixed pixel widths in the design usually mean "fill the container" in code.
35
+ Ask what the responsive behaviour is rather than hard-coding 390px.
36
+
37
+ ## 3. Export assets
38
+
39
+ `download_assets`, one item per node, chosen per node after checking its type and
40
+ fills:
41
+
42
+ - **SVG** for pure vector — icons, logos, path illustrations.
43
+ - **PNG at `scale: 2`** for anything with a raster IMAGE fill, a photo, or
44
+ blur/shadow effects SVG cannot represent.
45
+ - **JPG** only for large photos where size matters.
46
+
47
+ Paths follow this project's conventions (check `CLAUDE.md` / `AGENTS.md` and
48
+ where existing assets live). Without a convention, defaults are
49
+ `assets/icons/<name>.svg` and `assets/images/<name>.<ext>`. Everything is written
50
+ relative to the directory the MCP server runs in and cannot escape it.
51
+
52
+ ## 4. Verify
53
+
54
+ Run the app or the component's story, then compare with `get_screenshot` of the
55
+ Figma node side by side. Spacing, font size/weight, radius and colour are the
56
+ usual misses. State plainly what you could not match and why.
57
+
58
+ ## Don't
59
+
60
+ - Don't copy absolute x/y into CSS. Rebuild with flex from the auto layout.
61
+ - Don't invent token names. Use the repo's, or flag the gap.
62
+ - Don't export a whole frame as one PNG to "save time" — export its real assets.
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: figrelay-generate-design
3
+ description: Assemble full screens in Figma out of the file's existing components, styles and variables through the Figma Relay MCP server — instead of drawing rectangles from scratch. Use when the user asks for a new page/screen/flow "using our design system", "with our components", or wants several screens that stay consistent.
4
+ ---
5
+
6
+ # Generating screens from a design system
7
+
8
+ Rule: **instantiate, don't draw.** A screen made of raw rectangles is not a
9
+ deliverable when the file has components.
10
+
11
+ ## 1. Inventory first (do not skip)
12
+
13
+ - `get_local_components` — every COMPONENT / COMPONENT_SET in the file, all pages.
14
+ - `get_local_styles` — paint, text, effect styles.
15
+ - `get_variable_defs` — collections, modes, tokens.
16
+ - `get_metadata` + `list_pages` — where existing screens live; open the right page
17
+ with `set_current_page`.
18
+
19
+ Screenshot one existing screen (`find_nodes` → `get_screenshot`) to learn the
20
+ house layout: page padding, section spacing, header/footer pattern. Copy it.
21
+
22
+ Report what is missing before building. If there is no Card component, say so and
23
+ ask whether to build one or approximate — don't silently fake it.
24
+
25
+ ## 2. Compose
26
+
27
+ 1. Root `create_shape` FRAME at the target size, named like the existing screens,
28
+ placed to the right of them with a clear gap.
29
+ 2. Resize the root first, **then** `set_auto_layout` VERTICAL on it with the house
30
+ padding and spacing — resizing afterwards resets sizing modes to FIXED.
31
+ Every section inside is itself an auto-layout frame. Nothing absolute.
32
+ 3. `create_instance` for each component, `set_instance_properties` for its variant
33
+ and text overrides. Swap with `swap_component` rather than detaching.
34
+ **Never `detach_instance`** to "make it fit" — that is how a design system dies.
35
+ 4. Only `create_text` / `create_shape` for genuinely bespoke content, and then
36
+ style it with `apply_style` / `set_bound_variable`, never a literal hex.
37
+
38
+ ## 3. Verify and iterate
39
+
40
+ `get_screenshot` per section as you go, not once at the end. Check against the
41
+ house screen: alignment, rhythm, and that every colour came from a style or
42
+ variable. Fix, re-screenshot, then `set_selection` on the root frame.
43
+
44
+ ## Multiple screens
45
+
46
+ Build the first screen, screenshot it, **get the user's sign-off**, then clone
47
+ the structure for the rest (`clone_node` with `parent_id` and x/y — a clone inside
48
+ a SECTION otherwise lands at 0,0). Wire the flow with `set_prototype_links`
49
+ between the top-level frames and `set_flow_starting_point` on the entry screen.
50
+
51
+ ## Bulk work
52
+
53
+ Dozens of identical instances, or rebinding every fill in a subtree, is what
54
+ `run_script` is for. Everything smaller stays on the atomic tools — they fail with
55
+ a precise error instead of half-writing the document.
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: figrelay-generate-library
3
+ description: Build or update a Figma design system library from this codebase through the Figma Relay MCP server — tokens as variables, text/paint/effect styles, then components with variants. Runs in phases and stops for review between them. Use when the user asks to "create our design system in Figma", "sync our tokens to Figma", or "turn our component library into Figma components".
4
+ ---
5
+
6
+ # Codebase → Figma library
7
+
8
+ Long, destructive-by-accident work. **Stop for review after every phase.** Never
9
+ run the whole thing in one go.
10
+
11
+ ## Phase 1 — Discovery (read only)
12
+
13
+ - In the repo: find the **token source file** (theme config, design-tokens JSON,
14
+ Tailwind config, CSS custom properties in source form) and the per-component
15
+ style definitions (`tva({...})`, variant maps, style objects). Grep, don't guess.
16
+ - In Figma: `get_variable_defs`, `get_local_styles`, `get_local_components`,
17
+ `list_pages` — what already exists.
18
+ - **Check the plan's mode limit** before promising light/dark via modes — see
19
+ Phase 2.
20
+
21
+ **Never reverse-engineer a compiled stylesheet.** A built CSS bundle is hundreds
22
+ of KB of utility output, gets truncated by the read cap, and usually hides the
23
+ `:root` block past the cut. The config source has the literal values, and the
24
+ component's variant block has the real spec — `xs: 'px-3.5 h-8'` gives you
25
+ padding 14 / height 32 and the exact token name `bg-primary-500`. That fidelity
26
+ is what makes the library match the app; screenshots and built CSS do not.
27
+
28
+ Report a plan: tokens found, components found, what already exists in Figma, what
29
+ you propose to create vs update, and anything ambiguous. **Wait for approval.**
30
+
31
+ ## Phase 2 — Foundations (variables and styles)
32
+
33
+ Do this phase in `run_script`, not the typed variable tools — a hundred tokens is
34
+ a hundred round-trips otherwise. Read the `run_script` rules in
35
+ `figrelay-design` first; the ones that bite hardest here:
36
+
37
+ - **Modes are a plan limit.** `addMode` on a free/starter plan throws
38
+ *"Limited to 1 modes only"*. Probe it on the first collection before building
39
+ anything around light/dark. Fallback: two sibling collections
40
+ (`tokens · Light`, `tokens · Dark`), components bound to Light, and tell the
41
+ user plainly that mode switching is off the table. Discovering this 300
42
+ variables in means redoing the phase.
43
+ - **The script that throws keeps what it made.** The collection created on the
44
+ line before a failed `addMode` survives. Every script here is find-or-create
45
+ keyed by name, and adopts an orphan rather than making a second one:
46
+ `let c = cols.find(x => x.name === target) ?? adopt(cols.find(x => x.name === 'tokens'))`.
47
+ - **Slash names** (`primary/500`, `space/md`) — that is what makes the groups in
48
+ the Variables panel. Convert `--color-primary-500` on the way in.
49
+ - **Seed bound paints from `valuesByMode`** — cheap insurance. In-file bindings
50
+ render correctly even seeded black (Figma normalises at bind time); the seed
51
+ only matters for bindings that cannot resolve.
52
+ - Names mirror the code so a human can trace a token back to its source.
53
+ - Updating an existing collection: set the value on the existing variable.
54
+ A duplicate collection is the failure mode here.
55
+ - `create_paint_style`, `create_text_style`, `create_effect_style` for what
56
+ designers pick from the UI.
57
+
58
+ **Stop and show `get_variable_defs` output.**
59
+
60
+ ### Splitting or merging collections
61
+
62
+ Needed whenever the mode limit forces one collection into two. Every step below
63
+ is measured; the sequence as a bulk operation is not, so verify as you go.
64
+
65
+ 1. Create the target collection's variables with the same names, copying values
66
+ from the source via `valuesByMode`.
67
+ 2. Build a `name → newVariable` map.
68
+ 3. Scan for consumers with `node.boundVariables?.fills?.some(a => ids.includes(a.id))`
69
+ and rebind each hit. Rebinding preserves the rendered value and needs no
70
+ reseed; renaming a collection or variable never breaks a binding.
71
+ 4. **Re-scan and assert zero consumers of the old collection before removing it.**
72
+ This is the whole safety net: a node left bound to a removed collection keeps
73
+ its alias and its last colour, looks perfectly fine, and is dead — and
74
+ `getVariableByIdAsync` still returns a live-looking object for it, so a null
75
+ check finds nothing. Test with list membership
76
+ (`getLocalVariablesAsync().some(v => v.id === aliasId)`), never fetchability.
77
+ 5. Only then `collection.remove()` — immediate, with no undo from the API.
78
+
79
+ ## Phase 3 — File structure
80
+
81
+ One page per group (Foundations, Components, Patterns) via the user's convention;
82
+ `set_current_page` before building. Moving finished components to another page
83
+ later is safe — cross-page instance references hold (verified over 54 instances);
84
+ `set_current_page` in a script is `setCurrentPageAsync`. A labelled section per component, laid out on
85
+ a grid with generous spacing — a library people scroll through, not a pile.
86
+
87
+ ## Phase 4 — Components
88
+
89
+ Per component, simplest first:
90
+
91
+ 1. Build the base frame, **resize it first, then `set_auto_layout`** — resizing
92
+ after configuring sizing modes silently resets them to FIXED and you get a
93
+ component locked at the resized height with its text spilling out. Bind every
94
+ colour/radius/space to the Phase 2 variables (`set_bound_variable`) or styles
95
+ (`apply_style`).
96
+ 2. Build each variant as its own frame named `Property=Value`
97
+ (`Variant=Primary`, `Size=Large`), `create_component` each, then
98
+ `combine_as_variants`.
99
+ 3. `add_component_property` for TEXT / BOOLEAN / INSTANCE_SWAP props, bound to the
100
+ child layer they drive, so the property actually does something.
101
+ 4. `get_screenshot` the set and compare against the real rendered component.
102
+
103
+ Batch by component family, pause between families.
104
+
105
+ ## Phase 5 — QA
106
+
107
+ - Every component: screenshot vs the real thing in the app.
108
+ - Every fill/radius/spacing: bound to a variable or style, no stray hex
109
+ (`find_nodes` by fill colour catches leftovers).
110
+ - Naming matches the code, variants resolve, instances still work after edits.
111
+ - Report coverage: what shipped, what was skipped, what needs a designer.
112
+
113
+ ## Never
114
+
115
+ - Never `remove_nodes` on existing library components to "rebuild cleanly" —
116
+ it breaks every instance across every file. `rename_nodes` and edit in place.
117
+ - Never run a phase the user has not approved.
118
+ - Never resize a frame after setting its auto-layout sizing modes.
119
+ - Never treat a `run_script` error as "nothing happened" — check the document
120
+ first; the throw is often on the return line, after all the work succeeded.
121
+ - Never design around variable modes without confirming the plan allows >1.
122
+ - Never scrape compiled CSS for tokens when the config source is in the repo.
123
+ - Never remove a variable collection without first asserting it has no
124
+ consumers left — the failure is silent and looks correct.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figrelay",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "scripts": {
5
5
  "prepublishOnly": "cp ../README.md ./README.md"
6
6
  },
@@ -17,11 +17,11 @@
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/quannh287/figma-mcp-console.git"
20
+ "url": "git+https://github.com/quannh287/figma-relay.git"
21
21
  },
22
- "homepage": "https://github.com/quannh287/figma-mcp-console#readme",
22
+ "homepage": "https://github.com/quannh287/figma-relay#readme",
23
23
  "bugs": {
24
- "url": "https://github.com/quannh287/figma-mcp-console/issues"
24
+ "url": "https://github.com/quannh287/figma-relay/issues"
25
25
  },
26
26
  "keywords": [
27
27
  "figma",
package/run.js CHANGED
@@ -51,6 +51,24 @@ if (process.argv[2] === "install-plugin") {
51
51
  process.exit(0);
52
52
  }
53
53
 
54
+ // Subcommand: install-skills — copy the agent skills into the current project
55
+ // so the AI client picks them up from .claude/skills/<name>/SKILL.md.
56
+ if (process.argv[2] === "install-skills") {
57
+ const srcDir = path.join(__dirname, "bin", "skills");
58
+ const destDir = path.join(process.cwd(), ".claude", "skills");
59
+ if (!fs.existsSync(srcDir)) fail("skills not found in this package");
60
+ for (const name of fs.readdirSync(srcDir)) {
61
+ fs.mkdirSync(path.join(destDir, name), { recursive: true });
62
+ fs.copyFileSync(
63
+ path.join(srcDir, name, "SKILL.md"),
64
+ path.join(destDir, name, "SKILL.md"),
65
+ );
66
+ process.stdout.write(` ${name}\n`);
67
+ }
68
+ process.stdout.write(`\n✅ Skills installed into ${destDir}\n`);
69
+ process.exit(0);
70
+ }
71
+
54
72
  if (!fs.existsSync(binPath)) fail(`binary not found for ${plat}-${arch}`);
55
73
  try {
56
74
  fs.chmodSync(binPath, 0o755);