@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 +4 -3
- package/dist/server.js +244 -171
- package/package.json +3 -2
- package/plugin/code.js +4 -1
- package/plugin/ui.html +17 -2
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.
|
|
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
|
-
|
|
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
|
-
| **
|
|
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.
|
|
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" } }
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 ||
|
|
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
|
|
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 ||
|
|
31926
|
-
LOG_FILE =
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
32305
|
-
writeFileSync6(
|
|
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(
|
|
32537
|
+
mkdirSync6(join10(dir, "assets"), { recursive: true });
|
|
32331
32538
|
for (const [file, bytes] of Object.entries(assetBytes))
|
|
32332
|
-
writeFileSync6(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
32777
|
-
var pluginSource =
|
|
32778
|
-
var pluginTarget =
|
|
32779
|
-
var
|
|
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
|
|
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 ||
|
|
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.
|
|
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.
|
|
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)
|
|
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");
|