@clovis500c/figma-bridge 1.12.0 → 1.13.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
@@ -11,7 +11,7 @@ for the optional comments and version history tools.)
11
11
  Works with **Claude Code**, **Claude Desktop**, **Codex**, **Antigravity**, **Gemini CLI**, **Cursor**, **Windsurf**
12
12
  and any other MCP client.
13
13
 
14
- <p align="center"><img src="docs/plugin.jpg" alt="Figma Bridge plugin" width="620"></p>
14
+ <p align="center"><img src="docs/plugin.png" alt="Figma Bridge plugin" width="620"></p>
15
15
 
16
16
  <!-- Demo: record it with docs/record-demo.md, save it as docs/demo.gif, then uncomment.
17
17
  <p align="center"><img src="docs/demo.gif" alt="An agent building, importing and exporting with Figma Bridge" width="720"></p>
@@ -68,7 +68,7 @@ Requires [Node.js](https://nodejs.org) 20+ and the Figma **desktop** app.
68
68
 
69
69
  3. Restart your AI client, open a Figma file and run **Plugins → Development → Figma Bridge**. A green **Live** badge means it's connected.
70
70
 
71
- To update the plugin later, run `npx @clovis500c/figma-bridge plugin` and reopen it in Figma.
71
+ **Updates are automatic:** each time your AI client starts, it runs the latest release and refreshes the plugin. Reopen the plugin in Figma when it shows **Update installed**.
72
72
 
73
73
  <details>
74
74
  <summary>With Bun instead of npm</summary>
@@ -182,7 +182,8 @@ Each tool describes its parameters to the agent, which needs no extra instructio
182
182
  |---|---|
183
183
  | **Offline** | The MCP server isn't running: start or restart your AI client and check that `FigmaBridge` is enabled. |
184
184
  | **Port busy** | Another program uses port 3055. Close it; the plugin reconnects. |
185
- | **Version mismatch** | The plugin and the server come from different releases: reopen the plugin, or update both. |
185
+ | **Update installed** | A newer version was installed: click **Reopen**, then run the plugin again (Ctrl+Alt+P). |
186
+ | **Older server** | Your AI client still runs an older release: restart it. |
186
187
 
187
188
  Logs, screenshots, comparison heatmaps and exported code are written to `%TEMP%\figma-bridge`.
188
189
  `import_web` uses your installed Chrome or Edge; set `FIGMA_BRIDGE_BROWSER` to use another Chromium-based browser.
package/dist/server.js CHANGED
@@ -46,8 +46,175 @@ var __esm = (fn, res, err) => () => {
46
46
  };
47
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
48
 
49
+ // src/setup.ts
50
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
51
+ import { dirname, join } from "node:path";
52
+ function clientList(home, appData) {
53
+ return [
54
+ { id: "claude-code", label: "Claude Code", files: [join(home, ".claude.json")], format: "json", extra: { type: "stdio" } },
55
+ { id: "claude-desktop", label: "Claude Desktop", files: [join(appData, "Claude", "claude_desktop_config.json")], format: "json" },
56
+ { id: "codex", label: "Codex", files: [join(home, ".codex", "config.toml")], format: "toml" },
57
+ {
58
+ id: "antigravity",
59
+ label: "Antigravity",
60
+ files: [join(home, ".gemini", "antigravity", "mcp_config.json"), join(home, ".gemini", "config", "mcp_config.json")],
61
+ format: "json"
62
+ },
63
+ { id: "gemini-cli", label: "Gemini CLI", files: [join(home, ".gemini", "settings.json")], format: "json" },
64
+ { id: "cursor", label: "Cursor", files: [join(home, ".cursor", "mcp.json")], format: "json" },
65
+ { id: "windsurf", label: "Windsurf", files: [join(home, ".codeium", "windsurf", "mcp_config.json")], format: "json" }
66
+ ];
67
+ }
68
+ function backup(path, text) {
69
+ writeFileSync(path + ".bak-figma-bridge", text);
70
+ }
71
+ function writeJson(path, entry) {
72
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
73
+ const config = text.trim() ? JSON.parse(text) : {};
74
+ if (text.trim())
75
+ backup(path, text);
76
+ config.mcpServers = { ...config.mcpServers, [SERVER_NAME]: entry };
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
79
+ `);
80
+ }
81
+ function tomlBlock(command, args) {
82
+ return [
83
+ `[mcp_servers.${SERVER_NAME}]`,
84
+ `command = '${command}'`,
85
+ `args = [${args.map((a) => `'${a}'`).join(", ")}]`,
86
+ "startup_timeout_sec = 60",
87
+ "tool_timeout_sec = 120"
88
+ ].join(`
89
+ `);
90
+ }
91
+ function writeToml(path, command, args) {
92
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
93
+ if (text.trim())
94
+ backup(path, text);
95
+ const kept = [];
96
+ let skipping = false;
97
+ for (const line of text.split(/\r?\n/)) {
98
+ const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
99
+ if (header)
100
+ skipping = header[1] === `mcp_servers.${SERVER_NAME}` || header[1].startsWith(`mcp_servers.${SERVER_NAME}.`);
101
+ if (!skipping)
102
+ kept.push(line);
103
+ }
104
+ const body = kept.join(`
105
+ `).trimEnd();
106
+ mkdirSync(dirname(path), { recursive: true });
107
+ writeFileSync(path, (body ? body + `
108
+
109
+ ` : "") + tomlBlock(command, args) + `
110
+ `);
111
+ }
112
+ function runSetup(opts) {
113
+ const all = clientList(opts.home, opts.appData);
114
+ const forced = opts.clients ?? [];
115
+ const unknown = forced.filter((id) => !all.some((c) => c.id === id));
116
+ if (unknown.length)
117
+ throw new Error(`Unknown client(s): ${unknown.join(", ")}. Known: ${all.map((c) => c.id).join(", ")}`);
118
+ const result = { updated: [], failed: [], missing: [] };
119
+ for (const client of all) {
120
+ const detected = client.files.filter((f) => existsSync(dirname(f)) && (client.id !== "gemini-cli" || existsSync(f)));
121
+ const targets = forced.length ? forced.includes(client.id) ? detected.length ? detected : [client.files[0]] : [] : detected;
122
+ if (!targets.length) {
123
+ if (!forced.length)
124
+ result.missing.push(client.label);
125
+ continue;
126
+ }
127
+ for (const file of targets) {
128
+ try {
129
+ if (client.format === "toml")
130
+ writeToml(file, opts.command, opts.args);
131
+ else
132
+ writeJson(file, { ...client.extra, command: opts.command, args: opts.args });
133
+ result.updated.push({ client: client.id, label: client.label, file });
134
+ } catch (e) {
135
+ result.failed.push({ client: client.id, label: client.label, file, error: e.message });
136
+ }
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+ function installPlugin(from, to, version) {
142
+ mkdirSync(to, { recursive: true });
143
+ for (const f of PLUGIN_FILES)
144
+ copyFileSync(join(from, f), join(to, f));
145
+ if (version)
146
+ writeFileSync(join(to, "version.json"), JSON.stringify({ version }) + `
147
+ `);
148
+ return join(to, "manifest.json");
149
+ }
150
+ function compareVersions(a, b) {
151
+ const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
152
+ const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
153
+ for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
154
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
155
+ if (d)
156
+ return d;
157
+ }
158
+ return 0;
159
+ }
160
+ function syncPlugin(from, to, version) {
161
+ if (!PLUGIN_FILES.every((f) => existsSync(join(from, f))))
162
+ return "skipped";
163
+ const had = existsSync(join(to, "manifest.json"));
164
+ let installed = "";
165
+ try {
166
+ installed = JSON.parse(readFileSync(join(to, "version.json"), "utf8")).version || "";
167
+ } catch {}
168
+ if (installed) {
169
+ const order = compareVersions(installed, version);
170
+ if (order > 0)
171
+ return "newer";
172
+ if (order === 0 && PLUGIN_FILES.every((f) => existsSync(join(to, f))))
173
+ return "current";
174
+ }
175
+ installPlugin(from, to, version);
176
+ return had ? "updated" : "installed";
177
+ }
178
+ function printSetup(r, rerun) {
179
+ for (const label of r.missing)
180
+ console.log(dim(` – ${label.padEnd(15)} not found`));
181
+ for (const u of r.updated)
182
+ console.log(green(` ✔ ${u.label.padEnd(15)}`) + dim(u.file));
183
+ for (const f of r.failed)
184
+ console.log(yellow(` ! ${f.label.padEnd(15)}could not update ${f.file}: ${f.error}`));
185
+ if (!r.updated.length)
186
+ console.log(yellow(`
187
+ No AI client found. Use \`${rerun} --client <name>\` or \`${rerun} --print\` for manual config.`));
188
+ }
189
+ function printSnippets(command, args) {
190
+ console.log(bold("JSON clients") + dim(" (Claude, Antigravity, Gemini CLI, Cursor, Windsurf…)"));
191
+ console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2));
192
+ console.log(bold(`
193
+ Codex`) + dim(" (~/.codex/config.toml)"));
194
+ console.log(tomlBlock(command, args) + `
195
+ `);
196
+ }
197
+ function parseClients(argv) {
198
+ const out = [];
199
+ argv.forEach((a, i) => {
200
+ if (a === "--client" && argv[i + 1])
201
+ out.push(...argv[i + 1].split(","));
202
+ else if (a.startsWith("--client="))
203
+ out.push(...a.slice(9).split(","));
204
+ });
205
+ return out;
206
+ }
207
+ var SERVER_NAME = "FigmaBridge", PLUGIN_FILES, color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s, bold, green, dim, yellow;
208
+ var init_setup = __esm(() => {
209
+ PLUGIN_FILES = ["manifest.json", "code.js", "ui.html"];
210
+ bold = color(1);
211
+ green = color(32);
212
+ dim = color(2);
213
+ yellow = color(33);
214
+ });
215
+
49
216
  // package.json
