canicode 0.5.2 → 0.6.1
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 +8 -5
- package/dist/cli/index.js +37 -244
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +91 -116
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
<a href="https://github.com/let-sunny/canicode/actions/workflows/release.yml"><img src="https://github.com/let-sunny/canicode/actions/workflows/release.yml/badge.svg" alt="Release"></a>
|
|
11
11
|
<a href="https://let-sunny.github.io/canicode/"><img src="https://img.shields.io/badge/Try_it-GitHub_Pages-blue" alt="GitHub Pages"></a>
|
|
12
12
|
<a href="https://www.figma.com/community/plugin/1617144221046795292/canicode"><img src="https://img.shields.io/badge/Figma_Plugin-under_review-orange" alt="Figma Plugin"></a>
|
|
13
|
+
<a href="https://github.com/let-sunny/canicode#mcp-server-claude-code--cursor--claude-desktop"><img src="https://img.shields.io/badge/MCP_Registry-published-green" alt="MCP Registry"></a>
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
<p align="center">Analyze Figma designs. Score how dev-friendly and AI-friendly they are. Get actionable issues before writing code.</p>
|
|
16
17
|
|
|
18
|
+
<p align="center"><strong><a href="https://github.com/let-sunny/canicode/discussions/new?category=share-your-figma">Share your Figma design</a></strong> to help improve scoring accuracy.</p>
|
|
19
|
+
|
|
17
20
|
<p align="center"><strong><a href="https://let-sunny.github.io/canicode/">Try it in your browser</a></strong> — no install needed.</p>
|
|
18
21
|
|
|
19
22
|
<p align="center">
|
|
@@ -102,7 +105,7 @@ canicode init --token figd_xxxxxxxxxxxxx
|
|
|
102
105
|
|
|
103
106
|
**Claude Code:**
|
|
104
107
|
```bash
|
|
105
|
-
claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y canicode canicode-mcp
|
|
108
|
+
claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y -p canicode canicode-mcp
|
|
106
109
|
```
|
|
107
110
|
|
|
108
111
|
**Cursor** (`~/.cursor/mcp.json`):
|
|
@@ -146,9 +149,10 @@ Then ask: *"Analyze this Figma design: https://www.figma.com/design/..."*
|
|
|
146
149
|
|
|
147
150
|
| Flag | Source | Token required |
|
|
148
151
|
|------|--------|----------------|
|
|
149
|
-
| (none) |
|
|
150
|
-
| `--
|
|
151
|
-
|
|
152
|
+
| (none) | Figma REST API | Yes |
|
|
153
|
+
| `--api` | Figma REST API (explicit) | Yes |
|
|
154
|
+
|
|
155
|
+
For token-free analysis, use the **canicode MCP server** with the official Figma MCP, or the **`/canicode` skill** in Claude Code.
|
|
152
156
|
|
|
153
157
|
Token priority:
|
|
154
158
|
1. `--token` flag (one-time override)
|
|
@@ -299,7 +303,6 @@ Save Figma file data as JSON for offline analysis:
|
|
|
299
303
|
|
|
300
304
|
```bash
|
|
301
305
|
canicode save-fixture https://www.figma.com/design/ABC123/MyDesign
|
|
302
|
-
canicode save-fixture https://www.figma.com/design/ABC123/MyDesign --mcp
|
|
303
306
|
```
|
|
304
307
|
|
|
305
308
|
</details>
|
package/dist/cli/index.js
CHANGED
|
@@ -1060,137 +1060,6 @@ async function loadFigmaFileFromJson(filePath) {
|
|
|
1060
1060
|
const fileKey = basename(filePath, ".json");
|
|
1061
1061
|
return transformFigmaResponse(fileKey, data);
|
|
1062
1062
|
}
|
|
1063
|
-
|
|
1064
|
-
// src/adapters/figma-mcp-adapter.ts
|
|
1065
|
-
var TAG_TYPE_MAP = {
|
|
1066
|
-
canvas: "CANVAS",
|
|
1067
|
-
frame: "FRAME",
|
|
1068
|
-
group: "GROUP",
|
|
1069
|
-
section: "SECTION",
|
|
1070
|
-
component: "COMPONENT",
|
|
1071
|
-
"component-set": "COMPONENT_SET",
|
|
1072
|
-
instance: "INSTANCE",
|
|
1073
|
-
rectangle: "RECTANGLE",
|
|
1074
|
-
"rounded-rectangle": "RECTANGLE",
|
|
1075
|
-
ellipse: "ELLIPSE",
|
|
1076
|
-
vector: "VECTOR",
|
|
1077
|
-
text: "TEXT",
|
|
1078
|
-
line: "LINE",
|
|
1079
|
-
"boolean-operation": "BOOLEAN_OPERATION",
|
|
1080
|
-
star: "STAR",
|
|
1081
|
-
"regular-polygon": "REGULAR_POLYGON",
|
|
1082
|
-
slice: "SLICE",
|
|
1083
|
-
sticky: "STICKY",
|
|
1084
|
-
table: "TABLE",
|
|
1085
|
-
"table-cell": "TABLE_CELL",
|
|
1086
|
-
symbol: "COMPONENT",
|
|
1087
|
-
slot: "FRAME"
|
|
1088
|
-
};
|
|
1089
|
-
function parseXml(xml) {
|
|
1090
|
-
const nodes = [];
|
|
1091
|
-
const stack = [];
|
|
1092
|
-
const tagRegex = /<(\/?)([\w-]+)([^>]*?)(\/?)>/g;
|
|
1093
|
-
let match;
|
|
1094
|
-
while ((match = tagRegex.exec(xml)) !== null) {
|
|
1095
|
-
const isClosing = match[1] === "/";
|
|
1096
|
-
const tagName = match[2];
|
|
1097
|
-
const attrString = match[3] ?? "";
|
|
1098
|
-
const isSelfClosing = match[4] === "/";
|
|
1099
|
-
if (isClosing) {
|
|
1100
|
-
const finished = stack.pop();
|
|
1101
|
-
if (finished) {
|
|
1102
|
-
const parent = stack[stack.length - 1];
|
|
1103
|
-
if (parent) {
|
|
1104
|
-
parent.children.push(finished);
|
|
1105
|
-
} else {
|
|
1106
|
-
nodes.push(finished);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
} else {
|
|
1110
|
-
const attrs = parseAttributes(attrString);
|
|
1111
|
-
const node = { tag: tagName, attrs, children: [] };
|
|
1112
|
-
if (isSelfClosing) {
|
|
1113
|
-
const parent = stack[stack.length - 1];
|
|
1114
|
-
if (parent) {
|
|
1115
|
-
parent.children.push(node);
|
|
1116
|
-
} else {
|
|
1117
|
-
nodes.push(node);
|
|
1118
|
-
}
|
|
1119
|
-
} else {
|
|
1120
|
-
stack.push(node);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
while (stack.length > 0) {
|
|
1125
|
-
const finished = stack.pop();
|
|
1126
|
-
const parent = stack[stack.length - 1];
|
|
1127
|
-
if (parent) {
|
|
1128
|
-
parent.children.push(finished);
|
|
1129
|
-
} else {
|
|
1130
|
-
nodes.push(finished);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
return nodes;
|
|
1134
|
-
}
|
|
1135
|
-
function parseAttributes(attrString) {
|
|
1136
|
-
const attrs = {};
|
|
1137
|
-
const attrRegex = /([\w-]+)="([^"]*)"/g;
|
|
1138
|
-
let match;
|
|
1139
|
-
while ((match = attrRegex.exec(attrString)) !== null) {
|
|
1140
|
-
const key = match[1];
|
|
1141
|
-
const value = match[2];
|
|
1142
|
-
if (key && value !== void 0) {
|
|
1143
|
-
attrs[key] = value;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
return attrs;
|
|
1147
|
-
}
|
|
1148
|
-
function toAnalysisNode(xmlNode) {
|
|
1149
|
-
const type = TAG_TYPE_MAP[xmlNode.tag] ?? "FRAME";
|
|
1150
|
-
const id = xmlNode.attrs["id"] ?? "0:0";
|
|
1151
|
-
const name = xmlNode.attrs["name"] ?? xmlNode.tag;
|
|
1152
|
-
const hidden = xmlNode.attrs["hidden"] === "true";
|
|
1153
|
-
const x = parseFloat(xmlNode.attrs["x"] ?? "0");
|
|
1154
|
-
const y = parseFloat(xmlNode.attrs["y"] ?? "0");
|
|
1155
|
-
const width = parseFloat(xmlNode.attrs["width"] ?? "0");
|
|
1156
|
-
const height = parseFloat(xmlNode.attrs["height"] ?? "0");
|
|
1157
|
-
const node = {
|
|
1158
|
-
id,
|
|
1159
|
-
name,
|
|
1160
|
-
type,
|
|
1161
|
-
visible: !hidden,
|
|
1162
|
-
absoluteBoundingBox: { x, y, width, height }
|
|
1163
|
-
};
|
|
1164
|
-
if (xmlNode.children.length > 0) {
|
|
1165
|
-
node.children = xmlNode.children.map(toAnalysisNode);
|
|
1166
|
-
}
|
|
1167
|
-
return node;
|
|
1168
|
-
}
|
|
1169
|
-
function parseMcpMetadataXml(xml, fileKey, fileName) {
|
|
1170
|
-
const parsed = parseXml(xml);
|
|
1171
|
-
const children = parsed.map(toAnalysisNode);
|
|
1172
|
-
let document;
|
|
1173
|
-
if (children.length === 1 && children[0]) {
|
|
1174
|
-
document = children[0];
|
|
1175
|
-
} else {
|
|
1176
|
-
document = {
|
|
1177
|
-
id: "0:0",
|
|
1178
|
-
name: "Document",
|
|
1179
|
-
type: "DOCUMENT",
|
|
1180
|
-
visible: true,
|
|
1181
|
-
children
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
return {
|
|
1185
|
-
fileKey,
|
|
1186
|
-
name: fileName ?? fileKey,
|
|
1187
|
-
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1188
|
-
version: "mcp",
|
|
1189
|
-
document,
|
|
1190
|
-
components: {},
|
|
1191
|
-
styles: {}
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
1063
|
var AIREADY_DIR = join(homedir(), ".canicode");
|
|
1195
1064
|
var CONFIG_PATH = join(AIREADY_DIR, "config.json");
|
|
1196
1065
|
var REPORTS_DIR = join(AIREADY_DIR, "reports");
|
|
@@ -1257,7 +1126,7 @@ function isFigmaUrl(input) {
|
|
|
1257
1126
|
function isJsonFile(input) {
|
|
1258
1127
|
return input.endsWith(".json");
|
|
1259
1128
|
}
|
|
1260
|
-
async function loadFile(input, token
|
|
1129
|
+
async function loadFile(input, token) {
|
|
1261
1130
|
if (isJsonFile(input)) {
|
|
1262
1131
|
const filePath = resolve(input);
|
|
1263
1132
|
if (!existsSync(filePath)) {
|
|
@@ -1267,31 +1136,13 @@ async function loadFile(input, token, mode = "auto") {
|
|
|
1267
1136
|
return { file: await loadFigmaFileFromJson(filePath) };
|
|
1268
1137
|
}
|
|
1269
1138
|
if (isFigmaUrl(input)) {
|
|
1270
|
-
const { fileKey, nodeId
|
|
1271
|
-
|
|
1272
|
-
return loadFromMcp(fileKey, nodeId, fileName);
|
|
1273
|
-
}
|
|
1274
|
-
if (mode === "api") {
|
|
1275
|
-
return loadFromApi(fileKey, nodeId, token);
|
|
1276
|
-
}
|
|
1277
|
-
try {
|
|
1278
|
-
console.log("Auto-detecting data source... trying MCP first.");
|
|
1279
|
-
return await loadFromMcp(fileKey, nodeId, fileName);
|
|
1280
|
-
} catch (mcpError) {
|
|
1281
|
-
const mcpMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
|
|
1282
|
-
console.log(`MCP unavailable (${mcpMsg}). Falling back to REST API.`);
|
|
1283
|
-
return loadFromApi(fileKey, nodeId, token);
|
|
1284
|
-
}
|
|
1139
|
+
const { fileKey, nodeId } = parseFigmaUrl(input);
|
|
1140
|
+
return loadFromApi(fileKey, nodeId, token);
|
|
1285
1141
|
}
|
|
1286
1142
|
throw new Error(
|
|
1287
1143
|
`Invalid input: ${input}. Provide a Figma URL or JSON file path.`
|
|
1288
1144
|
);
|
|
1289
1145
|
}
|
|
1290
|
-
async function loadFromMcp(fileKey, nodeId, fileName) {
|
|
1291
|
-
console.log(`Loading via MCP: ${fileKey} (node: ${nodeId ?? "root"})`);
|
|
1292
|
-
const file = await loadViaMcp(fileKey, nodeId ?? "0:1", fileName);
|
|
1293
|
-
return { file, nodeId };
|
|
1294
|
-
}
|
|
1295
1146
|
async function loadFromApi(fileKey, nodeId, token) {
|
|
1296
1147
|
console.log(`Fetching from Figma REST API: ${fileKey}`);
|
|
1297
1148
|
if (nodeId) {
|
|
@@ -1310,20 +1161,6 @@ async function loadFromApi(fileKey, nodeId, token) {
|
|
|
1310
1161
|
nodeId
|
|
1311
1162
|
};
|
|
1312
1163
|
}
|
|
1313
|
-
async function loadViaMcp(fileKey, nodeId, fileName) {
|
|
1314
|
-
const { execSync } = await import('child_process');
|
|
1315
|
-
const result = execSync(
|
|
1316
|
-
`claude --print "Use the mcp__figma__get_metadata tool with fileKey=\\"${fileKey}\\" and nodeId=\\"${nodeId.replace(/-/g, ":")}\\" \u2014 return ONLY the raw XML output, nothing else."`,
|
|
1317
|
-
{ encoding: "utf-8", timeout: 12e4 }
|
|
1318
|
-
);
|
|
1319
|
-
const xmlStart = result.indexOf("<");
|
|
1320
|
-
const xmlEnd = result.lastIndexOf(">");
|
|
1321
|
-
if (xmlStart === -1 || xmlEnd === -1) {
|
|
1322
|
-
throw new Error("MCP did not return valid XML metadata");
|
|
1323
|
-
}
|
|
1324
|
-
const xml = result.slice(xmlStart, xmlEnd + 1);
|
|
1325
|
-
return parseMcpMetadataXml(xml, fileKey, fileName);
|
|
1326
|
-
}
|
|
1327
1164
|
|
|
1328
1165
|
// src/core/scoring.ts
|
|
1329
1166
|
var SEVERITY_DENSITY_WEIGHT = {
|
|
@@ -2923,7 +2760,7 @@ function printDocsSetup() {
|
|
|
2923
2760
|
CANICODE SETUP GUIDE
|
|
2924
2761
|
|
|
2925
2762
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2926
|
-
1. CLI
|
|
2763
|
+
1. CLI (REST API)
|
|
2927
2764
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2928
2765
|
|
|
2929
2766
|
Install:
|
|
@@ -2937,11 +2774,6 @@ CANICODE SETUP GUIDE
|
|
|
2937
2774
|
canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
2938
2775
|
(opens report in browser automatically, use --no-open to disable)
|
|
2939
2776
|
|
|
2940
|
-
Data source flags:
|
|
2941
|
-
--api REST API (uses saved token)
|
|
2942
|
-
--mcp Figma MCP bridge (Claude Code only, no token needed)
|
|
2943
|
-
(none) Auto: try MCP first, fallback to API
|
|
2944
|
-
|
|
2945
2777
|
Options:
|
|
2946
2778
|
--preset strict|relaxed|dev-friendly|ai-ready
|
|
2947
2779
|
--config ./my-config.json
|
|
@@ -2952,54 +2784,25 @@ CANICODE SETUP GUIDE
|
|
|
2952
2784
|
~/.canicode/reports/report-YYYY-MM-DD-HH-mm-<filekey>.html
|
|
2953
2785
|
|
|
2954
2786
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2955
|
-
2.
|
|
2787
|
+
2. CLAUDE CODE SKILL (Figma MCP, no token needed)
|
|
2956
2788
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2957
2789
|
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
Install (once):
|
|
2961
|
-
claude mcp add figma -- npx -y @anthropic-ai/claude-code-mcp-figma
|
|
2962
|
-
claude mcp add --transport stdio canicode npx canicode-mcp
|
|
2963
|
-
|
|
2964
|
-
Flow:
|
|
2965
|
-
Claude Code
|
|
2966
|
-
-> Figma MCP get_metadata(fileKey, nodeId) -> XML node tree
|
|
2967
|
-
-> canicode MCP analyze(designData: XML) -> analysis result
|
|
2790
|
+
Requires the official Figma MCP server at project level.
|
|
2968
2791
|
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
Install (once):
|
|
2972
|
-
claude mcp add --transport stdio canicode npx canicode-mcp
|
|
2973
|
-
canicode init --token figd_xxxxxxxxxxxxx
|
|
2974
|
-
|
|
2975
|
-
Flow:
|
|
2976
|
-
Claude Code
|
|
2977
|
-
-> canicode MCP analyze(input: URL) -> internal REST API fetch -> result
|
|
2978
|
-
|
|
2979
|
-
Use (both routes \u2014 just ask Claude Code):
|
|
2980
|
-
"Analyze this Figma design: https://www.figma.com/design/..."
|
|
2981
|
-
|
|
2982
|
-
Route A vs B:
|
|
2983
|
-
A: No token, 2 MCP servers, Claude orchestrates 2 calls
|
|
2984
|
-
B: Token needed, 1 MCP server, canicode fetches directly
|
|
2985
|
-
|
|
2986
|
-
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2987
|
-
3. CLAUDE SKILLS (lightweight)
|
|
2988
|
-
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
2989
|
-
|
|
2990
|
-
Install:
|
|
2991
|
-
cp -r path/to/canicode/.claude/skills/canicode .claude/skills/
|
|
2992
|
-
|
|
2993
|
-
Setup (for REST API):
|
|
2994
|
-
npx canicode init --token figd_xxxxxxxxxxxxx
|
|
2792
|
+
Setup (once):
|
|
2793
|
+
claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
2995
2794
|
|
|
2996
2795
|
Use (in Claude Code):
|
|
2997
|
-
/canicode
|
|
2796
|
+
/canicode https://www.figma.com/design/ABC123/MyDesign?node-id=1-234
|
|
2998
2797
|
|
|
2999
|
-
|
|
2798
|
+
Flow:
|
|
2799
|
+
Claude Code
|
|
2800
|
+
-> Figma MCP get_metadata(fileKey, nodeId) -> XML node tree
|
|
2801
|
+
-> Convert to fixture JSON
|
|
2802
|
+
-> canicode analyze fixture.json -> report
|
|
3000
2803
|
|
|
3001
2804
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
3002
|
-
TOKEN PRIORITY (
|
|
2805
|
+
TOKEN PRIORITY (CLI mode)
|
|
3003
2806
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
3004
2807
|
|
|
3005
2808
|
1. --token flag (one-time override)
|
|
@@ -3011,9 +2814,11 @@ CANICODE SETUP GUIDE
|
|
|
3011
2814
|
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
3012
2815
|
|
|
3013
2816
|
CI/CD, automation -> CLI + FIGMA_TOKEN env var
|
|
3014
|
-
Claude Code
|
|
3015
|
-
|
|
3016
|
-
|
|
2817
|
+
Claude Code (full) -> canicode MCP + Figma MCP (no token needed)
|
|
2818
|
+
Claude Code (light) -> /canicode skill + Figma MCP (no token needed)
|
|
2819
|
+
In Figma -> Figma Plugin
|
|
2820
|
+
Browser -> Web App (GitHub Pages)
|
|
2821
|
+
Quick trial, offline -> CLI + JSON fixtures
|
|
3017
2822
|
`.trimStart());
|
|
3018
2823
|
}
|
|
3019
2824
|
function printDocsRules() {
|
|
@@ -3055,7 +2860,7 @@ EXAMPLE
|
|
|
3055
2860
|
"maxHeight": 48,
|
|
3056
2861
|
"nameContains": "icon"
|
|
3057
2862
|
},
|
|
3058
|
-
"message": ""{name}" is an icon but not a component",
|
|
2863
|
+
"message": "\\"{name}\\" is an icon but not a component",
|
|
3059
2864
|
"why": "Icons should be reusable components.",
|
|
3060
2865
|
"impact": "Developers hardcode icons.",
|
|
3061
2866
|
"fix": "Convert to component and publish to library."
|
|
@@ -4414,16 +4219,13 @@ function countNodes2(node) {
|
|
|
4414
4219
|
}
|
|
4415
4220
|
return count;
|
|
4416
4221
|
}
|
|
4417
|
-
cli.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--
|
|
4222
|
+
cli.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--output <path>", "HTML report output path").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--custom-rules <path>", "Path to custom rules JSON file").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign --api --token YOUR_TOKEN").example(" canicode analyze ./fixtures/design.json --output report.html").example(" canicode analyze ./fixtures/design.json --custom-rules ./my-rules.json").example(" canicode analyze ./fixtures/design.json --config ./my-config.json").action(async (input, options) => {
|
|
4418
4223
|
const analysisStart = Date.now();
|
|
4419
4224
|
trackEvent(EVENTS.ANALYSIS_STARTED, { source: isJsonFile(input) ? "fixture" : "figma" });
|
|
4420
4225
|
try {
|
|
4421
|
-
if (options.
|
|
4422
|
-
throw new Error("Cannot use --mcp and --api together. Choose one.");
|
|
4423
|
-
}
|
|
4424
|
-
if (!options.mcp && !options.token && !getFigmaToken() && !isJsonFile(input)) {
|
|
4226
|
+
if (!options.token && !getFigmaToken() && !isJsonFile(input)) {
|
|
4425
4227
|
throw new Error(
|
|
4426
|
-
"canicode is not configured. Run 'canicode init --token YOUR_TOKEN' first
|
|
4228
|
+
"canicode is not configured. Run 'canicode init --token YOUR_TOKEN' first."
|
|
4427
4229
|
);
|
|
4428
4230
|
}
|
|
4429
4231
|
if (options.screenshot) {
|
|
@@ -4435,8 +4237,7 @@ cli.command("analyze <input>", "Analyze a Figma file or JSON fixture").option("-
|
|
|
4435
4237
|
}
|
|
4436
4238
|
console.log("Screenshot comparison mode enabled (coming soon).\n");
|
|
4437
4239
|
}
|
|
4438
|
-
const
|
|
4439
|
-
const { file, nodeId } = await loadFile(input, options.token, mode);
|
|
4240
|
+
const { file, nodeId } = await loadFile(input, options.token);
|
|
4440
4241
|
const totalNodes = countNodes2(file.document);
|
|
4441
4242
|
let effectiveNodeId = nodeId;
|
|
4442
4243
|
if (!effectiveNodeId && totalNodes > MAX_NODES_WITHOUT_SCOPE) {
|
|
@@ -4718,17 +4519,13 @@ cli.command(
|
|
|
4718
4519
|
cli.command(
|
|
4719
4520
|
"save-fixture <input>",
|
|
4720
4521
|
"Save Figma file data as a JSON fixture for offline analysis"
|
|
4721
|
-
).option("--output <path>", "Output JSON path (default: fixtures/<filekey>.json)").option("--
|
|
4522
|
+
).option("--output <path>", "Output JSON path (default: fixtures/<filekey>.json)").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").example(" canicode save-fixture https://www.figma.com/design/ABC123/MyDesign").example(" canicode save-fixture https://www.figma.com/design/ABC123/MyDesign --token YOUR_TOKEN").action(async (input, options) => {
|
|
4722
4523
|
try {
|
|
4723
|
-
if (options.mcp && options.api) {
|
|
4724
|
-
throw new Error("Cannot use --mcp and --api together. Choose one.");
|
|
4725
|
-
}
|
|
4726
4524
|
if (isFigmaUrl(input) && !parseFigmaUrl(input).nodeId) {
|
|
4727
4525
|
console.warn("\nWarning: No node-id specified. Saving entire file as fixture.");
|
|
4728
4526
|
console.warn("Tip: Add ?node-id=XXX to save a specific section.\n");
|
|
4729
4527
|
}
|
|
4730
|
-
const
|
|
4731
|
-
const { file } = await loadFile(input, options.token, mode);
|
|
4528
|
+
const { file } = await loadFile(input, options.token);
|
|
4732
4529
|
const outputPath = resolve(
|
|
4733
4530
|
options.output ?? `fixtures/${file.fileKey}.json`
|
|
4734
4531
|
);
|
|
@@ -4759,19 +4556,17 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
|
|
|
4759
4556
|
return;
|
|
4760
4557
|
}
|
|
4761
4558
|
if (options.mcp) {
|
|
4762
|
-
console.log(`MCP SETUP
|
|
4559
|
+
console.log(`FIGMA MCP SETUP (for Claude Code)
|
|
4763
4560
|
`);
|
|
4764
|
-
console.log(`1.
|
|
4765
|
-
console.log(` claude mcp add
|
|
4561
|
+
console.log(`1. Register the official Figma MCP server at project level:`);
|
|
4562
|
+
console.log(` claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
4766
4563
|
`);
|
|
4767
|
-
console.log(`
|
|
4768
|
-
console.log(` claude mcp add --transport stdio canicode npx canicode-mcp
|
|
4564
|
+
console.log(` This creates .mcp.json in your project root.
|
|
4769
4565
|
`);
|
|
4770
|
-
console.log(`
|
|
4771
|
-
console.log(` canicode
|
|
4566
|
+
console.log(`2. Use the /canicode skill in Claude Code:`);
|
|
4567
|
+
console.log(` /canicode https://www.figma.com/design/.../MyDesign?node-id=1-234
|
|
4772
4568
|
`);
|
|
4773
|
-
console.log(`
|
|
4774
|
-
console.log(` "Analyze this Figma design: https://www.figma.com/design/..."`);
|
|
4569
|
+
console.log(` The skill calls Figma MCP directly \u2014 no FIGMA_TOKEN needed.`);
|
|
4775
4570
|
return;
|
|
4776
4571
|
}
|
|
4777
4572
|
console.log(`CANICODE SETUP
|
|
@@ -4784,7 +4579,7 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
|
|
|
4784
4579
|
`);
|
|
4785
4580
|
console.log(`Option 2: Figma MCP (recommended for Claude Code)`);
|
|
4786
4581
|
console.log(` canicode init --mcp`);
|
|
4787
|
-
console.log(`
|
|
4582
|
+
console.log(` Uses the /canicode skill in Claude Code with official Figma MCP
|
|
4788
4583
|
`);
|
|
4789
4584
|
console.log(`After setup:`);
|
|
4790
4585
|
console.log(` canicode analyze "https://www.figma.com/design/..."`);
|
|
@@ -4895,9 +4690,8 @@ cli.help((sections) => {
|
|
|
4895
4690
|
{
|
|
4896
4691
|
title: "\nData source",
|
|
4897
4692
|
body: [
|
|
4898
|
-
` --mcp Load via Figma MCP (no token needed)`,
|
|
4899
4693
|
` --api Load via Figma REST API (needs FIGMA_TOKEN)`,
|
|
4900
|
-
` (
|
|
4694
|
+
` --token <token> Figma API token (or use FIGMA_TOKEN env var)`
|
|
4901
4695
|
].join("\n")
|
|
4902
4696
|
},
|
|
4903
4697
|
{
|
|
@@ -4910,7 +4704,6 @@ cli.help((sections) => {
|
|
|
4910
4704
|
{
|
|
4911
4705
|
title: "\nExamples",
|
|
4912
4706
|
body: [
|
|
4913
|
-
` $ canicode analyze "https://www.figma.com/design/..." --mcp`,
|
|
4914
4707
|
` $ canicode analyze "https://www.figma.com/design/..." --api`,
|
|
4915
4708
|
` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
|
|
4916
4709
|
` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`
|
|
@@ -4920,7 +4713,7 @@ cli.help((sections) => {
|
|
|
4920
4713
|
title: "\nInstallation",
|
|
4921
4714
|
body: [
|
|
4922
4715
|
` CLI: npm install -g canicode`,
|
|
4923
|
-
` MCP: claude mcp add --
|
|
4716
|
+
` MCP: claude mcp add canicode -- npx -y -p canicode canicode-mcp`,
|
|
4924
4717
|
` Skills: github.com/let-sunny/canicode`
|
|
4925
4718
|
].join("\n")
|
|
4926
4719
|
}
|