@todoforai/figma-api 1.0.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/LICENSE +21 -0
- package/README.md +118 -0
- package/package.json +22 -0
- package/plugin/code.js +48 -0
- package/plugin/manifest.json +13 -0
- package/plugin/ui.html +64 -0
- package/src/api.ts +105 -0
- package/src/bridge.ts +96 -0
- package/src/index.ts +645 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TODOforAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# figma-api
|
|
2
|
+
|
|
3
|
+
Figma CLI — a subcommand-per-endpoint wrapper of the official **Figma REST API**,
|
|
4
|
+
plus a **plugin bridge** for the canvas writes the REST API can't do.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
figma-api auth figd_xxx # personal access token (or FIGMA_TOKEN env)
|
|
8
|
+
figma-api me
|
|
9
|
+
figma-api file https://figma.com/design/AbC123/My-File
|
|
10
|
+
figma-api comment-add AbC123 "looks great" --node 1:2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Inspired by the Figma MCP servers (read design context) but extended to the full
|
|
14
|
+
REST surface **and** to live canvas manipulation through a companion plugin.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i -g @todoforai/figma-api # or: bun add -g @todoforai/figma-api
|
|
20
|
+
figma-api --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Auth
|
|
24
|
+
|
|
25
|
+
Personal Access Token, stored in `~/.config/figma-api/credentials.json`, or passed
|
|
26
|
+
via `FIGMA_TOKEN` (`FIGMA_API_TOKEN`) env. OAuth bearer tokens also work.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# https://www.figma.com/settings → Personal access tokens → Generate
|
|
30
|
+
figma-api auth figd_xxx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Scopes: reads need `file_content:read`; writes need the matching scope
|
|
34
|
+
(`file_comments:write`, `file_dev_resources:write`, `webhooks:write`,
|
|
35
|
+
`file_variables:*` is Enterprise-only).
|
|
36
|
+
|
|
37
|
+
## REST commands
|
|
38
|
+
|
|
39
|
+
Every file command takes a raw file key **or** a Figma URL (`node-id` is parsed
|
|
40
|
+
from the URL).
|
|
41
|
+
|
|
42
|
+
Read: `me`, `file`, `nodes`, `file-meta`, `versions`, `images`, `image-fills`,
|
|
43
|
+
`comments`, `reactions`, `projects`, `project-files`, `components`/`team-components`/
|
|
44
|
+
`component`, `component-sets`/`team-component-sets`, `styles`/`team-styles`/`style`,
|
|
45
|
+
`variables-local`/`variables-published`, `dev-resources`, `webhooks`/`webhook`/
|
|
46
|
+
`webhook-requests`, `analytics`, `activity-logs`, `payments`, `oembed`.
|
|
47
|
+
|
|
48
|
+
Write: `comment-add`/`comment-delete`, `reaction-add`/`reaction-delete`,
|
|
49
|
+
`variables-modify`, `dev-resource-add`/`dev-resources-bulk-add`/`dev-resources-update`/
|
|
50
|
+
`dev-resource-delete`, `webhook-create`/`webhook-update`/`webhook-delete`.
|
|
51
|
+
|
|
52
|
+
Run `figma-api <command> --help` for parameters, scopes and examples.
|
|
53
|
+
|
|
54
|
+
## Plugin bridge — canvas writes
|
|
55
|
+
|
|
56
|
+
The REST API **cannot create canvas nodes** (frames/text/shapes) or edit variables
|
|
57
|
+
off-Enterprise. The Figma **Plugin API** can. The bridge lets the CLI drive a small
|
|
58
|
+
companion plugin:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
figma-api run '...' ──POST──▶ relay (figma-api bridge) ◀──poll── Figma plugin
|
|
62
|
+
──result─▶ (runs on canvas)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Setup:
|
|
66
|
+
|
|
67
|
+
1. `figma-api bridge` — start the relay (keep it running).
|
|
68
|
+
2. Figma **desktop** → Plugins → Development → **Import plugin from manifest** →
|
|
69
|
+
`plugin/manifest.json`. Run it, paste the relay URL, click **Connect**.
|
|
70
|
+
3. Drive it from the CLI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
figma-api ping
|
|
74
|
+
figma-api run 'const t = figma.createText(); await figma.loadFontAsync({family:"Inter",style:"Regular"}); t.characters="Hi from the CLI"; figma.currentPage.appendChild(t); return t'
|
|
75
|
+
figma-api run @make-card.js
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`run` executes arbitrary Figma Plugin API code (`figma` in scope, `await` and
|
|
79
|
+
`return` supported), so it subsumes any draw/create helper.
|
|
80
|
+
|
|
81
|
+
Cross-machine: `cloudflared tunnel --url http://localhost:8917` and paste that
|
|
82
|
+
https URL into the plugin.
|
|
83
|
+
|
|
84
|
+
> ⚠️ **Security:** the relay has no auth and `run` executes arbitrary code in your
|
|
85
|
+
> Figma document. Only use it on a trusted machine/network; don't expose the tunnel
|
|
86
|
+
> URL publicly or run code you don't trust.
|
|
87
|
+
|
|
88
|
+
> Note: development (unpublished) plugins can only be imported in the **Figma
|
|
89
|
+
> desktop app**, not the browser. Once published, the browser works too.
|
|
90
|
+
|
|
91
|
+
## Why both REST and a plugin?
|
|
92
|
+
|
|
93
|
+
| | REST API | Plugin bridge |
|
|
94
|
+
|---|----------|---------------|
|
|
95
|
+
| Files / nodes / images (read) | ✅ | ✅ |
|
|
96
|
+
| Comments, dev resources, webhooks | ✅ | — |
|
|
97
|
+
| Create frames / text / shapes | ❌ (no endpoint) | ✅ |
|
|
98
|
+
| Edit variables off-Enterprise | ❌ (403) | ✅ |
|
|
99
|
+
| Works headless / CI | ✅ | needs Figma open |
|
|
100
|
+
|
|
101
|
+
## Reference
|
|
102
|
+
|
|
103
|
+
- [figma/rest-api-spec](https://github.com/figma/rest-api-spec) — authoritative endpoint list.
|
|
104
|
+
- [GLips/Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP) — read design-context MCP.
|
|
105
|
+
- [Official Figma MCP](https://help.figma.com/hc/en-us/articles/32132100833559) — `mcp.figma.com`.
|
|
106
|
+
- The bridge mirrors the architecture of community plugin bridges (e.g.
|
|
107
|
+
southleft/figma-console-mcp) but driven by a plain CLI instead of MCP.
|
|
108
|
+
|
|
109
|
+
## Dev
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bun install
|
|
113
|
+
bun src/index.ts --help
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@todoforai/figma-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Figma CLI — full REST API wrapper (files, comments, variables, webhooks…) plus a plugin bridge for canvas writes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"figma-api": "src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": ["src", "plugin", "README.md", "LICENSE"],
|
|
10
|
+
"keywords": ["figma", "figma-api", "cli", "rest", "design", "plugin", "mcp"],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/todoforai/figma-api.git"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^14.0.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/plugin/code.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// figma-api bridge plugin. The UI (ui.html) long-polls the relay, forwards each
|
|
2
|
+
// command here, this runs it and returns the result. The REST API can't create
|
|
3
|
+
// canvas nodes or edit variables off-Enterprise — the Plugin API can, and the
|
|
4
|
+
// figma-api CLI drives it through `figma-api run`.
|
|
5
|
+
|
|
6
|
+
// Serialize a value for JSON transport: Figma nodes → {id,type,name}, recurse
|
|
7
|
+
// plain objects/arrays, guard against cycles and unstringifiable values.
|
|
8
|
+
function serialize(v, seen) {
|
|
9
|
+
if (v == null || typeof v === "boolean" || typeof v === "number" || typeof v === "string") return v;
|
|
10
|
+
if (typeof v === "bigint") return v.toString();
|
|
11
|
+
if (typeof v === "function" || typeof v === "symbol" || typeof v === "undefined") return undefined;
|
|
12
|
+
if (typeof v.id === "string" && typeof v.type === "string") return { id: v.id, type: v.type, name: v.name };
|
|
13
|
+
seen = seen || new Set();
|
|
14
|
+
if (seen.has(v)) return "[circular]";
|
|
15
|
+
seen.add(v);
|
|
16
|
+
if (Array.isArray(v)) return v.map((x) => serialize(x, seen));
|
|
17
|
+
const o = {};
|
|
18
|
+
for (const k of Object.keys(v)) { try { o[k] = serialize(v[k], seen); } catch (_) {} }
|
|
19
|
+
return o;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function exec(cmd) {
|
|
23
|
+
if (cmd.op === "ping") {
|
|
24
|
+
return { pong: true, page: figma.currentPage.name, selection: figma.currentPage.selection.map((n) => n.id) };
|
|
25
|
+
}
|
|
26
|
+
if (cmd.op === "eval") {
|
|
27
|
+
// Run arbitrary Plugin API code. `figma` is in scope; code may use await
|
|
28
|
+
// and `return` a value. WARNING: this executes whatever the relay delivers.
|
|
29
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
30
|
+
const out = await new AsyncFunction("figma", cmd.code || "")(figma);
|
|
31
|
+
return { value: serialize(out) };
|
|
32
|
+
}
|
|
33
|
+
return { error: "unknown op: " + cmd.op };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
figma.showUI(__html__, { width: 340, height: 320 });
|
|
37
|
+
|
|
38
|
+
figma.ui.onmessage = async (msg) => {
|
|
39
|
+
if (msg.type === "exec") {
|
|
40
|
+
let result;
|
|
41
|
+
try { result = await exec(msg.cmd); }
|
|
42
|
+
catch (e) { result = { error: String((e && e.message) || e) }; }
|
|
43
|
+
figma.ui.postMessage({ type: "result", id: msg.cmd.id, result });
|
|
44
|
+
figma.notify(result.error ? "⚠️ " + result.error : "✅ " + msg.cmd.op);
|
|
45
|
+
} else if (msg.type === "close") {
|
|
46
|
+
figma.closePlugin();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figma-api bridge",
|
|
3
|
+
"id": "figma-api-bridge-todoforai",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"main": "code.js",
|
|
6
|
+
"ui": "ui.html",
|
|
7
|
+
"editorType": ["figma"],
|
|
8
|
+
"documentAccess": "dynamic-page",
|
|
9
|
+
"networkAccess": {
|
|
10
|
+
"allowedDomains": ["*"],
|
|
11
|
+
"reasoning": "Long-polls the local figma-api relay to receive drawing commands from the CLI."
|
|
12
|
+
}
|
|
13
|
+
}
|
package/plugin/ui.html
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head><meta charset="utf-8" /></head>
|
|
4
|
+
<body style="font:13px -apple-system,Segoe UI,Roboto,sans-serif;margin:0;padding:14px;color:#1a1a1a">
|
|
5
|
+
<h3 style="margin:0 0 6px">figma-api bridge</h3>
|
|
6
|
+
<p style="margin:0 0 10px;color:#666">Connect to the relay, then drive drawing from the <code>figma-api</code> CLI.</p>
|
|
7
|
+
|
|
8
|
+
<label style="display:block;margin:6px 0 2px;font-weight:600">Relay URL</label>
|
|
9
|
+
<input id="relay" value="http://localhost:8917" style="width:100%;box-sizing:border-box;padding:7px;border:1px solid #ccc;border-radius:6px" />
|
|
10
|
+
|
|
11
|
+
<div style="display:flex;gap:8px;margin-top:10px">
|
|
12
|
+
<button id="connect" style="flex:1;padding:9px;background:#1F6FEB;color:#fff;border:0;border-radius:8px;font-weight:600;cursor:pointer">Connect</button>
|
|
13
|
+
<button id="stop" style="flex:1;padding:9px;background:#eee;color:#333;border:0;border-radius:8px;font-weight:600;cursor:pointer">Stop</button>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div id="status" style="margin-top:10px;color:#666;min-height:18px">Idle.</div>
|
|
17
|
+
<pre id="log" style="margin-top:8px;background:#0d1117;color:#8b949e;padding:8px;border-radius:6px;height:120px;overflow:auto;font-size:11px"></pre>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
const $ = (id) => document.getElementById(id);
|
|
21
|
+
let running = false;
|
|
22
|
+
const log = (m) => { const l = $("log"); l.textContent += m + "\n"; l.scrollTop = l.scrollHeight; };
|
|
23
|
+
|
|
24
|
+
let resolveResult = null;
|
|
25
|
+
// plugin code → result → resolve the pending exec
|
|
26
|
+
onmessage = (e) => {
|
|
27
|
+
const m = e.data.pluginMessage;
|
|
28
|
+
if (m && m.type === "result" && resolveResult) { const r = resolveResult; resolveResult = null; r(m); }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function loop() {
|
|
32
|
+
const relay = $("relay").value.replace(/\/$/, "");
|
|
33
|
+
$("status").textContent = "🟢 Connected — polling " + relay;
|
|
34
|
+
while (running) {
|
|
35
|
+
let cmd;
|
|
36
|
+
try {
|
|
37
|
+
cmd = await (await fetch(relay + "/poll")).json();
|
|
38
|
+
} catch (_) {
|
|
39
|
+
$("status").textContent = "🔴 Relay unreachable — retrying";
|
|
40
|
+
await new Promise((res) => setTimeout(res, 1500));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!cmd || !cmd.op) continue;
|
|
44
|
+
log("◀ " + cmd.op + " (" + cmd.id + ")");
|
|
45
|
+
// Run one command at a time and wait for its result (no overlap).
|
|
46
|
+
const m = await new Promise((resolve) => {
|
|
47
|
+
resolveResult = resolve;
|
|
48
|
+
parent.postMessage({ pluginMessage: { type: "exec", cmd } }, "*");
|
|
49
|
+
});
|
|
50
|
+
let bodyResult = m.result;
|
|
51
|
+
try { JSON.stringify(bodyResult); } catch (_) { bodyResult = { error: "result not serializable" }; }
|
|
52
|
+
log("▶ " + m.id + " " + JSON.stringify(bodyResult).slice(0, 60));
|
|
53
|
+
try {
|
|
54
|
+
await fetch(relay + "/result", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: m.id, result: bodyResult }) });
|
|
55
|
+
} catch (_) { log("⚠ result POST failed"); }
|
|
56
|
+
}
|
|
57
|
+
$("status").textContent = "⏹ Stopped.";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$("connect").onclick = () => { if (!running) { running = true; loop(); } };
|
|
61
|
+
$("stop").onclick = () => { running = false; };
|
|
62
|
+
</script>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
const BASE = "https://api.figma.com";
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".config", "figma-api");
|
|
7
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
8
|
+
|
|
9
|
+
export interface Credentials {
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function saveCredentials(creds: Credentials): void {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + "\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadCredentials(): Credentials | null {
|
|
19
|
+
if (!existsSync(CREDENTIALS_FILE)) return null;
|
|
20
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function credentialsExist(): boolean {
|
|
24
|
+
return existsSync(CREDENTIALS_FILE) || !!(process.env.FIGMA_TOKEN || process.env.FIGMA_API_TOKEN);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve token from env (FIGMA_TOKEN / FIGMA_API_TOKEN) or saved credentials. */
|
|
28
|
+
export function loadToken(): string {
|
|
29
|
+
const env = process.env.FIGMA_TOKEN || process.env.FIGMA_API_TOKEN;
|
|
30
|
+
if (env) return env;
|
|
31
|
+
const creds = loadCredentials();
|
|
32
|
+
if (!creds?.token) {
|
|
33
|
+
console.error("No token found. Run: figma-api auth <personal-access-token>");
|
|
34
|
+
console.error("Or set FIGMA_TOKEN env var. Create a token at https://www.figma.com/settings (Personal access tokens).");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return creds.token;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** OAuth bearer tokens start with "figo"/"figu"; PATs use the X-Figma-Token header. */
|
|
41
|
+
function authHeaders(): Record<string, string> {
|
|
42
|
+
const token = loadToken();
|
|
43
|
+
if (token.startsWith("Bearer ") || /^fig[ou]/.test(token)) {
|
|
44
|
+
return { Authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}` };
|
|
45
|
+
}
|
|
46
|
+
return { "X-Figma-Token": token };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildUrl(path: string, params?: Record<string, unknown>): string {
|
|
50
|
+
const url = new URL(`${BASE}${path}`);
|
|
51
|
+
if (params) {
|
|
52
|
+
for (const [k, v] of Object.entries(params)) {
|
|
53
|
+
if (v === undefined || v === null || v === "") continue;
|
|
54
|
+
url.searchParams.set(k, String(v));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function request(method: string, path: string, params?: Record<string, unknown>, body?: unknown): Promise<Response> {
|
|
61
|
+
const init: RequestInit = { method, headers: { ...authHeaders() } };
|
|
62
|
+
if (body !== undefined) {
|
|
63
|
+
(init.headers as Record<string, string>)["Content-Type"] = "application/json";
|
|
64
|
+
init.body = JSON.stringify(body);
|
|
65
|
+
}
|
|
66
|
+
return fetch(buildUrl(path, params), init);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const get = (path: string, params?: Record<string, unknown>) => request("GET", path, params);
|
|
70
|
+
export const post = (path: string, body?: unknown, params?: Record<string, unknown>) => request("POST", path, params, body ?? {});
|
|
71
|
+
export const put = (path: string, body?: unknown, params?: Record<string, unknown>) => request("PUT", path, params, body ?? {});
|
|
72
|
+
export const del = (path: string, params?: Record<string, unknown>) => request("DELETE", path, params);
|
|
73
|
+
|
|
74
|
+
/** Print a Response as pretty JSON (or raw text), exit non-zero on HTTP error. */
|
|
75
|
+
export async function output(res: Response): Promise<void> {
|
|
76
|
+
const text = await res.text();
|
|
77
|
+
let data: unknown = text;
|
|
78
|
+
try { data = JSON.parse(text); } catch { /* keep raw */ }
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
console.error(`HTTP ${res.status} ${res.statusText}`);
|
|
81
|
+
console.error(typeof data === "string" ? data : JSON.stringify(data, null, 2));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
console.log(typeof data === "string" ? data : JSON.stringify(data, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Accept a raw file key or a Figma URL and return { fileKey, nodeId? }.
|
|
89
|
+
* URLs: https://www.figma.com/file|design|board/<KEY>/<name>?node-id=1-2
|
|
90
|
+
* node-id query uses "1-2" form; the API expects "1:2".
|
|
91
|
+
*/
|
|
92
|
+
export function parseFigmaTarget(input: string): { fileKey: string; nodeId?: string } {
|
|
93
|
+
const urlMatch = input.match(/figma\.com\/(?:file|design|board|proto)\/([A-Za-z0-9]+)/);
|
|
94
|
+
const fileKey = urlMatch ? urlMatch[1] : input;
|
|
95
|
+
let nodeId: string | undefined;
|
|
96
|
+
const nodeMatch = input.match(/[?&]node-id=([^&]+)/);
|
|
97
|
+
if (nodeMatch) nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
|
|
98
|
+
return { fileKey, nodeId };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Read JSON from an inline string or a @file.json path. */
|
|
102
|
+
export function readJsonArg(arg: string): unknown {
|
|
103
|
+
const raw = arg.startsWith("@") ? readFileSync(arg.slice(1), "utf-8") : arg;
|
|
104
|
+
return JSON.parse(raw);
|
|
105
|
+
}
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Relay/bridge between the figma-api CLI and the Figma plugin.
|
|
2
|
+
// The plugin can't be addressed directly, but it can poll over HTTP — so the CLI
|
|
3
|
+
// enqueues commands here, the plugin long-polls /poll, executes them on the
|
|
4
|
+
// canvas, and posts the result back to /result.
|
|
5
|
+
//
|
|
6
|
+
// figma-api draw-* ──POST /cmd──▶ relay ◀──GET /poll── plugin
|
|
7
|
+
// ──POST /result─▶ (draws, returns ids)
|
|
8
|
+
|
|
9
|
+
interface Cmd { id: string; op: string; [k: string]: unknown }
|
|
10
|
+
|
|
11
|
+
const queue: Cmd[] = [];
|
|
12
|
+
const resultWaiters = new Map<string, (v: unknown) => void>();
|
|
13
|
+
const pollWaiters: ((c: Cmd | null) => void)[] = [];
|
|
14
|
+
let lastSeen = 0;
|
|
15
|
+
|
|
16
|
+
function json(body: unknown, status = 200): Response {
|
|
17
|
+
return new Response(JSON.stringify(body), {
|
|
18
|
+
status,
|
|
19
|
+
headers: { "content-type": "application/json", "access-control-allow-origin": "*", "access-control-allow-headers": "*" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function startBridge(port: number): void {
|
|
24
|
+
Bun.serve({
|
|
25
|
+
port,
|
|
26
|
+
async fetch(req) {
|
|
27
|
+
const url = new URL(req.url);
|
|
28
|
+
if (req.method === "OPTIONS") return json({}, 204);
|
|
29
|
+
|
|
30
|
+
// Health / status
|
|
31
|
+
if (url.pathname === "/health") {
|
|
32
|
+
return json({ ok: true, queued: queue.length, pluginSeenMsAgo: lastSeen ? Date.now() - lastSeen : null });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// CLI → enqueue a command and wait for the plugin's result
|
|
36
|
+
if (url.pathname === "/cmd" && req.method === "POST") {
|
|
37
|
+
const body = (await req.json()) as Record<string, unknown>;
|
|
38
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
39
|
+
const cmd: Cmd = { id, op: String(body.op), ...body };
|
|
40
|
+
const waiter = pollWaiters.shift();
|
|
41
|
+
if (waiter) waiter(cmd);
|
|
42
|
+
else queue.push(cmd);
|
|
43
|
+
const result = await new Promise((resolve) => {
|
|
44
|
+
resultWaiters.set(id, resolve);
|
|
45
|
+
setTimeout(() => { if (resultWaiters.delete(id)) resolve({ timeout: true }); }, 30000);
|
|
46
|
+
});
|
|
47
|
+
return json({ id, result });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Plugin → long-poll for the next command
|
|
51
|
+
if (url.pathname === "/poll") {
|
|
52
|
+
lastSeen = Date.now();
|
|
53
|
+
const next = queue.shift();
|
|
54
|
+
if (next) return json(next);
|
|
55
|
+
const cmd = await new Promise<Cmd | null>((resolve) => {
|
|
56
|
+
pollWaiters.push(resolve);
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
const i = pollWaiters.indexOf(resolve);
|
|
59
|
+
if (i >= 0) { pollWaiters.splice(i, 1); resolve(null); }
|
|
60
|
+
}, 25000);
|
|
61
|
+
});
|
|
62
|
+
return json(cmd ?? {});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Plugin → post a command result
|
|
66
|
+
if (url.pathname === "/result" && req.method === "POST") {
|
|
67
|
+
const body = (await req.json()) as { id: string; result: unknown };
|
|
68
|
+
const w = resultWaiters.get(body.id);
|
|
69
|
+
if (w) { resultWaiters.delete(body.id); w(body.result); }
|
|
70
|
+
return json({ ok: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return json({ error: "not found" }, 404);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
console.log(`figma-api bridge listening on http://localhost:${port}`);
|
|
77
|
+
console.log("Endpoints: POST /cmd GET /poll POST /result GET /health");
|
|
78
|
+
console.log("Point the plugin at this URL (or a cloudflared tunnel of it), then run draw-* commands.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** CLI helper: push a command to a running bridge and return the plugin's result. */
|
|
82
|
+
export async function sendCommand(relay: string, op: string, params: Record<string, unknown>): Promise<void> {
|
|
83
|
+
const res = await fetch(`${relay.replace(/\/$/, "")}/cmd`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ op, ...params }),
|
|
87
|
+
}).catch((e) => { console.error(`Bridge not reachable at ${relay}: ${e.message}\nStart it with: figma-api bridge`); process.exit(1); });
|
|
88
|
+
const data = (await (res as Response).json()) as { result?: { timeout?: boolean; error?: string } };
|
|
89
|
+
const result = data.result;
|
|
90
|
+
if (result?.timeout) {
|
|
91
|
+
console.error("Timed out waiting for the plugin. Is it open and Connected to the relay?");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log(JSON.stringify(result ?? data, null, 2));
|
|
95
|
+
if (result?.error) process.exit(1);
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import {
|
|
5
|
+
saveCredentials, loadCredentials,
|
|
6
|
+
get, post, put, del, output, parseFigmaTarget, readJsonArg,
|
|
7
|
+
} from "./api";
|
|
8
|
+
import { startBridge, sendCommand } from "./bridge";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_RELAY = process.env.FIGMA_RELAY || "http://localhost:8917";
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("figma-api")
|
|
14
|
+
.description(
|
|
15
|
+
"Figma REST API CLI — raw read+write wrapper for files, nodes, images, comments,\n" +
|
|
16
|
+
"components, styles, variables, dev resources, webhooks, projects and analytics.\n" +
|
|
17
|
+
"Auth: figma-api auth <personal-access-token> (or FIGMA_TOKEN env var)\n" +
|
|
18
|
+
"Token: create at https://www.figma.com/settings → Personal access tokens\n" +
|
|
19
|
+
"Most commands accept a file URL or a raw file key. node-id is read from the URL too.\n\n" +
|
|
20
|
+
"NOTE: Creating canvas layers (frames/shapes/text) is NOT in the REST API — that\n" +
|
|
21
|
+
"lives in the Figma plugin/MCP runtime. Writable here: comments, reactions,\n" +
|
|
22
|
+
"variables, dev resources and webhooks (token needs the matching write scopes)."
|
|
23
|
+
)
|
|
24
|
+
.version("1.0.0")
|
|
25
|
+
.addHelpText("after", `
|
|
26
|
+
Examples:
|
|
27
|
+
figma-api auth figd_xxx
|
|
28
|
+
figma-api me
|
|
29
|
+
figma-api file https://www.figma.com/design/AbC123/My-File
|
|
30
|
+
figma-api nodes AbC123 --ids 1:2,3:4
|
|
31
|
+
figma-api images AbC123 --ids 1:2 --format svg --scale 2
|
|
32
|
+
figma-api comments AbC123
|
|
33
|
+
figma-api comment-add AbC123 "Looks great!" --x 100 --y 200 --node 1:2
|
|
34
|
+
figma-api variables-local AbC123
|
|
35
|
+
figma-api webhook-create FILE_UPDATE https://my.app/hook --context file --context-id AbC123
|
|
36
|
+
|
|
37
|
+
Run 'figma-api <command> --help' for per-command details, parameters and examples.`);
|
|
38
|
+
|
|
39
|
+
// ── auth ───────────────────────────────────────────────────────────────────
|
|
40
|
+
program
|
|
41
|
+
.command("auth [token]")
|
|
42
|
+
.description("Save a personal access token, or show auth status")
|
|
43
|
+
.addHelpText("after", `
|
|
44
|
+
Get a token:
|
|
45
|
+
1. Open https://www.figma.com/settings
|
|
46
|
+
2. Scroll to "Personal access tokens" → Generate new token
|
|
47
|
+
3. Pick scopes: file_content:read is enough for reads. For writes also add
|
|
48
|
+
file_comments:write, file_variables:write, file_dev_resources:write,
|
|
49
|
+
webhooks:write (some scopes need a paid/Enterprise plan).
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
figma-api auth # show current status + instructions
|
|
53
|
+
figma-api auth figd_abc123 # save token to ~/.config/figma-api/credentials.json
|
|
54
|
+
|
|
55
|
+
You can also skip saving and pass the token via env:
|
|
56
|
+
FIGMA_TOKEN=figd_abc123 figma-api me`)
|
|
57
|
+
.action((token?: string) => {
|
|
58
|
+
if (token) {
|
|
59
|
+
saveCredentials({ token });
|
|
60
|
+
console.log("✅ Token saved to ~/.config/figma-api/credentials.json");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log("Figma REST API — Personal Access Token auth\n");
|
|
64
|
+
console.log("How to get a token:");
|
|
65
|
+
console.log(" 1. https://www.figma.com/settings → Personal access tokens");
|
|
66
|
+
console.log(" 2. Generate, choose scopes (reads: file_content:read; writes need more)");
|
|
67
|
+
console.log(" 3. Save it: figma-api auth <token>\n");
|
|
68
|
+
const env = process.env.FIGMA_TOKEN || process.env.FIGMA_API_TOKEN;
|
|
69
|
+
const creds = loadCredentials();
|
|
70
|
+
if (env) console.log(`Current status:\n ✅ FIGMA_TOKEN env set (${env.slice(0, 8)}...)`);
|
|
71
|
+
else if (creds?.token) console.log(`Current status:\n ✅ Saved token ${creds.token.slice(0, 8)}...`);
|
|
72
|
+
else console.log("Current status:\n ❌ No token saved and no FIGMA_TOKEN env var");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── user ─────────────────────────────────────────────────────────────────────
|
|
76
|
+
program
|
|
77
|
+
.command("me")
|
|
78
|
+
.description("Get the authenticated user (whoami)")
|
|
79
|
+
.addHelpText("after", `\nExample:\n figma-api me`)
|
|
80
|
+
.action(async () => output(await get("/v1/me")));
|
|
81
|
+
|
|
82
|
+
// ── files ─────────────────────────────────────────────────────────────────────
|
|
83
|
+
program
|
|
84
|
+
.command("file <key-or-url>")
|
|
85
|
+
.description("Get the full document JSON for a file")
|
|
86
|
+
.option("--ids <ids>", "comma-separated node IDs to limit the returned tree (e.g. 1:2,3:4)")
|
|
87
|
+
.option("--depth <n>", "how deep to traverse the document tree (1 = pages only)")
|
|
88
|
+
.option("--version <id>", "a specific version ID (default: current)")
|
|
89
|
+
.option("--geometry <mode>", "set to 'paths' to include vector geometry")
|
|
90
|
+
.option("--branch-data", "include branch metadata")
|
|
91
|
+
.addHelpText("after", `
|
|
92
|
+
Examples:
|
|
93
|
+
figma-api file https://www.figma.com/design/AbC123/My-File
|
|
94
|
+
figma-api file AbC123 --depth 2
|
|
95
|
+
figma-api file AbC123 --ids 1:2,3:4 --geometry paths
|
|
96
|
+
|
|
97
|
+
Tip: pipe to jq, e.g. figma-api file AbC123 --depth 1 | jq '.document.children[].name'`)
|
|
98
|
+
.action(async (input: string, o: any) => {
|
|
99
|
+
const { fileKey, nodeId } = parseFigmaTarget(input);
|
|
100
|
+
const ids = o.ids ?? nodeId;
|
|
101
|
+
await output(await get(`/v1/files/${fileKey}`, {
|
|
102
|
+
ids, depth: o.depth, version: o.version, geometry: o.geometry,
|
|
103
|
+
branch_data: o.branchData ? true : undefined,
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command("nodes <key-or-url>")
|
|
109
|
+
.description("Get document JSON for specific nodes only (lighter than 'file')")
|
|
110
|
+
.option("--ids <ids>", "comma-separated node IDs (required unless node-id is in the URL)")
|
|
111
|
+
.option("--depth <n>", "traversal depth within each node")
|
|
112
|
+
.option("--version <id>", "a specific version ID")
|
|
113
|
+
.option("--geometry <mode>", "set to 'paths' to include vector geometry")
|
|
114
|
+
.addHelpText("after", `
|
|
115
|
+
Examples:
|
|
116
|
+
figma-api nodes AbC123 --ids 1:2,3:4
|
|
117
|
+
figma-api nodes "https://www.figma.com/design/AbC123/x?node-id=1-2" # ids from URL`)
|
|
118
|
+
.action(async (input: string, o: any) => {
|
|
119
|
+
const { fileKey, nodeId } = parseFigmaTarget(input);
|
|
120
|
+
const ids = o.ids ?? nodeId;
|
|
121
|
+
if (!ids) { console.error("Provide --ids or a URL containing node-id"); process.exit(1); }
|
|
122
|
+
await output(await get(`/v1/files/${fileKey}/nodes`, {
|
|
123
|
+
ids, depth: o.depth, version: o.version, geometry: o.geometry,
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command("file-meta <key-or-url>")
|
|
129
|
+
.description("Get lightweight file metadata (name, last touched, editor type, thumbnail)")
|
|
130
|
+
.addHelpText("after", `\nExample:\n figma-api file-meta AbC123`)
|
|
131
|
+
.action(async (input: string) => {
|
|
132
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
133
|
+
await output(await get(`/v1/files/${fileKey}/meta`));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command("versions <key-or-url>")
|
|
138
|
+
.description("List the version history of a file")
|
|
139
|
+
.addHelpText("after", `\nExample:\n figma-api versions AbC123`)
|
|
140
|
+
.action(async (input: string) => {
|
|
141
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
142
|
+
await output(await get(`/v1/files/${fileKey}/versions`));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── images ────────────────────────────────────────────────────────────────────
|
|
146
|
+
program
|
|
147
|
+
.command("images <key-or-url>")
|
|
148
|
+
.description("Render nodes to images and return their URLs")
|
|
149
|
+
.option("--ids <ids>", "comma-separated node IDs to render (or from URL node-id)")
|
|
150
|
+
.option("--format <fmt>", "jpg | png | svg | pdf", "png")
|
|
151
|
+
.option("--scale <n>", "scale factor 0.01–4 (raster only)")
|
|
152
|
+
.option("--version <id>", "a specific version ID")
|
|
153
|
+
.option("--svg-include-id", "include id attributes in SVG output")
|
|
154
|
+
.option("--svg-include-node-id", "include node id attributes in SVG output")
|
|
155
|
+
.option("--no-svg-outline-text", "keep SVG text as <text> instead of vector outlines")
|
|
156
|
+
.option("--no-svg-simplify-stroke", "do not simplify inside/outside strokes in SVG")
|
|
157
|
+
.option("--no-contents-only", "include content that overlaps the rendered node")
|
|
158
|
+
.option("--use-absolute-bounds", "render full node dimensions ignoring clipping")
|
|
159
|
+
.addHelpText("after", `
|
|
160
|
+
Returns a JSON map of node-id → rendered image URL (valid for a short time).
|
|
161
|
+
Examples:
|
|
162
|
+
figma-api images AbC123 --ids 1:2 --format svg
|
|
163
|
+
figma-api images AbC123 --ids 1:2,3:4 --format png --scale 2`)
|
|
164
|
+
.action(async (input: string, o: any) => {
|
|
165
|
+
const { fileKey, nodeId } = parseFigmaTarget(input);
|
|
166
|
+
const ids = o.ids ?? nodeId;
|
|
167
|
+
if (!ids) { console.error("Provide --ids or a URL containing node-id"); process.exit(1); }
|
|
168
|
+
await output(await get(`/v1/images/${fileKey}`, {
|
|
169
|
+
ids, format: o.format, scale: o.scale, version: o.version,
|
|
170
|
+
svg_include_id: o.svgIncludeId ? true : undefined,
|
|
171
|
+
svg_include_node_id: o.svgIncludeNodeId ? true : undefined,
|
|
172
|
+
svg_outline_text: o.svgOutlineText === false ? false : undefined,
|
|
173
|
+
svg_simplify_stroke: o.svgSimplifyStroke === false ? false : undefined,
|
|
174
|
+
contents_only: o.contentsOnly === false ? false : undefined,
|
|
175
|
+
use_absolute_bounds: o.useAbsoluteBounds ? true : undefined,
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
program
|
|
180
|
+
.command("image-fills <key-or-url>")
|
|
181
|
+
.description("Get download URLs for all images placed as fills in a file")
|
|
182
|
+
.addHelpText("after", `\nExample:\n figma-api image-fills AbC123`)
|
|
183
|
+
.action(async (input: string) => {
|
|
184
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
185
|
+
await output(await get(`/v1/files/${fileKey}/images`));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── comments (read + write) ────────────────────────────────────────────────────
|
|
189
|
+
program
|
|
190
|
+
.command("comments <key-or-url>")
|
|
191
|
+
.description("List comments in a file")
|
|
192
|
+
.option("--as-md", "return comment bodies as Markdown")
|
|
193
|
+
.addHelpText("after", `\nExample:\n figma-api comments AbC123`)
|
|
194
|
+
.action(async (input: string, o: any) => {
|
|
195
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
196
|
+
await output(await get(`/v1/files/${fileKey}/comments`, { as_md: o.asMd ? true : undefined }));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command("comment-add <key-or-url> <message>")
|
|
201
|
+
.description("Add a comment to a file (write)")
|
|
202
|
+
.option("--x <px>", "pin x coordinate on the canvas")
|
|
203
|
+
.option("--y <px>", "pin y coordinate on the canvas")
|
|
204
|
+
.option("--node <id>", "anchor the pin to a node id (with optional --x/--y offset)")
|
|
205
|
+
.option("--reply-to <comment-id>", "reply to an existing comment instead of a new pin")
|
|
206
|
+
.addHelpText("after", `
|
|
207
|
+
Pin placement: use --x/--y for an absolute canvas pin, optionally --node to anchor
|
|
208
|
+
to a node, or --reply-to to thread under an existing comment.
|
|
209
|
+
Examples:
|
|
210
|
+
figma-api comment-add AbC123 "Tighten this spacing" --x 120 --y 240
|
|
211
|
+
figma-api comment-add AbC123 "On this frame" --node 1:2
|
|
212
|
+
figma-api comment-add AbC123 "Agreed" --reply-to 99887766`)
|
|
213
|
+
.action(async (input: string, message: string, o: any) => {
|
|
214
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
215
|
+
const body: Record<string, unknown> = { message };
|
|
216
|
+
if (o.replyTo) body.comment_id = o.replyTo;
|
|
217
|
+
else if (o.node) {
|
|
218
|
+
body.client_meta = { node_id: o.node, node_offset: { x: Number(o.x ?? 0), y: Number(o.y ?? 0) } };
|
|
219
|
+
} else if (o.x !== undefined || o.y !== undefined) {
|
|
220
|
+
if (o.x === undefined || o.y === undefined) {
|
|
221
|
+
console.error("A canvas pin needs both --x and --y (or use --node).");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
body.client_meta = { x: Number(o.x), y: Number(o.y) };
|
|
225
|
+
}
|
|
226
|
+
await output(await post(`/v1/files/${fileKey}/comments`, body));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
program
|
|
230
|
+
.command("comment-delete <key-or-url> <comment-id>")
|
|
231
|
+
.description("Delete a comment (write)")
|
|
232
|
+
.addHelpText("after", `\nExample:\n figma-api comment-delete AbC123 99887766`)
|
|
233
|
+
.action(async (input: string, commentId: string) => {
|
|
234
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
235
|
+
await output(await del(`/v1/files/${fileKey}/comments/${commentId}`));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
program
|
|
239
|
+
.command("reactions <key-or-url> <comment-id>")
|
|
240
|
+
.description("List reactions on a comment")
|
|
241
|
+
.option("--cursor <c>", "pagination cursor")
|
|
242
|
+
.addHelpText("after", `\nExample:\n figma-api reactions AbC123 99887766`)
|
|
243
|
+
.action(async (input: string, commentId: string, o: any) => {
|
|
244
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
245
|
+
await output(await get(`/v1/files/${fileKey}/comments/${commentId}/reactions`, { cursor: o.cursor }));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
program
|
|
249
|
+
.command("reaction-add <key-or-url> <comment-id> <emoji>")
|
|
250
|
+
.description("Add an emoji reaction to a comment (write)")
|
|
251
|
+
.addHelpText("after", `
|
|
252
|
+
Emoji must be a supported shortcode, e.g. :eyes:, :heart_eyes:, :+1:.
|
|
253
|
+
Example:
|
|
254
|
+
figma-api reaction-add AbC123 99887766 :eyes:`)
|
|
255
|
+
.action(async (input: string, commentId: string, emoji: string) => {
|
|
256
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
257
|
+
await output(await post(`/v1/files/${fileKey}/comments/${commentId}/reactions`, { emoji }));
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
program
|
|
261
|
+
.command("reaction-delete <key-or-url> <comment-id> <emoji>")
|
|
262
|
+
.description("Remove an emoji reaction from a comment (write)")
|
|
263
|
+
.addHelpText("after", `\nExample:\n figma-api reaction-delete AbC123 99887766 :eyes:`)
|
|
264
|
+
.action(async (input: string, commentId: string, emoji: string) => {
|
|
265
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
266
|
+
await output(await del(`/v1/files/${fileKey}/comments/${commentId}/reactions`, { emoji }));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── projects ────────────────────────────────────────────────────────────────
|
|
270
|
+
program
|
|
271
|
+
.command("projects <team-id>")
|
|
272
|
+
.description("List projects in a team")
|
|
273
|
+
.addHelpText("after", `
|
|
274
|
+
team-id is in the team URL: figma.com/files/team/<TEAM_ID>/...
|
|
275
|
+
Example:
|
|
276
|
+
figma-api projects 1101234567890`)
|
|
277
|
+
.action(async (teamId: string) => output(await get(`/v1/teams/${teamId}/projects`)));
|
|
278
|
+
|
|
279
|
+
program
|
|
280
|
+
.command("project-files <project-id>")
|
|
281
|
+
.description("List files in a project")
|
|
282
|
+
.option("--branch-data", "include branch metadata for each file")
|
|
283
|
+
.addHelpText("after", `\nExample:\n figma-api project-files 55512345`)
|
|
284
|
+
.action(async (projectId: string, o: any) =>
|
|
285
|
+
output(await get(`/v1/projects/${projectId}/files`, { branch_data: o.branchData ? true : undefined })));
|
|
286
|
+
|
|
287
|
+
// ── components & styles ───────────────────────────────────────────────────────
|
|
288
|
+
program
|
|
289
|
+
.command("components <key-or-url>")
|
|
290
|
+
.description("List published components in a file")
|
|
291
|
+
.addHelpText("after", `\nExample:\n figma-api components AbC123`)
|
|
292
|
+
.action(async (input: string) => {
|
|
293
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
294
|
+
await output(await get(`/v1/files/${fileKey}/components`));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
program
|
|
298
|
+
.command("team-components <team-id>")
|
|
299
|
+
.description("List published components in a team library (paginated)")
|
|
300
|
+
.option("--page-size <n>", "results per page")
|
|
301
|
+
.option("--after <cursor>", "pagination cursor")
|
|
302
|
+
.addHelpText("after", `\nExample:\n figma-api team-components 1101234567890 --page-size 50`)
|
|
303
|
+
.action(async (teamId: string, o: any) =>
|
|
304
|
+
output(await get(`/v1/teams/${teamId}/components`, { page_size: o.pageSize, after: o.after })));
|
|
305
|
+
|
|
306
|
+
program
|
|
307
|
+
.command("component <key>")
|
|
308
|
+
.description("Get a single published component by its key")
|
|
309
|
+
.addHelpText("after", `\nExample:\n figma-api component 8f2c...`)
|
|
310
|
+
.action(async (key: string) => output(await get(`/v1/components/${key}`)));
|
|
311
|
+
|
|
312
|
+
program
|
|
313
|
+
.command("component-sets <key-or-url>")
|
|
314
|
+
.description("List published component sets (variants) in a file")
|
|
315
|
+
.addHelpText("after", `\nExample:\n figma-api component-sets AbC123`)
|
|
316
|
+
.action(async (input: string) => {
|
|
317
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
318
|
+
await output(await get(`/v1/files/${fileKey}/component_sets`));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
program
|
|
322
|
+
.command("team-component-sets <team-id>")
|
|
323
|
+
.description("List published component sets in a team library (paginated)")
|
|
324
|
+
.option("--page-size <n>", "results per page")
|
|
325
|
+
.option("--after <cursor>", "pagination cursor")
|
|
326
|
+
.addHelpText("after", `\nExample:\n figma-api team-component-sets 1101234567890`)
|
|
327
|
+
.action(async (teamId: string, o: any) =>
|
|
328
|
+
output(await get(`/v1/teams/${teamId}/component_sets`, { page_size: o.pageSize, after: o.after })));
|
|
329
|
+
|
|
330
|
+
program
|
|
331
|
+
.command("styles <key-or-url>")
|
|
332
|
+
.description("List published styles in a file")
|
|
333
|
+
.addHelpText("after", `\nExample:\n figma-api styles AbC123`)
|
|
334
|
+
.action(async (input: string) => {
|
|
335
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
336
|
+
await output(await get(`/v1/files/${fileKey}/styles`));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
program
|
|
340
|
+
.command("team-styles <team-id>")
|
|
341
|
+
.description("List published styles in a team library (paginated)")
|
|
342
|
+
.option("--page-size <n>", "results per page")
|
|
343
|
+
.option("--after <cursor>", "pagination cursor")
|
|
344
|
+
.addHelpText("after", `\nExample:\n figma-api team-styles 1101234567890`)
|
|
345
|
+
.action(async (teamId: string, o: any) =>
|
|
346
|
+
output(await get(`/v1/teams/${teamId}/styles`, { page_size: o.pageSize, after: o.after })));
|
|
347
|
+
|
|
348
|
+
program
|
|
349
|
+
.command("style <key>")
|
|
350
|
+
.description("Get a single published style by its key")
|
|
351
|
+
.addHelpText("after", `\nExample:\n figma-api style 1a2b...`)
|
|
352
|
+
.action(async (key: string) => output(await get(`/v1/styles/${key}`)));
|
|
353
|
+
|
|
354
|
+
// ── variables (read + write, Enterprise) ──────────────────────────────────────
|
|
355
|
+
program
|
|
356
|
+
.command("variables-local <key-or-url>")
|
|
357
|
+
.description("Get local variables and collections in a file (Enterprise)")
|
|
358
|
+
.addHelpText("after", `
|
|
359
|
+
Requires file_variables:read scope and an Enterprise plan.
|
|
360
|
+
Example:
|
|
361
|
+
figma-api variables-local AbC123`)
|
|
362
|
+
.action(async (input: string) => {
|
|
363
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
364
|
+
await output(await get(`/v1/files/${fileKey}/variables/local`));
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
program
|
|
368
|
+
.command("variables-published <key-or-url>")
|
|
369
|
+
.description("Get published variables from a library file (Enterprise)")
|
|
370
|
+
.addHelpText("after", `\nExample:\n figma-api variables-published AbC123`)
|
|
371
|
+
.action(async (input: string) => {
|
|
372
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
373
|
+
await output(await get(`/v1/files/${fileKey}/variables/published`));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
program
|
|
377
|
+
.command("variables-modify <key-or-url> <json>")
|
|
378
|
+
.description("Create / update / delete variables and collections in bulk (write, Enterprise)")
|
|
379
|
+
.addHelpText("after", `
|
|
380
|
+
Requires file_variables:write scope + Enterprise. <json> is a payload object (or
|
|
381
|
+
@file.json) with any of: variableCollections, variableModes, variables,
|
|
382
|
+
variableModeValues. Each entry has an "action" of CREATE | UPDATE | DELETE.
|
|
383
|
+
|
|
384
|
+
Example (inline):
|
|
385
|
+
figma-api variables-modify AbC123 '{"variableCollections":[{"action":"CREATE","name":"Tokens"}]}'
|
|
386
|
+
Example (from file):
|
|
387
|
+
figma-api variables-modify AbC123 @payload.json
|
|
388
|
+
|
|
389
|
+
Docs: https://www.figma.com/developers/api#post-variables-endpoint`)
|
|
390
|
+
.action(async (input: string, json: string) => {
|
|
391
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
392
|
+
await output(await post(`/v1/files/${fileKey}/variables`, readJsonArg(json)));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ── dev resources (read + write) ──────────────────────────────────────────────
|
|
396
|
+
program
|
|
397
|
+
.command("dev-resources <key-or-url>")
|
|
398
|
+
.description("List dev resources (links) attached to nodes in a file")
|
|
399
|
+
.option("--node-ids <ids>", "comma-separated node IDs to filter by")
|
|
400
|
+
.addHelpText("after", `\nExample:\n figma-api dev-resources AbC123 --node-ids 1:2,3:4`)
|
|
401
|
+
.action(async (input: string, o: any) => {
|
|
402
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
403
|
+
await output(await get(`/v1/files/${fileKey}/dev_resources`, { node_ids: o.nodeIds }));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
program
|
|
407
|
+
.command("dev-resource-add <key-or-url> <name> <url>")
|
|
408
|
+
.description("Attach a dev resource link to a node (write)")
|
|
409
|
+
.requiredOption("--node <id>", "node id to attach the link to")
|
|
410
|
+
.addHelpText("after", `
|
|
411
|
+
Example:
|
|
412
|
+
figma-api dev-resource-add AbC123 "PR #42" https://github.com/org/repo/pull/42 --node 1:2
|
|
413
|
+
|
|
414
|
+
Bulk: use 'dev-resources-bulk-add' with a JSON array for multiple at once.`)
|
|
415
|
+
.action(async (input: string, name: string, url: string, o: any) => {
|
|
416
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
417
|
+
const body = { dev_resources: [{ name, url, file_key: fileKey, node_id: o.node }] };
|
|
418
|
+
await output(await post(`/v1/dev_resources`, body));
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
program
|
|
422
|
+
.command("dev-resources-bulk-add <json>")
|
|
423
|
+
.description("Create multiple dev resources from a JSON array (write)")
|
|
424
|
+
.addHelpText("after", `
|
|
425
|
+
<json> (or @file.json) is an array of { name, url, file_key, node_id }.
|
|
426
|
+
Example:
|
|
427
|
+
figma-api dev-resources-bulk-add '[{"name":"Docs","url":"https://x","file_key":"AbC123","node_id":"1:2"}]'`)
|
|
428
|
+
.action(async (json: string) => {
|
|
429
|
+
const arr = readJsonArg(json);
|
|
430
|
+
await output(await post(`/v1/dev_resources`, { dev_resources: arr }));
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
program
|
|
434
|
+
.command("dev-resources-update <json>")
|
|
435
|
+
.description("Update existing dev resources by id from a JSON array (write)")
|
|
436
|
+
.addHelpText("after", `
|
|
437
|
+
<json> (or @file.json) is an array of { id, name?, url? }.
|
|
438
|
+
Example:
|
|
439
|
+
figma-api dev-resources-update '[{"id":"abc","name":"Renamed"}]'`)
|
|
440
|
+
.action(async (json: string) => {
|
|
441
|
+
const arr = readJsonArg(json);
|
|
442
|
+
await output(await put(`/v1/dev_resources`, { dev_resources: arr }));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
program
|
|
446
|
+
.command("dev-resource-delete <key-or-url> <dev-resource-id>")
|
|
447
|
+
.description("Delete a dev resource from a file (write)")
|
|
448
|
+
.addHelpText("after", `\nExample:\n figma-api dev-resource-delete AbC123 abc-id`)
|
|
449
|
+
.action(async (input: string, id: string) => {
|
|
450
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
451
|
+
await output(await del(`/v1/files/${fileKey}/dev_resources/${id}`));
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ── webhooks v2 (read + write) ─────────────────────────────────────────────────
|
|
455
|
+
program
|
|
456
|
+
.command("webhooks")
|
|
457
|
+
.description("List webhooks (by context or for the whole plan)")
|
|
458
|
+
.option("--context <type>", "team | project | file")
|
|
459
|
+
.option("--context-id <id>", "id matching the context type")
|
|
460
|
+
.option("--plan-api-id <id>", "list all webhooks for a plan")
|
|
461
|
+
.option("--cursor <c>", "pagination cursor")
|
|
462
|
+
.addHelpText("after", `
|
|
463
|
+
Examples:
|
|
464
|
+
figma-api webhooks --context file --context-id AbC123
|
|
465
|
+
figma-api webhooks --context team --context-id 1101234567890`)
|
|
466
|
+
.action(async (o: any) =>
|
|
467
|
+
output(await get(`/v2/webhooks`, { context: o.context, context_id: o.contextId, plan_api_id: o.planApiId, cursor: o.cursor })));
|
|
468
|
+
|
|
469
|
+
program
|
|
470
|
+
.command("webhook <webhook-id>")
|
|
471
|
+
.description("Get a single webhook by id")
|
|
472
|
+
.addHelpText("after", `\nExample:\n figma-api webhook 1234567`)
|
|
473
|
+
.action(async (id: string) => output(await get(`/v2/webhooks/${id}`)));
|
|
474
|
+
|
|
475
|
+
program
|
|
476
|
+
.command("webhook-create <event> <endpoint>")
|
|
477
|
+
.description("Create a webhook (write)")
|
|
478
|
+
.requiredOption("--context <type>", "team | project | file")
|
|
479
|
+
.requiredOption("--context-id <id>", "id matching the context type")
|
|
480
|
+
.requiredOption("--passcode <code>", "passcode echoed back in each request for verification")
|
|
481
|
+
.option("--status <status>", "ACTIVE | PAUSED", "ACTIVE")
|
|
482
|
+
.option("--description <text>", "human description of the webhook")
|
|
483
|
+
.addHelpText("after", `
|
|
484
|
+
event: PING | FILE_UPDATE | FILE_VERSION_UPDATE | FILE_DELETE | LIBRARY_PUBLISH
|
|
485
|
+
| FILE_COMMENT | DEV_MODE_STATUS_UPDATE
|
|
486
|
+
Requires webhooks:write scope. Figma sends a PING to the endpoint on creation.
|
|
487
|
+
Example:
|
|
488
|
+
figma-api webhook-create FILE_UPDATE https://my.app/hook --context file --context-id AbC123 --passcode s3cret`)
|
|
489
|
+
.action(async (event: string, endpoint: string, o: any) => {
|
|
490
|
+
const body: Record<string, unknown> = {
|
|
491
|
+
event_type: event, endpoint, context: o.context, context_id: o.contextId,
|
|
492
|
+
passcode: o.passcode, status: o.status,
|
|
493
|
+
};
|
|
494
|
+
if (o.description) body.description = o.description;
|
|
495
|
+
await output(await post(`/v2/webhooks`, body));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
program
|
|
499
|
+
.command("webhook-update <webhook-id>")
|
|
500
|
+
.description("Update a webhook's event, endpoint, status, passcode or description (write)")
|
|
501
|
+
.option("--event <event>", "new event type")
|
|
502
|
+
.option("--endpoint <url>", "new endpoint URL")
|
|
503
|
+
.option("--passcode <code>", "new passcode")
|
|
504
|
+
.option("--status <status>", "ACTIVE | PAUSED")
|
|
505
|
+
.option("--description <text>", "new description")
|
|
506
|
+
.addHelpText("after", `\nExample:\n figma-api webhook-update 1234567 --status PAUSED`)
|
|
507
|
+
.action(async (id: string, o: any) => {
|
|
508
|
+
const body: Record<string, unknown> = {};
|
|
509
|
+
if (o.event) body.event_type = o.event;
|
|
510
|
+
if (o.endpoint) body.endpoint = o.endpoint;
|
|
511
|
+
if (o.passcode) body.passcode = o.passcode;
|
|
512
|
+
if (o.status) body.status = o.status;
|
|
513
|
+
if (o.description) body.description = o.description;
|
|
514
|
+
await output(await put(`/v2/webhooks/${id}`, body));
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
program
|
|
518
|
+
.command("webhook-delete <webhook-id>")
|
|
519
|
+
.description("Delete a webhook (write)")
|
|
520
|
+
.addHelpText("after", `\nExample:\n figma-api webhook-delete 1234567`)
|
|
521
|
+
.action(async (id: string) => output(await del(`/v2/webhooks/${id}`)));
|
|
522
|
+
|
|
523
|
+
program
|
|
524
|
+
.command("webhook-requests <webhook-id>")
|
|
525
|
+
.description("Get recent delivery attempts (requests + responses) for a webhook")
|
|
526
|
+
.addHelpText("after", `\nExample:\n figma-api webhook-requests 1234567`)
|
|
527
|
+
.action(async (id: string) => output(await get(`/v2/webhooks/${id}/requests`)));
|
|
528
|
+
|
|
529
|
+
// ── library analytics (Enterprise) ─────────────────────────────────────────────
|
|
530
|
+
program
|
|
531
|
+
.command("analytics <key-or-url> <asset> <kind>")
|
|
532
|
+
.description("Library analytics: asset=component|style|variable, kind=actions|usages (Enterprise)")
|
|
533
|
+
.requiredOption("--group-by <field>", "actions: component|style|variable | team ; usages: component|style|variable | file")
|
|
534
|
+
.option("--start-date <YYYY-MM-DD>", "range start (actions only)")
|
|
535
|
+
.option("--end-date <YYYY-MM-DD>", "range end (actions only)")
|
|
536
|
+
.option("--order <dir>", "asc | desc")
|
|
537
|
+
.option("--cursor <c>", "pagination cursor")
|
|
538
|
+
.addHelpText("after", `
|
|
539
|
+
Requires library_analytics:read + Enterprise.
|
|
540
|
+
Examples:
|
|
541
|
+
figma-api analytics AbC123 component usages --group-by file
|
|
542
|
+
figma-api analytics AbC123 style actions --group-by team --start-date 2024-01-01 --end-date 2024-02-01`)
|
|
543
|
+
.action(async (input: string, asset: string, kind: string, o: any) => {
|
|
544
|
+
const { fileKey } = parseFigmaTarget(input);
|
|
545
|
+
if (!["component", "style", "variable"].includes(asset)) { console.error("asset must be component|style|variable"); process.exit(1); }
|
|
546
|
+
if (!["actions", "usages"].includes(kind)) { console.error("kind must be actions|usages"); process.exit(1); }
|
|
547
|
+
await output(await get(`/v1/analytics/libraries/${fileKey}/${asset}/${kind}`, {
|
|
548
|
+
group_by: o.groupBy, start_date: o.startDate, end_date: o.endDate, order: o.order, cursor: o.cursor,
|
|
549
|
+
}));
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ── activity / payments / oembed ────────────────────────────────────────────────
|
|
553
|
+
program
|
|
554
|
+
.command("activity-logs")
|
|
555
|
+
.description("Get activity logs for an organization (Enterprise, org token)")
|
|
556
|
+
.option("--events <list>", "comma-separated event types to filter")
|
|
557
|
+
.option("--start-time <unix>", "range start (epoch seconds)")
|
|
558
|
+
.option("--end-time <unix>", "range end (epoch seconds)")
|
|
559
|
+
.option("--limit <n>", "max events")
|
|
560
|
+
.option("--order <dir>", "asc | desc")
|
|
561
|
+
.addHelpText("after", `\nExample:\n figma-api activity-logs --limit 100 --order desc`)
|
|
562
|
+
.action(async (o: any) => output(await get(`/v1/activity_logs`, {
|
|
563
|
+
events: o.events, start_time: o.startTime, end_time: o.endTime, limit: o.limit, order: o.order,
|
|
564
|
+
})));
|
|
565
|
+
|
|
566
|
+
program
|
|
567
|
+
.command("payments")
|
|
568
|
+
.description("Get payment information for a plugin/widget/community-file user")
|
|
569
|
+
.option("--token <plugin_payment_token>", "plugin payment token (from plugin runtime; alternative to --user-id)")
|
|
570
|
+
.option("--user-id <id>", "the Figma user id (use with one of --plugin-id/--widget-id/--community-file-id)")
|
|
571
|
+
.option("--plugin-id <id>", "plugin resource id")
|
|
572
|
+
.option("--widget-id <id>", "widget resource id")
|
|
573
|
+
.option("--community-file-id <id>", "community file resource id")
|
|
574
|
+
.addHelpText("after", `
|
|
575
|
+
Provide either --token, or --user-id together with one of --plugin-id / --widget-id
|
|
576
|
+
/ --community-file-id.
|
|
577
|
+
Examples:
|
|
578
|
+
figma-api payments --token <plugin_payment_token>
|
|
579
|
+
figma-api payments --user-id 12345 --plugin-id 67890`)
|
|
580
|
+
.action(async (o: any) => output(await get(`/v1/payments`, {
|
|
581
|
+
plugin_payment_token: o.token, user_id: o.userId,
|
|
582
|
+
plugin_id: o.pluginId, widget_id: o.widgetId, community_file_id: o.communityFileId,
|
|
583
|
+
})));
|
|
584
|
+
|
|
585
|
+
program
|
|
586
|
+
.command("oembed <url>")
|
|
587
|
+
.description("Get oEmbed metadata for a public Figma file/prototype URL")
|
|
588
|
+
.addHelpText("after", `\nExample:\n figma-api oembed https://www.figma.com/design/AbC123/My-File`)
|
|
589
|
+
.action(async (url: string) => output(await get(`/v1/oembed`, { url })));
|
|
590
|
+
|
|
591
|
+
// ── plugin bridge (canvas writes the REST API can't do) ───────────────────────
|
|
592
|
+
program
|
|
593
|
+
.command("bridge")
|
|
594
|
+
.description("Start the relay that lets the figma-api CLI drive the Figma plugin (canvas writes)")
|
|
595
|
+
.option("--port <n>", "port to listen on", "8917")
|
|
596
|
+
.addHelpText("after", `
|
|
597
|
+
The Figma REST API cannot create canvas nodes (frames/text/shapes) or edit
|
|
598
|
+
variables off-Enterprise. This relay bridges the CLI and the companion Figma
|
|
599
|
+
plugin (see plugin/ folder), which can.
|
|
600
|
+
|
|
601
|
+
Flow:
|
|
602
|
+
1. figma-api bridge # start this relay (keep running)
|
|
603
|
+
2. In Figma desktop: Plugins → Development → Import plugin from manifest →
|
|
604
|
+
api-apps/figma-api/plugin/manifest.json, run it, Connect to the relay URL.
|
|
605
|
+
3. figma-api run '...' # CLI → relay → plugin runs it
|
|
606
|
+
|
|
607
|
+
Cross-machine: expose the relay with 'cloudflared tunnel --url http://localhost:8917'
|
|
608
|
+
and paste that https URL into the plugin's Relay field.
|
|
609
|
+
|
|
610
|
+
⚠️ Security: the relay has no auth and 'run' executes arbitrary code in your
|
|
611
|
+
Figma document. Only run it on a trusted machine/network; do not expose the
|
|
612
|
+
tunnel URL publicly or run code you don't trust.`)
|
|
613
|
+
.action((o: any) => startBridge(Number(o.port)));
|
|
614
|
+
|
|
615
|
+
program
|
|
616
|
+
.command("run <code-or-@file>")
|
|
617
|
+
.description("Run arbitrary Figma Plugin API code in the connected plugin (full canvas + variables write)")
|
|
618
|
+
.option("--relay <url>", "relay URL", DEFAULT_RELAY)
|
|
619
|
+
.addHelpText("after", `
|
|
620
|
+
Whatever the Figma Plugin API can do, this can do — create frames/text/shapes,
|
|
621
|
+
edit variables (no Enterprise paywall, unlike REST), read selection, etc.
|
|
622
|
+
\`figma\` is in scope; you may use await and \`return\` a value (nodes come back as
|
|
623
|
+
{id,type,name}).
|
|
624
|
+
|
|
625
|
+
Pass code inline or as @file.js. Requires a running bridge + Connected plugin.
|
|
626
|
+
|
|
627
|
+
⚠️ Executes arbitrary code in your Figma document — only run code you trust.
|
|
628
|
+
|
|
629
|
+
Examples:
|
|
630
|
+
figma-api run 'return figma.currentPage.selection.map(n => ({id:n.id, type:n.type, name:n.name}))'
|
|
631
|
+
figma-api run 'const t = figma.createText(); await figma.loadFontAsync({family:"Inter",style:"Regular"}); t.characters="Hi from CLI"; figma.currentPage.appendChild(t); return t'
|
|
632
|
+
figma-api run @make-card.js`)
|
|
633
|
+
.action(async (codeArg: string, o: any) => {
|
|
634
|
+
const code = codeArg.startsWith("@") ? readFileSync(codeArg.slice(1), "utf-8") : codeArg;
|
|
635
|
+
await sendCommand(o.relay, "eval", { code });
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
program
|
|
639
|
+
.command("ping")
|
|
640
|
+
.description("Ping the connected plugin to verify the bridge round-trip")
|
|
641
|
+
.option("--relay <url>", "relay URL", DEFAULT_RELAY)
|
|
642
|
+
.addHelpText("after", `\nReturns the plugin's current page name and selection.\nExample:\n figma-api ping`)
|
|
643
|
+
.action(async (o: any) => sendCommand(o.relay, "ping", {}));
|
|
644
|
+
|
|
645
|
+
program.parse();
|