50
- var version = "1.12.0";
217
+ var version = "1.13.0";
51
218
  var init_package = () => {};
52
219
 
53
220
  // node_modules/zod/v3/helpers/util.js
@@ -30585,6 +30752,8 @@ function snapshot(opts) {
30585
30752
  }
30586
30753
 
30587
30754
  // src/web/browser.ts
30755
+ import { existsSync as existsSync5 } from "node:fs";
30756
+ import { join as join8 } from "node:path";
30588
30757
  import { pathToFileURL } from "node:url";
30589
30758
  async function loadPlaywright() {
30590
30759
  try {
@@ -30593,12 +30762,39 @@ async function loadPlaywright() {
30593
30762
  throw new WebImportError("import_web needs the optional package playwright-core, which is not installed. Reinstall without --no-optional, or run `npm install -g playwright-core` (it uses your installed Chrome or Edge; no browser is downloaded).", "MISSING_DEPENDENCY");
30594
30763
  }
30595
30764
  }
30765
+ function otherChromiumBrowsers() {
30766
+ const env = process.env;
30767
+ const roots = process.platform === "win32" ? [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA, "C:\\Program Files", "C:\\Program Files (x86)"].filter(Boolean) : [];
30768
+ const candidates = process.platform === "win32" ? [
30769
+ { label: "Brave", paths: roots.map((r) => join8(r, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")) },
30770
+ { label: "Chromium", paths: roots.map((r) => join8(r, "Chromium", "Application", "chrome.exe")) },
30771
+ { label: "Vivaldi", paths: roots.map((r) => join8(r, "Vivaldi", "Application", "vivaldi.exe")) },
30772
+ { label: "Opera", paths: roots.map((r) => join8(r, "Programs", "Opera", "opera.exe")) }
30773
+ ] : process.platform === "darwin" ? [
30774
+ { label: "Brave", paths: ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"] },
30775
+ { label: "Chromium", paths: ["/Applications/Chromium.app/Contents/MacOS/Chromium"] },
30776
+ { label: "Arc", paths: ["/Applications/Arc.app/Contents/MacOS/Arc"] }
30777
+ ] : [
30778
+ { label: "Brave", paths: ["/usr/bin/brave-browser", "/usr/bin/brave"] },
30779
+ { label: "Chromium", paths: ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"] }
30780
+ ];
30781
+ const found = [];
30782
+ for (const c of candidates) {
30783
+ const path = c.paths.find((p) => existsSync5(p));
30784
+ if (path)
30785
+ found.push({ label: c.label, path });
30786
+ }
30787
+ return found;
30788
+ }
30596
30789
  async function launchBrowser() {
30597
30790
  const { chromium } = await loadPlaywright();
30598
30791
  const tries = [];
30599
30792
  if (process.env.FIGMA_BRIDGE_BROWSER)
30600
30793
  tries.push({ label: process.env.FIGMA_BRIDGE_BROWSER, opts: { executablePath: process.env.FIGMA_BRIDGE_BROWSER } });
30601
- tries.push({ label: "Chrome", opts: { channel: "chrome" } }, { label: "Edge", opts: { channel: "msedge" } }, { label: "Playwright Chromium", opts: {} });
30794
+ tries.push({ label: "Chrome", opts: { channel: "chrome" } }, { label: "Edge", opts: { channel: "msedge" } });
30795
+ for (const b of otherChromiumBrowsers())
30796
+ tries.push({ label: b.label, opts: { executablePath: b.path } });
30797
+ tries.push({ label: "Playwright Chromium", opts: {} });
30602
30798
  const errors = [];
30603
30799
  for (const t of tries) {
30604
30800
  try {
@@ -30608,7 +30804,7 @@ async function launchBrowser() {
30608
30804
  `)[0]}`);
30609
30805
  }
30610
30806
  }
30611
- throw new WebImportError(`No browser found to render the page. Install Google Chrome or Microsoft Edge, or set FIGMA_BRIDGE_BROWSER to a Chromium-based browser. (${errors.join("; ")})`, "NO_BROWSER");
30807
+ throw new WebImportError(`No browser found to render the page. Install Chrome, Edge or Brave, or set FIGMA_BRIDGE_BROWSER to a Chromium-based browser. (${errors.join("; ")})`, "NO_BROWSER");
30612
30808
  }
30613
30809
  async function render(source, opts) {
30614
30810
  const given = [source.url, source.html, source.path].filter((v) => v !== undefined).length;
@@ -31647,7 +31843,7 @@ var init_import = __esm(() => {
31647
31843
  import { createHash as createHash2 } from "node:crypto";
31648
31844
  import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, rmSync, statSync as statSync2, writeFileSync as writeFileSync5 } from "node:fs";
31649
31845
  import { homedir as homedir2 } from "node:os";
31650
- import { join as join8 } from "node:path";
31846
+ import { join as join9 } from "node:path";
31651
31847
  function ensureDir() {
31652
31848
  mkdirSync5(SNIPPETS_DIR, { recursive: true });
31653
31849
  }
@@ -31664,14 +31860,14 @@ function files() {
31664
31860
  }
31665
31861
  function listSnippets() {
31666
31862
  return files().map((f) => {
31667
- const source = readFileSync8(join8(SNIPPETS_DIR, f), "utf8");
31863
+ const source = readFileSync8(join9(SNIPPETS_DIR, f), "utf8");
31668
31864
  return { name: f.slice(0, -3), description: header2(source, "description"), usage: header2(source, "usage") };
31669
31865
  });
31670
31866
  }
31671
31867
  function getSnippet(name) {
31672
31868
  checkName(name);
31673
31869
  try {
31674
- return readFileSync8(join8(SNIPPETS_DIR, `${name}.js`), "utf8");
31870
+ return readFileSync8(join9(SNIPPETS_DIR, `${name}.js`), "utf8");
31675
31871
  } catch {
31676
31872
  throw new BridgeError(`No snippet named "${name}".`, "NOT_FOUND");
31677
31873
  }
@@ -31684,37 +31880,37 @@ function saveSnippet(name, code, description = "", usage = "") {
31684
31880
  // @usage ${usage.replace(/\n/g, " ") || `await lib.${name}(args)`}
31685
31881
  ${body}
31686
31882
  `;
31687
- const path = join8(SNIPPETS_DIR, `${name}.js`);
31883
+ const path = join9(SNIPPETS_DIR, `${name}.js`);
31688
31884
  writeFileSync5(path, text);
31689
31885
  return path;
31690
31886
  }
31691
31887
  function deleteSnippet(name) {
31692
31888
  checkName(name);
31693
- rmSync(join8(SNIPPETS_DIR, `${name}.js`), { force: true });
31889
+ rmSync(join9(SNIPPETS_DIR, `${name}.js`), { force: true });
31694
31890
  }
31695
31891
  function loadLibrary() {
31696
31892
  const list = files();
31697
- const stamp = list.map((f) => `${f}:${statSync2(join8(SNIPPETS_DIR, f)).mtimeMs}`).join("|");
31893
+ const stamp = list.map((f) => `${f}:${statSync2(join9(SNIPPETS_DIR, f)).mtimeMs}`).join("|");
31698
31894
  if (cached2 && cached2.stamp === stamp)
31699
31895
  return cached2;
31700
31896
  const lib = {};
31701
31897
  for (const f of list)
31702
- lib[f.slice(0, -3)] = readFileSync8(join8(SNIPPETS_DIR, f), "utf8");
31898
+ lib[f.slice(0, -3)] = readFileSync8(join9(SNIPPETS_DIR, f), "utf8");
31703
31899
  cached2 = { stamp, hash: stamp ? createHash2("sha1").update(stamp + JSON.stringify(lib)).digest("base64url").slice(0, 12) : "empty", lib };
31704
31900
  return cached2;
31705
31901
  }
31706
31902
  var SNIPPETS_DIR, NAME, cached2 = null;
31707
31903
  var init_snippets = __esm(() => {
31708
31904
  init_bridge();
31709
- SNIPPETS_DIR = process.env.FIGMA_BRIDGE_SNIPPETS || join8(homedir2(), ".figma-bridge", "snippets");
31905
+ SNIPPETS_DIR = process.env.FIGMA_BRIDGE_SNIPPETS || join9(homedir2(), ".figma-bridge", "snippets");
31710
31906
  NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
31711
31907
  });
31712
31908
 
31713
31909
  // src/server.ts
31714
31910
  import { appendFileSync, mkdirSync as mkdirSync6, statSync as statSync3, truncateSync, writeFileSync as writeFileSync6 } from "node:fs";
31715
31911
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
31716
- import { tmpdir } from "node:os";
31717
- import { dirname as dirname4, join as join9 } from "node:path";
31912
+ import { homedir as homedir3, tmpdir } from "node:os";
31913
+ import { dirname as dirname4, join as join10 } from "node:path";
31718
31914
  import { fileURLToPath } from "node:url";
31719
31915
  function log(line) {
31720
31916
  const text = `[${new Date().toISOString().slice(11, 23)}] ${line}`;
@@ -31918,12 +32114,13 @@ var init_server3 = __esm(async () => {
31918
32114
  init_tokens();
31919
32115
  init_tokens_export();
31920
32116
  init_import();
32117
+ init_setup();
31921
32118
  init_snippets();
31922
32119
  init_package();
31923
32120
  PORT = Number(process.env.FIGMA_BRIDGE_PORT) || 3055;
31924
32121
  CHANNEL = process.env.FIGMA_BRIDGE_CHANNEL || "default";
31925
- OUT_DIR = process.env.FIGMA_BRIDGE_OUT || join9(tmpdir(), "figma-bridge");
31926
- LOG_FILE = join9(OUT_DIR, "bridge.log");
32122
+ OUT_DIR = process.env.FIGMA_BRIDGE_OUT || join10(tmpdir(), "figma-bridge");
32123
+ LOG_FILE = join10(OUT_DIR, "bridge.log");
31927
32124
  MAX_IMAGE_BYTES = 50 << 20;
31928
32125
  mkdirSync6(OUT_DIR, { recursive: true });
31929
32126
  try {
@@ -31932,6 +32129,16 @@ var init_server3 = __esm(async () => {
31932
32129
  } catch {}
31933
32130
  bridge = new Bridge({ port: PORT, channel: CHANNEL, version, log });
31934
32131
  bridge.start();
32132
+ if (process.env.FIGMA_BRIDGE_PLUGIN_SYNC !== "0") {
32133
+ try {
32134
+ const target = process.env.FIGMA_BRIDGE_PLUGIN_DIR || join10(homedir3(), ".figma-bridge", "plugin");
32135
+ const status = syncPlugin(join10(dirname4(fileURLToPath(import.meta.url)), "..", "plugin"), target, version);
32136
+ if (status === "installed" || status === "updated")
32137
+ log(`Figma plugin ${status} to v${version} in ${target}; reopen it in Figma`);
32138
+ } catch (e) {
32139
+ log(`Could not update the Figma plugin copy: ${e.message}`);
32140
+ }
32141
+ }
31935
32142
  INSTRUCTIONS = [
31936
32143
  "Figma Bridge drives the user's Figma desktop app through a local plugin. No rate limits: iterate freely.",
31937
32144
  "Workflow:",
@@ -32076,7 +32283,7 @@ ${f.content}`).join(`
32076
32283
  const single = files.length === 1 && /\.[a-z0-9]+$/i.test(args.path);
32077
32284
  const written = [];
32078
32285
  for (const f of files) {
32079
- const target = single ? args.path : join9(args.path, f.path);
32286
+ const target = single ? args.path : join10(args.path, f.path);
32080
32287
  written.push({ format: f.format, path: target, status: await writeIfChanged(target, f.content) });
32081
32288
  }
32082
32289
  return ok({ counts, files: written, ...warnings.length ? { warnings } : {} });
@@ -32114,7 +32321,7 @@ ${f.content}`).join(`
32114
32321
  const bytes = Buffer.from(r.bytesB64, "base64");
32115
32322
  const info = imageInfo(bytes);
32116
32323
  const ext = r.format === "JPG" ? "jpg" : "png";
32117
- const path = join9(OUT_DIR, `${safeName(r.name)}-${safeName(r.nodeId)}-${Date.now()}.${ext}`);
32324
+ const path = join10(OUT_DIR, `${safeName(r.name)}-${safeName(r.nodeId)}-${Date.now()}.${ext}`);
32118
32325
  await writeFile(path, bytes);
32119
32326
  const meta = { path, nodeId: r.nodeId, name: r.name, width: info?.width, height: info?.height, bytes: bytes.length };
32120
32327
  const content = [{ type: "text", text: json(meta) }];
@@ -32141,7 +32348,7 @@ ${f.content}`).join(`
32141
32348
  const diff = await bridge.request("compare_images", { actual: shot.bytesB64, reference: Buffer.from(refBytes).toString("base64"), referenceMime: info.mime, threshold: args.threshold, sideBySide: !!args.returnImage }, 90000);
32142
32349
  const pct = (v) => Math.round(v * 1e4) / 100;
32143
32350
  const toLayer = (v, axis) => Math.round((v / shot.scale + shot.offset[axis]) * 10) / 10;
32144
- const heatmapPath = join9(OUT_DIR, `compare-${safeName(shot.name)}-${Date.now()}.png`);
32351
+ const heatmapPath = join10(OUT_DIR, `compare-${safeName(shot.name)}-${Date.now()}.png`);
32145
32352
  await writeFile(heatmapPath, Buffer.from(diff.heatmap, "base64"));
32146
32353
  const refRatio = diff.reference.width / diff.reference.height;
32147
32354
  const ratio = diff.width / diff.height;
@@ -32215,7 +32422,7 @@ Example: {"url":"https://example.com","viewports":[1440,390]}`,
32215
32422
  const built = await bridge.request("build", { spec: v.spec, images, parentId: args.parentId, x, y, select: false, label: "Web import" }, 120000);
32216
32423
  let reference;
32217
32424
  if (v.screenshot) {
32218
- reference = join9(OUT_DIR, `import-${safeName(v.title || "page")}-${v.viewport}-${Date.now()}.png`);
32425
+ reference = join10(OUT_DIR, `import-${safeName(v.title || "page")}-${v.viewport}-${Date.now()}.png`);
32219
32426
  await writeFile(reference, v.screenshot);
32220
32427
  }
32221
32428
  frames.push({ viewport: v.viewport, rootId: built.rootId, name: v.spec.name, layers: built.created, size: { width: v.width, height: v.height }, reference });
@@ -32285,7 +32492,7 @@ Links are added to existing interactions unless replace:true; clear:[ids] remove
32285
32492
  }
32286
32493
  }, ({ nodeId, projectPath, framework, styling, name, outDir, write, overwrite }) => track("export_code", `${nodeId ?? "(selection)"} ${projectPath ?? `${framework ?? "html"}/${styling ?? "css"}`}`, async () => {
32287
32494
  const r = await bridge.request("export_tree", { nodeId }, 120000);
32288
- const dir = join9(OUT_DIR, `export-${safeName(r.tree.name)}-${Date.now()}`);
32495
+ const dir = join10(OUT_DIR, `export-${safeName(r.tree.name)}-${Date.now()}`);
32289
32496
  const finalName = {};
32290
32497
  const assetBytes = {};
32291
32498
  for (const [file, a] of Object.entries(r.assets)) {
@@ -32301,8 +32508,8 @@ Links are added to existing interactions unless replace:true; clear:[ids] remove
32301
32508
  if (projectPath) {
32302
32509
  const plan = planProjectExport(projectPath, tree, assetBytes, { framework, styling, name, outDir });
32303
32510
  for (const f of plan.files) {
32304
- mkdirSync6(dirname4(join9(dir, f.path)), { recursive: true });
32305
- writeFileSync6(join9(dir, f.path), typeof f.content === "string" ? f.content : Buffer.from(f.content));
32511
+ mkdirSync6(dirname4(join10(dir, f.path)), { recursive: true });
32512
+ writeFileSync6(join10(dir, f.path), typeof f.content === "string" ? f.content : Buffer.from(f.content));
32306
32513
  }
32307
32514
  const written = write ? writePlan(projectPath, plan.files, !!overwrite) : null;
32308
32515
  const s = plan.stack;
@@ -32327,13 +32534,13 @@ ${f.content}`).join(`
32327
32534
  … truncated: the full code is in ${dir}`;
32328
32535
  return { content: [{ type: "text", text: json(meta) }, { type: "text", text }] };
32329
32536
  }
32330
- mkdirSync6(join9(dir, "assets"), { recursive: true });
32537
+ mkdirSync6(join10(dir, "assets"), { recursive: true });
32331
32538
  for (const [file, bytes] of Object.entries(assetBytes))
32332
- writeFileSync6(join9(dir, "assets", file), bytes);
32539
+ writeFileSync6(join10(dir, "assets", file), bytes);
32333
32540
  const fw = framework ?? "html";
32334
32541
  const code = generateCode(tree, { framework: fw, styling: styling ?? "css", assetPath: (f) => `assets/${f}` });
32335
32542
  for (const f of code.files)
32336
- writeFileSync6(join9(dir, f.path), f.content);
32543
+ writeFileSync6(join10(dir, f.path), f.content);
32337
32544
  warnings.push(...code.warnings);
32338
32545
  const meta = {
32339
32546
  dir,
@@ -32631,154 +32838,20 @@ ${f.content}`).join(`
32631
32838
  });
32632
32839
 
32633
32840
  // src/cli.ts
32634
- import { homedir as homedir3 } from "node:os";
32635
- import { dirname as dirname5, join as join10 } from "node:path";
32636
- import { fileURLToPath as fileURLToPath2 } from "node:url";
32637
-
32638
- // src/setup.ts
32639
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
32640
- import { dirname, join } from "node:path";
32641
- var SERVER_NAME = "FigmaBridge";
32642
- function clientList(home, appData) {
32643
- return [
32644
- { id: "claude-code", label: "Claude Code", files: [join(home, ".claude.json")], format: "json", extra: { type: "stdio" } },
32645
- { id: "claude-desktop", label: "Claude Desktop", files: [join(appData, "Claude", "claude_desktop_config.json")], format: "json" },
32646
- { id: "codex", label: "Codex", files: [join(home, ".codex", "config.toml")], format: "toml" },
32647
- {
32648
- id: "antigravity",
32649
- label: "Antigravity",
32650
- files: [join(home, ".gemini", "antigravity", "mcp_config.json"), join(home, ".gemini", "config", "mcp_config.json")],
32651
- format: "json"
32652
- },
32653
- { id: "gemini-cli", label: "Gemini CLI", files: [join(home, ".gemini", "settings.json")], format: "json" },
32654
- { id: "cursor", label: "Cursor", files: [join(home, ".cursor", "mcp.json")], format: "json" },
32655
- { id: "windsurf", label: "Windsurf", files: [join(home, ".codeium", "windsurf", "mcp_config.json")], format: "json" }
32656
- ];
32657
- }
32658
- function backup(path, text) {
32659
- writeFileSync(path + ".bak-figma-bridge", text);
32660
- }
32661
- function writeJson(path, entry) {
32662
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
32663
- const config = text.trim() ? JSON.parse(text) : {};
32664
- if (text.trim())
32665
- backup(path, text);
32666
- config.mcpServers = { ...config.mcpServers, [SERVER_NAME]: entry };
32667
- mkdirSync(dirname(path), { recursive: true });
32668
- writeFileSync(path, JSON.stringify(config, null, 2) + `
32669
- `);
32670
- }
32671
- function tomlBlock(command, args) {
32672
- return [
32673
- `[mcp_servers.${SERVER_NAME}]`,
32674
- `command = '${command}'`,
32675
- `args = [${args.map((a) => `'${a}'`).join(", ")}]`,
32676
- "startup_timeout_sec = 60",
32677
- "tool_timeout_sec = 120"
32678
- ].join(`
32679
- `);
32680
- }
32681
- function writeToml(path, command, args) {
32682
- const text = existsSync(path) ? readFileSync(path, "utf8") : "";
32683
- if (text.trim())
32684
- backup(path, text);
32685
- const kept = [];
32686
- let skipping = false;
32687
- for (const line of text.split(/\r?\n/)) {
32688
- const header = /^\s*\[([^\]]+)\]\s*$/.exec(line);
32689
- if (header)
32690
- skipping = header[1] === `mcp_servers.${SERVER_NAME}` || header[1].startsWith(`mcp_servers.${SERVER_NAME}.`);
32691
- if (!skipping)
32692
- kept.push(line);
32693
- }
32694
- const body = kept.join(`
32695
- `).trimEnd();
32696
- mkdirSync(dirname(path), { recursive: true });
32697
- writeFileSync(path, (body ? body + `
32698
-
32699
- ` : "") + tomlBlock(command, args) + `
32700
- `);
32701
- }
32702
- function runSetup(opts) {
32703
- const all = clientList(opts.home, opts.appData);
32704
- const forced = opts.clients ?? [];
32705
- const unknown = forced.filter((id) => !all.some((c) => c.id === id));
32706
- if (unknown.length)
32707
- throw new Error(`Unknown client(s): ${unknown.join(", ")}. Known: ${all.map((c) => c.id).join(", ")}`);
32708
- const result = { updated: [], failed: [], missing: [] };
32709
- for (const client of all) {
32710
- const detected = client.files.filter((f) => existsSync(dirname(f)) && (client.id !== "gemini-cli" || existsSync(f)));
32711
- const targets = forced.length ? forced.includes(client.id) ? detected.length ? detected : [client.files[0]] : [] : detected;
32712
- if (!targets.length) {
32713
- if (!forced.length)
32714
- result.missing.push(client.label);
32715
- continue;
32716
- }
32717
- for (const file of targets) {
32718
- try {
32719
- if (client.format === "toml")
32720
- writeToml(file, opts.command, opts.args);
32721
- else
32722
- writeJson(file, { ...client.extra, command: opts.command, args: opts.args });
32723
- result.updated.push({ client: client.id, label: client.label, file });
32724
- } catch (e) {
32725
- result.failed.push({ client: client.id, label: client.label, file, error: e.message });
32726
- }
32727
- }
32728
- }
32729
- return result;
32730
- }
32731
- function installPlugin(from, to) {
32732
- mkdirSync(to, { recursive: true });
32733
- for (const f of ["manifest.json", "code.js", "ui.html"])
32734
- copyFileSync(join(from, f), join(to, f));
32735
- return join(to, "manifest.json");
32736
- }
32737
- var color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
32738
- var bold = color(1);
32739
- var green = color(32);
32740
- var dim = color(2);
32741
- var yellow = color(33);
32742
- function printSetup(r, rerun) {
32743
- for (const label of r.missing)
32744
- console.log(dim(` – ${label.padEnd(15)} not found`));
32745
- for (const u of r.updated)
32746
- console.log(green(` ✔ ${u.label.padEnd(15)}`) + dim(u.file));
32747
- for (const f of r.failed)
32748
- console.log(yellow(` ! ${f.label.padEnd(15)}could not update ${f.file}: ${f.error}`));
32749
- if (!r.updated.length)
32750
- console.log(yellow(`
32751
- No AI client found. Use \`${rerun} --client <name>\` or \`${rerun} --print\` for manual config.`));
32752
- }
32753
- function printSnippets(command, args) {
32754
- console.log(bold("JSON clients") + dim(" (Claude, Antigravity, Gemini CLI, Cursor, Windsurf…)"));
32755
- console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: { command, args } } }, null, 2));
32756
- console.log(bold(`
32757
- Codex`) + dim(" (~/.codex/config.toml)"));
32758
- console.log(tomlBlock(command, args) + `
32759
- `);
32760
- }
32761
- function parseClients(argv) {
32762
- const out = [];
32763
- argv.forEach((a, i) => {
32764
- if (a === "--client" && argv[i + 1])
32765
- out.push(...argv[i + 1].split(","));
32766
- else if (a.startsWith("--client="))
32767
- out.push(...a.slice(9).split(","));
32768
- });
32769
- return out;
32770
- }
32771
-
32772
- // src/cli.ts
32841
+ init_setup();
32773
32842
  init_package();
32843
+ import { homedir as homedir4 } from "node:os";
32844
+ import { dirname as dirname5, join as join11 } from "node:path";
32845
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
32774
32846
  var PACKAGE = "@clovis500c/figma-bridge";
32775
32847
  var [cmd, ...rest] = process.argv.slice(2);
32776
- var home = homedir3();
32777
- var pluginSource = join10(dirname5(fileURLToPath2(import.meta.url)), "..", "plugin");
32778
- var pluginTarget = join10(home, ".figma-bridge", "plugin");
32779
- var npx = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "-y", PACKAGE] } : { command: "npx", args: ["-y", PACKAGE] };
32848
+ var home = homedir4();
32849
+ var pluginSource = join11(dirname5(fileURLToPath2(import.meta.url)), "..", "plugin");
32850
+ var pluginTarget = join11(home, ".figma-bridge", "plugin");
32851
+ var spec = `${PACKAGE}@latest`;
32852
+ var npx = process.platform === "win32" ? { command: "cmd", args: ["/c", "npx", "-y", spec] } : { command: "npx", args: ["-y", spec] };
32780
32853
  function plugin() {
32781
- const manifest = installPlugin(pluginSource, pluginTarget);
32854
+ const manifest = installPlugin(pluginSource, pluginTarget, version);
32782
32855
  console.log(`${bold("Figma plugin")} copied to ${dim(pluginTarget)}`);
32783
32856
  return manifest;
32784
32857
  }
@@ -32787,7 +32860,7 @@ function next(manifest) {
32787
32860
  ${bold("Next")}
32788
32861
  1. Figma desktop → Plugins → Development → Import plugin from manifest…
32789
32862
  ${green(manifest)}
32790
- (once; after an update, run ${bold(`npx ${PACKAGE} plugin`)} again and reopen the plugin)
32863
+ (once: updates install themselves when your AI client restarts; then reopen the plugin)
32791
32864
  2. Restart your AI client, then run Plugins → Development → Figma Bridge in a Figma file.
32792
32865
  `);
32793
32866
  }
@@ -32805,7 +32878,7 @@ Figma Bridge setup
32805
32878
  break;
32806
32879
  }
32807
32880
  try {
32808
- const appData = process.env.APPDATA || join10(home, "AppData", "Roaming");
32881
+ const appData = process.env.APPDATA || join11(home, "AppData", "Roaming");
32809
32882
  printSetup(runSetup({ home, appData, command: npx.command, args: npx.args, clients: parseClients(rest) }), `npx ${PACKAGE} setup`);
32810
32883
  } catch (e) {
32811
32884
  console.log(yellow(e.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clovis500c/figma-bridge",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "type": "module",
5
5
  "description": "Local MCP bridge that lets any AI agent design in the Figma desktop app, with no rate limits.",
6
6
  "keywords": [
@@ -35,7 +35,8 @@
35
35
  "prepare": "bun run build",
36
36
  "check": "tsc -p tsconfig.json && tsc -p tsconfig.test.json && tsc -p plugin/tsconfig.json && bun run scripts/check-plugin.ts",
37
37
  "test": "bun run build && bun run test/selftest.ts",
38
- "test:unit": "bun test"
38
+ "test:unit": "bun test",
39
+ "preview": "bun run scripts/render-preview.ts"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@figma/plugin-typings": "^1.139.0",
package/plugin/code.js CHANGED
@@ -4226,7 +4226,7 @@
4226
4226
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
4227
4227
  }
4228
4228
  // package.json
4229
- var version = "1.12.0";
4229
+ var version = "1.13.0";
4230
4230
 
4231
4231
  // plugin/code.ts
4232
4232
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -4309,6 +4309,9 @@
4309
4309
  size = { width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height };
4310
4310
  applySize2();
4311
4311
  figma.clientStorage.setAsync("size", size);
4312
+ } else if (msg.t === "reopen") {
4313
+ figma.notify("Figma Bridge updated: run it again (Ctrl+Alt+P)", { timeout: 6000 });
4314
+ figma.closePlugin();
4312
4315
  } else if (msg.t === "notify")
4313
4316
  figma.notify(String(msg.text), { timeout: 2500 });
4314
4317
  else if (msg.t === "focus")
package/plugin/ui.html CHANGED
@@ -357,7 +357,8 @@
357
357
 
358
358
  <section class="banner warn" id="verBar">
359
359
  <span class="bico"><svg width="14" height="14"><use href="#i-alert"/></svg></span>
360
- <div class="main"><b>Version mismatch</b><span id="verMsg">Reopen the plugin or update.</span></div>
360
+ <div class="main"><b id="verTitle">Version mismatch</b><span id="verMsg">Reopen the plugin or update.</span></div>
361
+ <button class="mini" id="btnReopen">Reopen</button>
361
362
  </section>
362
363
 
363
364
  <section class="banner" id="waitBar">
@@ -932,10 +933,23 @@ function onProgress(msg) {
932
933
  renderBar();
933
934
  }
934
935
 
936
+ function newerThan(a, b) {
937
+ const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
938
+ for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
939
+ return false;
940
+ }
941
+
935
942
  function renderVersion() {
936
943
  const mismatch = !!(serverVersion && version && serverVersion !== version);
937
944
  document.body.classList.toggle("mismatch", mismatch);
938
- if (mismatch) $("verMsg").textContent = "Plugin v" + version + ", server v" + serverVersion + ": reopen the plugin or update.";
945
+ if (!mismatch) return;
946
+ // The server installs the matching plugin when it starts: a newer server means the update is already on disk.
947
+ const updateReady = newerThan(serverVersion, version);
948
+ $("verTitle").textContent = updateReady ? "Update installed: v" + serverVersion : "Older server: v" + serverVersion;
949
+ $("verMsg").textContent = updateReady
950
+ ? "Reopen the plugin to use it."
951
+ : "This plugin is v" + version + ". Restart your AI client to update its server.";
952
+ $("btnReopen").style.display = updateReady ? "" : "none";
939
953
  }
940
954
 
941
955
  // ─── Rendering ──────────────────────────────────────────────────────────────
@@ -1118,6 +1132,7 @@ $("btnWaitCancel").onclick = () => {
1118
1132
  const w = [...waits.values()].pop();
1119
1133
  if (w) toPlugin({ t: "cancelWait", id: w.id });
1120
1134
  };
1135
+ $("btnReopen").onclick = () => toPlugin({ t: "reopen" });
1121
1136
  $("btnClear").onclick = () => { entries.length = 0; openId = null; render(); };
1122
1137
  $("btnRetry").onclick = () => { retryDelay = 400; connect(); };
1123
1138
  $("btnCopyCmd").onclick = () => copy("Get-NetTCPConnection -LocalPort 3055");