@ttsalpha/mirra 1.2.0 → 1.3.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 +6 -4
- package/dist/index.js +182 -23
- package/dist/plugin/main.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,11 +65,13 @@ skips all of it. The command is written with the real path of the Node binary th
|
|
|
65
65
|
because a version manager puts `node` behind a path that changes with every shell and a
|
|
66
66
|
GUI client never sees it.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
It also registers the plugin with Figma Desktop by writing into Figma's own settings,
|
|
69
|
+
which only works while Figma is **closed** — that file is rewritten from memory on quit.
|
|
70
|
+
If Figma is running, or the format turns out to be one Mirra does not recognise, `init` says
|
|
71
|
+
so and falls back to the manual route: Figma Desktop → Plugins → Development → Import plugin
|
|
72
|
+
from manifest… → `~/.mirra/plugin/manifest.json`.
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
`~/.mirra/plugin/manifest.json`
|
|
72
|
-
2. **Restart the clients**, or they will not see the tools
|
|
74
|
+
Either way, **restart the clients**, or they will not see the tools.
|
|
73
75
|
|
|
74
76
|
And once per working session: open a file in Figma, run the Mirra plugin, leave its panel
|
|
75
77
|
open. Figma has no background plugins, so closing the panel ends the connection.
|
package/dist/index.js
CHANGED
|
@@ -32732,6 +32732,13 @@ function claudeCode(node2, entryPoint) {
|
|
|
32732
32732
|
};
|
|
32733
32733
|
}
|
|
32734
32734
|
|
|
32735
|
+
// src/figma-settings.ts
|
|
32736
|
+
import { execFile } from "node:child_process";
|
|
32737
|
+
import { readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
32738
|
+
import { homedir as homedir3 } from "node:os";
|
|
32739
|
+
import { dirname as dirname2, join as join3, sep as sep2 } from "node:path";
|
|
32740
|
+
import { promisify } from "node:util";
|
|
32741
|
+
|
|
32735
32742
|
// src/install.ts
|
|
32736
32743
|
import { spawn } from "node:child_process";
|
|
32737
32744
|
import { access as access2, cp, mkdir, readFile as readFile2, rm } from "node:fs/promises";
|
|
@@ -32784,6 +32791,106 @@ function reveal(path) {
|
|
|
32784
32791
|
spawn("open", ["-R", path], { stdio: "ignore", detached: true }).unref();
|
|
32785
32792
|
}
|
|
32786
32793
|
|
|
32794
|
+
// src/figma-settings.ts
|
|
32795
|
+
var FIGMA_SETTINGS = settingsPath();
|
|
32796
|
+
function settingsPath() {
|
|
32797
|
+
if (process.platform === "darwin") {
|
|
32798
|
+
return join3(homedir3(), "Library/Application Support/Figma/settings.json");
|
|
32799
|
+
}
|
|
32800
|
+
if (process.platform === "win32") {
|
|
32801
|
+
const local = process.env["LOCALAPPDATA"] ?? join3(homedir3(), "AppData/Local");
|
|
32802
|
+
return join3(local, "Figma/settings.json");
|
|
32803
|
+
}
|
|
32804
|
+
return join3(homedir3(), ".config/Figma/settings.json");
|
|
32805
|
+
}
|
|
32806
|
+
var PLUGIN_ID = "mirra-figma-bridge";
|
|
32807
|
+
async function registerPluginWithFigma(pluginDir) {
|
|
32808
|
+
if (await isFigmaRunning()) {
|
|
32809
|
+
return { status: "skipped", reason: "Figma Desktop is running; quit it first" };
|
|
32810
|
+
}
|
|
32811
|
+
let raw;
|
|
32812
|
+
try {
|
|
32813
|
+
raw = await readFile3(FIGMA_SETTINGS, "utf8");
|
|
32814
|
+
} catch {
|
|
32815
|
+
return { status: "skipped", reason: `no settings file at ${FIGMA_SETTINGS}` };
|
|
32816
|
+
}
|
|
32817
|
+
try {
|
|
32818
|
+
const settings = JSON.parse(raw);
|
|
32819
|
+
const entries = settings["localFileExtensions"];
|
|
32820
|
+
if (!Array.isArray(entries)) {
|
|
32821
|
+
return { status: "skipped", reason: "unfamiliar settings format" };
|
|
32822
|
+
}
|
|
32823
|
+
const next = rebuild(entries, pluginDir);
|
|
32824
|
+
if (!next) return { status: "already" };
|
|
32825
|
+
settings["localFileExtensions"] = next;
|
|
32826
|
+
const temporary = join3(dirname2(FIGMA_SETTINGS), `.mirra-${process.pid}.json`);
|
|
32827
|
+
await writeFile2(temporary, JSON.stringify(settings, null, 2));
|
|
32828
|
+
await rename2(temporary, FIGMA_SETTINGS);
|
|
32829
|
+
return { status: "registered" };
|
|
32830
|
+
} catch (error62) {
|
|
32831
|
+
return { status: "skipped", reason: error62 instanceof Error ? error62.message : String(error62) };
|
|
32832
|
+
}
|
|
32833
|
+
}
|
|
32834
|
+
function rebuild(entries, pluginDir) {
|
|
32835
|
+
const mine = entries.filter((entry) => isMine(entry, entries));
|
|
32836
|
+
if (isCleanTrio(mine, pluginDir)) return null;
|
|
32837
|
+
const others = entries.filter((entry) => !mine.includes(entry));
|
|
32838
|
+
return [...others, ...entriesFor(pluginDir, nextId(others))];
|
|
32839
|
+
}
|
|
32840
|
+
function isCleanTrio(mine, pluginDir) {
|
|
32841
|
+
const manifest = mine.find((entry) => entry.fileMetadata.type === "manifest");
|
|
32842
|
+
if (mine.length !== 3 || !manifest || manifest.fileMetadata.type !== "manifest") return false;
|
|
32843
|
+
if (manifest.manifestPath !== join3(pluginDir, "manifest.json")) return false;
|
|
32844
|
+
const { codeFileId, uiFileIds } = manifest.fileMetadata;
|
|
32845
|
+
const paths = new Map(mine.map((entry) => [entry.id, entry.manifestPath]));
|
|
32846
|
+
return paths.get(codeFileId) === join3(pluginDir, "main.js") && uiFileIds.length === 1 && paths.get(uiFileIds[0]) === join3(pluginDir, "ui.html");
|
|
32847
|
+
}
|
|
32848
|
+
function isMine(entry, all) {
|
|
32849
|
+
if (entry.lastKnownPluginId === PLUGIN_ID) return true;
|
|
32850
|
+
if (entry.manifestPath.startsWith(`${MIRRA_HOME}${sep2}`)) return true;
|
|
32851
|
+
const metadata = entry.fileMetadata;
|
|
32852
|
+
return metadata.type !== "manifest" && !all.some((e) => e.id === metadata.manifestFileId);
|
|
32853
|
+
}
|
|
32854
|
+
async function isFigmaRunning() {
|
|
32855
|
+
const run = promisify(execFile);
|
|
32856
|
+
try {
|
|
32857
|
+
if (process.platform === "win32") {
|
|
32858
|
+
const { stdout } = await run("tasklist", ["/fi", "imagename eq Figma.exe", "/nh"]);
|
|
32859
|
+
return stdout.includes("Figma.exe");
|
|
32860
|
+
}
|
|
32861
|
+
await run("pgrep", ["-x", "Figma"]);
|
|
32862
|
+
return true;
|
|
32863
|
+
} catch {
|
|
32864
|
+
return false;
|
|
32865
|
+
}
|
|
32866
|
+
}
|
|
32867
|
+
function nextId(entries) {
|
|
32868
|
+
return entries.reduce((highest, entry) => Math.max(highest, entry.id), 0) + 1;
|
|
32869
|
+
}
|
|
32870
|
+
function entriesFor(pluginDir, first) {
|
|
32871
|
+
const [manifestId, codeId, uiId] = [first, first + 1, first + 2];
|
|
32872
|
+
return [
|
|
32873
|
+
{
|
|
32874
|
+
id: manifestId,
|
|
32875
|
+
manifestPath: join3(pluginDir, "manifest.json"),
|
|
32876
|
+
lastKnownName: "Mirra",
|
|
32877
|
+
lastKnownPluginId: PLUGIN_ID,
|
|
32878
|
+
cachedContainsWidget: false,
|
|
32879
|
+
fileMetadata: { type: "manifest", codeFileId: codeId, uiFileIds: [uiId] }
|
|
32880
|
+
},
|
|
32881
|
+
{
|
|
32882
|
+
id: codeId,
|
|
32883
|
+
manifestPath: join3(pluginDir, "main.js"),
|
|
32884
|
+
fileMetadata: { type: "code", manifestFileId: manifestId }
|
|
32885
|
+
},
|
|
32886
|
+
{
|
|
32887
|
+
id: uiId,
|
|
32888
|
+
manifestPath: join3(pluginDir, "ui.html"),
|
|
32889
|
+
fileMetadata: { type: "ui", manifestFileId: manifestId }
|
|
32890
|
+
}
|
|
32891
|
+
];
|
|
32892
|
+
}
|
|
32893
|
+
|
|
32787
32894
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/util.js
|
|
32788
32895
|
var util;
|
|
32789
32896
|
(function(util2) {
|
|
@@ -40821,7 +40928,20 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
40821
40928
|
};
|
|
40822
40929
|
|
|
40823
40930
|
// src/version.ts
|
|
40824
|
-
var SERVER_VERSION = "1.
|
|
40931
|
+
var SERVER_VERSION = "1.3.0";
|
|
40932
|
+
|
|
40933
|
+
// src/tools/annotations.ts
|
|
40934
|
+
var READS = { readOnlyHint: true, openWorldHint: false };
|
|
40935
|
+
var ADDS = {
|
|
40936
|
+
readOnlyHint: false,
|
|
40937
|
+
destructiveHint: false,
|
|
40938
|
+
openWorldHint: false
|
|
40939
|
+
};
|
|
40940
|
+
var CHANGES = {
|
|
40941
|
+
readOnlyHint: false,
|
|
40942
|
+
destructiveHint: true,
|
|
40943
|
+
openWorldHint: false
|
|
40944
|
+
};
|
|
40825
40945
|
|
|
40826
40946
|
// src/errors.ts
|
|
40827
40947
|
var MirraError = class extends Error {
|
|
@@ -40853,7 +40973,12 @@ value keyed by mode name sets one value per mode. Both halves are optional.`;
|
|
|
40853
40973
|
function registerDefine(server, bridge) {
|
|
40854
40974
|
server.registerTool(
|
|
40855
40975
|
"mirra_define_tokens",
|
|
40856
|
-
{
|
|
40976
|
+
{
|
|
40977
|
+
annotations: CHANGES,
|
|
40978
|
+
title: "Define Figma variables",
|
|
40979
|
+
description: DESCRIPTION,
|
|
40980
|
+
inputSchema: DefineParams.shape
|
|
40981
|
+
},
|
|
40857
40982
|
async (params) => {
|
|
40858
40983
|
try {
|
|
40859
40984
|
const result = DefineResult.parse(await bridge.request("define", params));
|
|
@@ -40879,7 +41004,12 @@ The body is an async function receiving \`figma\`; use it only for what the type
|
|
|
40879
41004
|
function registerExec(server, bridge) {
|
|
40880
41005
|
server.registerTool(
|
|
40881
41006
|
"mirra_exec",
|
|
40882
|
-
{
|
|
41007
|
+
{
|
|
41008
|
+
annotations: CHANGES,
|
|
41009
|
+
title: "Run Plugin API code",
|
|
41010
|
+
description: DESCRIPTION2,
|
|
41011
|
+
inputSchema: ExecParams.shape
|
|
41012
|
+
},
|
|
40883
41013
|
async (params) => {
|
|
40884
41014
|
try {
|
|
40885
41015
|
const result = ExecResult.parse(await bridge.request("exec", params));
|
|
@@ -40892,11 +41022,11 @@ function registerExec(server, bridge) {
|
|
|
40892
41022
|
}
|
|
40893
41023
|
|
|
40894
41024
|
// src/tools/export.ts
|
|
40895
|
-
import { mkdir as mkdir2, writeFile as
|
|
41025
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
|
|
40896
41026
|
import { resolve as resolve3 } from "node:path";
|
|
40897
41027
|
|
|
40898
41028
|
// src/tools/paths.ts
|
|
40899
|
-
import { resolve as resolve2, sep as
|
|
41029
|
+
import { resolve as resolve2, sep as sep3 } from "node:path";
|
|
40900
41030
|
function rootDirectory() {
|
|
40901
41031
|
const configured = process.env["MIRRA_ROOT"];
|
|
40902
41032
|
return configured ? resolve2(configured) : process.cwd();
|
|
@@ -40905,7 +41035,7 @@ function resolveInRoot(path) {
|
|
|
40905
41035
|
const root = rootDirectory();
|
|
40906
41036
|
const full = resolve2(root, path);
|
|
40907
41037
|
if (!process.env["MIRRA_ROOT"]) return full;
|
|
40908
|
-
if (full !== root && !full.startsWith(root +
|
|
41038
|
+
if (full !== root && !full.startsWith(root + sep3)) {
|
|
40909
41039
|
throw new MirraError(
|
|
40910
41040
|
`${path} is outside MIRRA_ROOT`,
|
|
40911
41041
|
"outside_root",
|
|
@@ -40922,6 +41052,7 @@ function registerExport(server, bridge) {
|
|
|
40922
41052
|
server.registerTool(
|
|
40923
41053
|
"mirra_export_assets",
|
|
40924
41054
|
{
|
|
41055
|
+
annotations: CHANGES,
|
|
40925
41056
|
title: "Export Figma layers to files",
|
|
40926
41057
|
description: DESCRIPTION3,
|
|
40927
41058
|
inputSchema: ExportParams.shape
|
|
@@ -40934,7 +41065,7 @@ function registerExport(server, bridge) {
|
|
|
40934
41065
|
const written = [];
|
|
40935
41066
|
for (const asset2 of result.assets) {
|
|
40936
41067
|
const file2 = resolve3(directory, `${slug(asset2.name)}.${asset2.extension}`);
|
|
40937
|
-
await
|
|
41068
|
+
await writeFile3(file2, Buffer.from(asset2.data, "base64"));
|
|
40938
41069
|
written.push(file2);
|
|
40939
41070
|
}
|
|
40940
41071
|
return text([`wrote ${written.length} file(s):`, ...written]);
|
|
@@ -40957,7 +41088,12 @@ tool works on the current page, so start here when the layer you want is elsewhe
|
|
|
40957
41088
|
function registerFind(server, bridge) {
|
|
40958
41089
|
server.registerTool(
|
|
40959
41090
|
"mirra_find",
|
|
40960
|
-
{
|
|
41091
|
+
{
|
|
41092
|
+
annotations: READS,
|
|
41093
|
+
title: "Find Figma layers",
|
|
41094
|
+
description: FIND,
|
|
41095
|
+
inputSchema: FindParams.shape
|
|
41096
|
+
},
|
|
40961
41097
|
async (params) => {
|
|
40962
41098
|
try {
|
|
40963
41099
|
const result = FindResult.parse(await bridge.request("find", params));
|
|
@@ -40975,7 +41111,13 @@ function registerFind(server, bridge) {
|
|
|
40975
41111
|
);
|
|
40976
41112
|
server.registerTool(
|
|
40977
41113
|
"mirra_pages",
|
|
40978
|
-
{
|
|
41114
|
+
{
|
|
41115
|
+
// Not read-only: it can add a page and change which one is current.
|
|
41116
|
+
annotations: ADDS,
|
|
41117
|
+
title: "List, add or switch Figma pages",
|
|
41118
|
+
description: PAGES,
|
|
41119
|
+
inputSchema: PagesParams.shape
|
|
41120
|
+
},
|
|
40979
41121
|
async (params) => {
|
|
40980
41122
|
try {
|
|
40981
41123
|
const result = PagesResult.parse(await bridge.request("pages", params));
|
|
@@ -40990,7 +41132,7 @@ function registerFind(server, bridge) {
|
|
|
40990
41132
|
}
|
|
40991
41133
|
|
|
40992
41134
|
// src/tools/assets.ts
|
|
40993
|
-
import { readFile as
|
|
41135
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
40994
41136
|
import { extname } from "node:path";
|
|
40995
41137
|
var MIME = /* @__PURE__ */ new Map([
|
|
40996
41138
|
[".png", "image/png"],
|
|
@@ -41020,13 +41162,13 @@ function isPath(src) {
|
|
|
41020
41162
|
return true;
|
|
41021
41163
|
}
|
|
41022
41164
|
async function readSvg(path) {
|
|
41023
|
-
return
|
|
41165
|
+
return readFile4(resolveInRoot(path), "utf8");
|
|
41024
41166
|
}
|
|
41025
41167
|
async function readImage(path) {
|
|
41026
41168
|
const file2 = resolveInRoot(path);
|
|
41027
41169
|
const type = MIME.get(extname(file2).toLowerCase());
|
|
41028
41170
|
if (!type) throw new Error(`Unsupported image type: ${extname(file2) || file2}`);
|
|
41029
|
-
return `data:${type};base64,${(await
|
|
41171
|
+
return `data:${type};base64,${(await readFile4(file2)).toString("base64")}`;
|
|
41030
41172
|
}
|
|
41031
41173
|
|
|
41032
41174
|
// src/tools/patch.ts
|
|
@@ -41038,6 +41180,7 @@ function registerPatch(server, bridge) {
|
|
|
41038
41180
|
server.registerTool(
|
|
41039
41181
|
"mirra_patch",
|
|
41040
41182
|
{
|
|
41183
|
+
annotations: CHANGES,
|
|
41041
41184
|
title: "Patch existing Figma layers",
|
|
41042
41185
|
description: DESCRIPTION4,
|
|
41043
41186
|
inputSchema: PatchParams.shape
|
|
@@ -41062,6 +41205,7 @@ function registerPing(server, bridge) {
|
|
|
41062
41205
|
server.registerTool(
|
|
41063
41206
|
"mirra_ping",
|
|
41064
41207
|
{
|
|
41208
|
+
annotations: READS,
|
|
41065
41209
|
title: "Ping the Figma plugin",
|
|
41066
41210
|
description: "Check that the Mirra plugin is connected, and report the open file, current page and selection size."
|
|
41067
41211
|
},
|
|
@@ -41097,6 +41241,7 @@ function registerRead(server, bridge) {
|
|
|
41097
41241
|
server.registerTool(
|
|
41098
41242
|
"mirra_read",
|
|
41099
41243
|
{
|
|
41244
|
+
annotations: READS,
|
|
41100
41245
|
title: "Read Figma layers as a UI tree",
|
|
41101
41246
|
description: DESCRIPTION5,
|
|
41102
41247
|
inputSchema: ReadParams.shape
|
|
@@ -41126,6 +41271,7 @@ function registerRender(server, bridge) {
|
|
|
41126
41271
|
server.registerTool(
|
|
41127
41272
|
"mirra_render",
|
|
41128
41273
|
{
|
|
41274
|
+
annotations: ADDS,
|
|
41129
41275
|
title: "Render a UI tree into Figma",
|
|
41130
41276
|
description: DESCRIPTION6,
|
|
41131
41277
|
inputSchema: RenderParams.shape
|
|
@@ -41155,6 +41301,7 @@ function registerScreenshot(server, bridge) {
|
|
|
41155
41301
|
server.registerTool(
|
|
41156
41302
|
"mirra_screenshot",
|
|
41157
41303
|
{
|
|
41304
|
+
annotations: READS,
|
|
41158
41305
|
title: "Screenshot a Figma layer",
|
|
41159
41306
|
description: DESCRIPTION7,
|
|
41160
41307
|
inputSchema: ScreenshotParams.shape
|
|
@@ -41166,7 +41313,7 @@ function registerScreenshot(server, bridge) {
|
|
|
41166
41313
|
content: [
|
|
41167
41314
|
{
|
|
41168
41315
|
type: "text",
|
|
41169
|
-
text: `${shot.name} (${shot.width}x${shot.height}, ${shot.id})`
|
|
41316
|
+
text: `${shot.name} (${shot.width}x${shot.height}, ${shot.id})` + (shot.scale === 1 ? "" : ` at ${shot.scale.toFixed(2)}x`)
|
|
41170
41317
|
},
|
|
41171
41318
|
{ type: "image", data: shot.png, mimeType: "image/png" }
|
|
41172
41319
|
]
|
|
@@ -41185,7 +41332,7 @@ bound to the design system instead of to a hardcoded value.`;
|
|
|
41185
41332
|
function registerTokens(server, bridge) {
|
|
41186
41333
|
server.registerTool(
|
|
41187
41334
|
"mirra_tokens",
|
|
41188
|
-
{ title: "List design tokens", description: DESCRIPTION8 },
|
|
41335
|
+
{ annotations: READS, title: "List design tokens", description: DESCRIPTION8 },
|
|
41189
41336
|
async () => {
|
|
41190
41337
|
try {
|
|
41191
41338
|
const result = TokensResult.parse(await bridge.request("tokens"));
|
|
@@ -41422,7 +41569,7 @@ var USAGE = `mirra <command>
|
|
|
41422
41569
|
doctor Bind the plugin port and wait for the Figma plugin to connect
|
|
41423
41570
|
|
|
41424
41571
|
Flags:
|
|
41425
|
-
--no-reveal init: do not open the folder in Finder
|
|
41572
|
+
--no-reveal init: do not open the folder in Finder on a manual import
|
|
41426
41573
|
--no-config init: do not touch any client's configuration
|
|
41427
41574
|
|
|
41428
41575
|
Environment:
|
|
@@ -41478,14 +41625,26 @@ async function init() {
|
|
|
41478
41625
|
}
|
|
41479
41626
|
lines.push("", "A client that was already running has to be restarted to see the tools.", "");
|
|
41480
41627
|
}
|
|
41481
|
-
|
|
41482
|
-
|
|
41483
|
-
|
|
41484
|
-
|
|
41485
|
-
|
|
41486
|
-
|
|
41487
|
-
|
|
41488
|
-
|
|
41628
|
+
const figma = await registerPluginWithFigma(PLUGIN_HOME);
|
|
41629
|
+
const manual = figma.status === "skipped";
|
|
41630
|
+
if (manual) {
|
|
41631
|
+
lines.push(
|
|
41632
|
+
`Could not register the plugin with Figma Desktop: ${figma.reason}`,
|
|
41633
|
+
"Then, in Figma Desktop:",
|
|
41634
|
+
" 1. Plugins -> Development -> Import plugin from manifest...",
|
|
41635
|
+
` and choose ${manifest}`,
|
|
41636
|
+
" 2. Open a file, run Mirra, and leave the panel open",
|
|
41637
|
+
" 3. Ask the agent for mirra_ping; the panel badge should turn green"
|
|
41638
|
+
);
|
|
41639
|
+
} else {
|
|
41640
|
+
lines.push(
|
|
41641
|
+
figma.status === "registered" ? "Registered the plugin with Figma Desktop." : "Figma Desktop already knows the plugin.",
|
|
41642
|
+
"Then, in Figma Desktop:",
|
|
41643
|
+
" 1. Open a file, run Plugins -> Development -> Mirra, and leave the panel open",
|
|
41644
|
+
" 2. Ask the agent for mirra_ping; the panel badge should turn green"
|
|
41645
|
+
);
|
|
41646
|
+
}
|
|
41647
|
+
if (manual && !process.argv.includes("--no-reveal")) reveal(manifest);
|
|
41489
41648
|
console.log(lines.join("\n"));
|
|
41490
41649
|
}
|
|
41491
41650
|
async function doctor() {
|
package/dist/plugin/main.js
CHANGED