figma-relai 0.1.3 → 0.2.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/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-5YB7XMT6.js +3462 -0
- package/dist/chunk-5YB7XMT6.js.map +1 -0
- package/dist/chunk-XBWGSCRZ.js +33 -0
- package/dist/chunk-XBWGSCRZ.js.map +1 -0
- package/dist/docs-S7VJ2PPR.js +60 -0
- package/dist/docs-S7VJ2PPR.js.map +1 -0
- package/dist/doctor-ZN2PKAPH.js +138 -0
- package/dist/doctor-ZN2PKAPH.js.map +1 -0
- package/dist/index.js +85 -3098
- package/dist/index.js.map +1 -1
- package/dist/manifest-OUVKP73V.js +55 -0
- package/dist/manifest-OUVKP73V.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/state.ts
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function stateDir() {
|
|
8
|
+
return process.env.FIGMA_RELAI_STATE_DIR ?? join(homedir(), ".figma-relai");
|
|
9
|
+
}
|
|
10
|
+
function statePath() {
|
|
11
|
+
return join(stateDir(), "state.json");
|
|
12
|
+
}
|
|
13
|
+
function loadState() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(statePath(), "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function saveState(patch) {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
23
|
+
const next = { ...loadState(), ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
24
|
+
writeFileSync(statePath(), JSON.stringify(next, null, 2) + "\n");
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
loadState,
|
|
31
|
+
saveState
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=chunk-XBWGSCRZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/state.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// Persisted pairing state so the AI reconnects to the same room across\n// sessions without the designer re-pasting a join command.\n\nexport interface RelaiState {\n room?: string;\n updatedAt?: string;\n}\n\nfunction stateDir(): string {\n return process.env.FIGMA_RELAI_STATE_DIR ?? join(homedir(), \".figma-relai\");\n}\n\nfunction statePath(): string {\n return join(stateDir(), \"state.json\");\n}\n\nexport function loadState(): RelaiState {\n try {\n return JSON.parse(readFileSync(statePath(), \"utf8\")) as RelaiState;\n } catch {\n return {};\n }\n}\n\nexport function saveState(patch: Partial<RelaiState>): void {\n try {\n mkdirSync(stateDir(), { recursive: true });\n const next = { ...loadState(), ...patch, updatedAt: new Date().toISOString() };\n writeFileSync(statePath(), JSON.stringify(next, null, 2) + \"\\n\");\n } catch {\n // State persistence is best-effort; pairing still works via list_rooms\n }\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,cAAc,qBAAqB;AACvD,SAAS,eAAe;AACxB,SAAS,YAAY;AAUrB,SAAS,WAAmB;AAC1B,SAAO,QAAQ,IAAI,yBAAyB,KAAK,QAAQ,GAAG,cAAc;AAC5E;AAEA,SAAS,YAAoB;AAC3B,SAAO,KAAK,SAAS,GAAG,YAAY;AACtC;AAEO,SAAS,YAAwB;AACtC,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,UAAU,GAAG,MAAM,CAAC;AAAA,EACrD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,UAAU,OAAkC;AAC1D,MAAI;AACF,cAAU,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACzC,UAAM,OAAO,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AAC7E,kBAAc,UAAU,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,IAAI;AAAA,EACjE,QAAQ;AAAA,EAER;AACF;","names":[]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-2H7UOFLK.js";
|
|
3
|
+
|
|
4
|
+
// src/cli/docs.ts
|
|
5
|
+
function propType(p) {
|
|
6
|
+
if (p.enum) return p.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
7
|
+
const base = Array.isArray(p.type) ? p.type.join("|") : p.type ?? "any";
|
|
8
|
+
if (base === "array") {
|
|
9
|
+
const item = p.items?.enum ? p.items.enum.map((v) => JSON.stringify(v)).join(" | ") : p.items?.type ?? "any";
|
|
10
|
+
return `(${item})[]`;
|
|
11
|
+
}
|
|
12
|
+
return base;
|
|
13
|
+
}
|
|
14
|
+
function renderToolDoc(manifest, toolName) {
|
|
15
|
+
const tool = manifest.tools.find((t) => t.name === toolName);
|
|
16
|
+
if (!tool) {
|
|
17
|
+
const names = manifest.tools.map((t) => t.name).join(", ");
|
|
18
|
+
return `Unknown tool "${toolName}". Available: ${names}`;
|
|
19
|
+
}
|
|
20
|
+
const schema = tool.inputSchema;
|
|
21
|
+
const required = new Set(schema.required ?? []);
|
|
22
|
+
const lines = [
|
|
23
|
+
`# ${tool.name}`,
|
|
24
|
+
``,
|
|
25
|
+
`Category: ${tool.category}`,
|
|
26
|
+
``,
|
|
27
|
+
tool.description,
|
|
28
|
+
``
|
|
29
|
+
];
|
|
30
|
+
const props = Object.entries(schema.properties ?? {});
|
|
31
|
+
if (props.length > 0) {
|
|
32
|
+
lines.push(`## Parameters`, ``, `| name | type | required | description |`, `|---|---|---|---|`);
|
|
33
|
+
for (const [name, p] of props) {
|
|
34
|
+
const desc = (p.description ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
35
|
+
lines.push(`| ${name} | \`${propType(p)}\` | ${required.has(name) ? "yes" : "\u2014"} | ${desc} |`);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
lines.push(`No parameters.`);
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
function renderToolIndex(manifest) {
|
|
43
|
+
const lines = [`# figma-relai ${manifest.version} \u2014 ${manifest.tools.length} tools`, ``];
|
|
44
|
+
let category = "";
|
|
45
|
+
for (const tool of manifest.tools) {
|
|
46
|
+
if (tool.category !== category) {
|
|
47
|
+
category = tool.category;
|
|
48
|
+
lines.push(``, `## ${category}`, ``);
|
|
49
|
+
}
|
|
50
|
+
const first = tool.description.split(/[.:]/)[0];
|
|
51
|
+
lines.push(`- **${tool.name}** \u2014 ${first}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push(``, `Run \`figma-relai docs <tool>\` for parameters; \`figma-relai manifest\` for the full JSON contract.`);
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
renderToolDoc,
|
|
58
|
+
renderToolIndex
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=docs-S7VJ2PPR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/docs.ts"],"sourcesContent":["import type { Manifest } from \"./manifest.js\";\n\n// Renders tool docs from the same manifest the agents consume — one source,\n// two audiences (figma-relai docs [tool]).\n\ninterface SchemaProp {\n type?: string | string[];\n description?: string;\n enum?: unknown[];\n items?: { type?: string; enum?: unknown[] };\n}\n\nfunction propType(p: SchemaProp): string {\n if (p.enum) return p.enum.map((v) => JSON.stringify(v)).join(\" | \");\n const base = Array.isArray(p.type) ? p.type.join(\"|\") : (p.type ?? \"any\");\n if (base === \"array\") {\n const item = p.items?.enum\n ? p.items.enum.map((v) => JSON.stringify(v)).join(\" | \")\n : (p.items?.type ?? \"any\");\n return `(${item})[]`;\n }\n return base;\n}\n\nexport function renderToolDoc(manifest: Manifest, toolName: string): string {\n const tool = manifest.tools.find((t) => t.name === toolName);\n if (!tool) {\n const names = manifest.tools.map((t) => t.name).join(\", \");\n return `Unknown tool \"${toolName}\". Available: ${names}`;\n }\n const schema = tool.inputSchema as {\n properties?: Record<string, SchemaProp>;\n required?: string[];\n };\n const required = new Set(schema.required ?? []);\n const lines = [\n `# ${tool.name}`,\n ``,\n `Category: ${tool.category}`,\n ``,\n tool.description,\n ``,\n ];\n const props = Object.entries(schema.properties ?? {});\n if (props.length > 0) {\n lines.push(`## Parameters`, ``, `| name | type | required | description |`, `|---|---|---|---|`);\n for (const [name, p] of props) {\n const desc = (p.description ?? \"\").replace(/\\|/g, \"\\\\|\").replace(/\\n/g, \" \");\n lines.push(`| ${name} | \\`${propType(p)}\\` | ${required.has(name) ? \"yes\" : \"—\"} | ${desc} |`);\n }\n } else {\n lines.push(`No parameters.`);\n }\n return lines.join(\"\\n\");\n}\n\nexport function renderToolIndex(manifest: Manifest): string {\n const lines = [`# figma-relai ${manifest.version} — ${manifest.tools.length} tools`, ``];\n let category = \"\";\n for (const tool of manifest.tools) {\n if (tool.category !== category) {\n category = tool.category;\n lines.push(``, `## ${category}`, ``);\n }\n const first = tool.description.split(/[.:]/)[0];\n lines.push(`- **${tool.name}** — ${first}`);\n }\n lines.push(``, `Run \\`figma-relai docs <tool>\\` for parameters; \\`figma-relai manifest\\` for the full JSON contract.`);\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;AAYA,SAAS,SAAS,GAAuB;AACvC,MAAI,EAAE,KAAM,QAAO,EAAE,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK;AAClE,QAAM,OAAO,MAAM,QAAQ,EAAE,IAAI,IAAI,EAAE,KAAK,KAAK,GAAG,IAAK,EAAE,QAAQ;AACnE,MAAI,SAAS,SAAS;AACpB,UAAM,OAAO,EAAE,OAAO,OAClB,EAAE,MAAM,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,KAAK,IACpD,EAAE,OAAO,QAAQ;AACtB,WAAO,IAAI,IAAI;AAAA,EACjB;AACA,SAAO;AACT;AAEO,SAAS,cAAc,UAAoB,UAA0B;AAC1E,QAAM,OAAO,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,QAAQ;AAC3D,MAAI,CAAC,MAAM;AACT,UAAM,QAAQ,SAAS,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI;AACzD,WAAO,iBAAiB,QAAQ,iBAAiB,KAAK;AAAA,EACxD;AACA,QAAM,SAAS,KAAK;AAIpB,QAAM,WAAW,IAAI,IAAI,OAAO,YAAY,CAAC,CAAC;AAC9C,QAAM,QAAQ;AAAA,IACZ,KAAK,KAAK,IAAI;AAAA,IACd;AAAA,IACA,aAAa,KAAK,QAAQ;AAAA,IAC1B;AAAA,IACA,KAAK;AAAA,IACL;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,QAAQ,OAAO,cAAc,CAAC,CAAC;AACpD,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,KAAK,iBAAiB,IAAI,4CAA4C,mBAAmB;AAC/F,eAAW,CAAC,MAAM,CAAC,KAAK,OAAO;AAC7B,YAAM,QAAQ,EAAE,eAAe,IAAI,QAAQ,OAAO,KAAK,EAAE,QAAQ,OAAO,GAAG;AAC3E,YAAM,KAAK,KAAK,IAAI,QAAQ,SAAS,CAAC,CAAC,QAAQ,SAAS,IAAI,IAAI,IAAI,QAAQ,QAAG,MAAM,IAAI,IAAI;AAAA,IAC/F;AAAA,EACF,OAAO;AACL,UAAM,KAAK,gBAAgB;AAAA,EAC7B;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,gBAAgB,UAA4B;AAC1D,QAAM,QAAQ,CAAC,iBAAiB,SAAS,OAAO,WAAM,SAAS,MAAM,MAAM,UAAU,EAAE;AACvF,MAAI,WAAW;AACf,aAAW,QAAQ,SAAS,OAAO;AACjC,QAAI,KAAK,aAAa,UAAU;AAC9B,iBAAW,KAAK;AAChB,YAAM,KAAK,IAAI,MAAM,QAAQ,IAAI,EAAE;AAAA,IACrC;AACA,UAAM,QAAQ,KAAK,YAAY,MAAM,MAAM,EAAE,CAAC;AAC9C,UAAM,KAAK,OAAO,KAAK,IAAI,aAAQ,KAAK,EAAE;AAAA,EAC5C;AACA,QAAM,KAAK,IAAI,sGAAsG;AACrH,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadState
|
|
4
|
+
} from "./chunk-XBWGSCRZ.js";
|
|
5
|
+
import "./chunk-2H7UOFLK.js";
|
|
6
|
+
|
|
7
|
+
// src/cli/doctor.ts
|
|
8
|
+
import WebSocket from "ws";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var PORTS = [9055, 9056, 9057];
|
|
13
|
+
var PROBE_TIMEOUT_MS = 1500;
|
|
14
|
+
function probePort(port) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
17
|
+
let settled = false;
|
|
18
|
+
const done = (result) => {
|
|
19
|
+
if (!settled) {
|
|
20
|
+
settled = true;
|
|
21
|
+
try {
|
|
22
|
+
ws.close();
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
resolve(result);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const timer = setTimeout(() => done({ port, state: "foreign" }), PROBE_TIMEOUT_MS);
|
|
29
|
+
ws.on("open", () => {
|
|
30
|
+
ws.send(JSON.stringify({ type: "hello" }));
|
|
31
|
+
ws.send(JSON.stringify({ type: "list_rooms", id: "doctor" }));
|
|
32
|
+
});
|
|
33
|
+
let version;
|
|
34
|
+
ws.on("message", (raw) => {
|
|
35
|
+
try {
|
|
36
|
+
const msg = JSON.parse(raw.toString());
|
|
37
|
+
if (msg.type === "hello" && msg.server === "figma-relai") {
|
|
38
|
+
version = msg.version;
|
|
39
|
+
}
|
|
40
|
+
if (msg.type === "list_rooms_result") {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
done({ port, state: "relai", version, rooms: msg.rooms ?? [] });
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
ws.on("error", () => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
done({ port, state: "closed" });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function runDoctor() {
|
|
54
|
+
const results = [];
|
|
55
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
56
|
+
results.push(
|
|
57
|
+
nodeMajor >= 18 ? { check: "node", status: "ok", detail: `Node ${process.versions.node}` } : {
|
|
58
|
+
check: "node",
|
|
59
|
+
status: "warn",
|
|
60
|
+
detail: `Node ${process.versions.node} is below 18`,
|
|
61
|
+
fix: "Install Node 18+ (nvm install --lts)"
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
const probes = await Promise.all(PORTS.map(probePort));
|
|
65
|
+
const relay = probes.find((p) => p.state === "relai");
|
|
66
|
+
const foreign = probes.filter((p) => p.state === "foreign");
|
|
67
|
+
if (relay) {
|
|
68
|
+
results.push({
|
|
69
|
+
check: "relay",
|
|
70
|
+
status: "ok",
|
|
71
|
+
detail: `Relai relay v${relay.version ?? "?"} on port ${relay.port}`
|
|
72
|
+
});
|
|
73
|
+
const pluginRooms = (relay.rooms ?? []).filter((r) => r.hasPlugin);
|
|
74
|
+
results.push(
|
|
75
|
+
pluginRooms.length > 0 ? {
|
|
76
|
+
check: "plugin",
|
|
77
|
+
status: "ok",
|
|
78
|
+
detail: `Figma plugin online: ${pluginRooms.map((r) => `"${r.fileName ?? r.room}"`).join(", ")}`
|
|
79
|
+
} : {
|
|
80
|
+
check: "plugin",
|
|
81
|
+
status: "warn",
|
|
82
|
+
detail: "Relay is up but no Figma plugin is connected",
|
|
83
|
+
fix: "Open the Relai plugin inside your Figma file (it auto-connects)"
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
results.push({
|
|
88
|
+
check: "relay",
|
|
89
|
+
status: "warn",
|
|
90
|
+
detail: foreign.length > 0 ? `Ports ${foreign.map((p) => p.port).join(", ")} are occupied by something that is not a Relai relay` : "No relay on ports 9055\u20139057 \u2014 no MCP server is running",
|
|
91
|
+
fix: foreign.length > 0 ? "Free the port (lsof -i :9055) or let Relai fall back to 9056/9057" : "Start a conversation in your AI client with Relai registered (npx -y figma-relai)"
|
|
92
|
+
});
|
|
93
|
+
results.push({
|
|
94
|
+
check: "plugin",
|
|
95
|
+
status: "warn",
|
|
96
|
+
detail: "Cannot see the plugin without a relay"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const statePath = join(homedir(), ".figma-relai", "state.json");
|
|
100
|
+
if (existsSync(statePath)) {
|
|
101
|
+
const { room } = loadState();
|
|
102
|
+
results.push({
|
|
103
|
+
check: "state",
|
|
104
|
+
status: "ok",
|
|
105
|
+
detail: room ? `Saved room: ${room}` : "State file present (no room saved yet)"
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
results.push({
|
|
109
|
+
check: "state",
|
|
110
|
+
status: "ok",
|
|
111
|
+
detail: "No state file yet \u2014 will be created on first pairing"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
results.push(
|
|
115
|
+
process.env.FIGMA_TOKEN ? { check: "token", status: "ok", detail: "FIGMA_TOKEN set \u2014 comments unlocked" } : {
|
|
116
|
+
check: "token",
|
|
117
|
+
status: "ok",
|
|
118
|
+
detail: "FIGMA_TOKEN not set \u2014 everything works except comments",
|
|
119
|
+
fix: "Optional: add a personal access token to the MCP config env to unlock manage_comments"
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
function renderDoctor(results) {
|
|
125
|
+
const lines = ["figma-relai doctor", ""];
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
lines.push(`${r.status === "ok" ? "\u2713" : "!"} ${r.check.padEnd(7)} ${r.detail}`);
|
|
128
|
+
if (r.fix) lines.push(` \u2192 ${r.fix}`);
|
|
129
|
+
}
|
|
130
|
+
const warns = results.filter((r) => r.status === "warn").length;
|
|
131
|
+
lines.push("", warns === 0 ? "All clear." : `${warns} thing(s) to look at.`);
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
renderDoctor,
|
|
136
|
+
runDoctor
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=doctor-ZN2PKAPH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/doctor.ts"],"sourcesContent":["import WebSocket from \"ws\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { loadState } from \"../state.js\";\n\n// figma-relai doctor — environment triage in one command. Each check reports\n// ok/warn plus the one-line fix, so \"it doesn't work\" becomes actionable\n// without reading the troubleshooting docs.\n\ninterface CheckResult {\n check: string;\n status: \"ok\" | \"warn\";\n detail: string;\n fix?: string;\n}\n\nconst PORTS = [9055, 9056, 9057];\nconst PROBE_TIMEOUT_MS = 1500;\n\ninterface ProbeResult {\n port: number;\n state: \"relai\" | \"foreign\" | \"closed\";\n version?: string;\n rooms?: Array<{ room: string; hasPlugin: boolean; fileName?: string }>;\n}\n\nfunction probePort(port: number): Promise<ProbeResult> {\n return new Promise((resolve) => {\n const ws = new WebSocket(`ws://127.0.0.1:${port}`);\n let settled = false;\n const done = (result: ProbeResult) => {\n if (!settled) {\n settled = true;\n try {\n ws.close();\n } catch {\n // already closed\n }\n resolve(result);\n }\n };\n const timer = setTimeout(() => done({ port, state: \"foreign\" }), PROBE_TIMEOUT_MS);\n ws.on(\"open\", () => {\n ws.send(JSON.stringify({ type: \"hello\" }));\n ws.send(JSON.stringify({ type: \"list_rooms\", id: \"doctor\" }));\n });\n let version: string | undefined;\n ws.on(\"message\", (raw: Buffer) => {\n try {\n const msg = JSON.parse(raw.toString());\n if (msg.type === \"hello\" && msg.server === \"figma-relai\") {\n version = msg.version;\n }\n if (msg.type === \"list_rooms_result\") {\n clearTimeout(timer);\n done({ port, state: \"relai\", version, rooms: msg.rooms ?? [] });\n }\n } catch {\n // Non-JSON chatter = not our relay\n }\n });\n ws.on(\"error\", () => {\n clearTimeout(timer);\n done({ port, state: \"closed\" });\n });\n });\n}\n\nexport async function runDoctor(): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n\n const nodeMajor = Number(process.versions.node.split(\".\")[0]);\n results.push(\n nodeMajor >= 18\n ? { check: \"node\", status: \"ok\", detail: `Node ${process.versions.node}` }\n : {\n check: \"node\",\n status: \"warn\",\n detail: `Node ${process.versions.node} is below 18`,\n fix: \"Install Node 18+ (nvm install --lts)\",\n }\n );\n\n const probes = await Promise.all(PORTS.map(probePort));\n const relay = probes.find((p) => p.state === \"relai\");\n const foreign = probes.filter((p) => p.state === \"foreign\");\n if (relay) {\n results.push({\n check: \"relay\",\n status: \"ok\",\n detail: `Relai relay v${relay.version ?? \"?\"} on port ${relay.port}`,\n });\n const pluginRooms = (relay.rooms ?? []).filter((r) => r.hasPlugin);\n results.push(\n pluginRooms.length > 0\n ? {\n check: \"plugin\",\n status: \"ok\",\n detail: `Figma plugin online: ${pluginRooms\n .map((r) => `\"${r.fileName ?? r.room}\"`)\n .join(\", \")}`,\n }\n : {\n check: \"plugin\",\n status: \"warn\",\n detail: \"Relay is up but no Figma plugin is connected\",\n fix: \"Open the Relai plugin inside your Figma file (it auto-connects)\",\n }\n );\n } else {\n results.push({\n check: \"relay\",\n status: \"warn\",\n detail:\n foreign.length > 0\n ? `Ports ${foreign.map((p) => p.port).join(\", \")} are occupied by something that is not a Relai relay`\n : \"No relay on ports 9055–9057 — no MCP server is running\",\n fix:\n foreign.length > 0\n ? \"Free the port (lsof -i :9055) or let Relai fall back to 9056/9057\"\n : \"Start a conversation in your AI client with Relai registered (npx -y figma-relai)\",\n });\n results.push({\n check: \"plugin\",\n status: \"warn\",\n detail: \"Cannot see the plugin without a relay\",\n });\n }\n\n const statePath = join(homedir(), \".figma-relai\", \"state.json\");\n if (existsSync(statePath)) {\n const { room } = loadState();\n results.push({\n check: \"state\",\n status: \"ok\",\n detail: room ? `Saved room: ${room}` : \"State file present (no room saved yet)\",\n });\n } else {\n results.push({\n check: \"state\",\n status: \"ok\",\n detail: \"No state file yet — will be created on first pairing\",\n });\n }\n\n results.push(\n process.env.FIGMA_TOKEN\n ? { check: \"token\", status: \"ok\", detail: \"FIGMA_TOKEN set — comments unlocked\" }\n : {\n check: \"token\",\n status: \"ok\",\n detail: \"FIGMA_TOKEN not set — everything works except comments\",\n fix: \"Optional: add a personal access token to the MCP config env to unlock manage_comments\",\n }\n );\n\n return results;\n}\n\nexport function renderDoctor(results: CheckResult[]): string {\n const lines = [\"figma-relai doctor\", \"\"];\n for (const r of results) {\n lines.push(`${r.status === \"ok\" ? \"✓\" : \"!\"} ${r.check.padEnd(7)} ${r.detail}`);\n if (r.fix) lines.push(` → ${r.fix}`);\n }\n const warns = results.filter((r) => r.status === \"warn\").length;\n lines.push(\"\", warns === 0 ? \"All clear.\" : `${warns} thing(s) to look at.`);\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|