canicode 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,23 +11,26 @@
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
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>
14
+ <a href="https://github.com/marketplace/actions/canicode-action"><img src="https://img.shields.io/badge/GitHub_Action-Marketplace-2088FF" alt="GitHub Action"></a>
14
15
  </p>
15
16
 
16
- <p align="center">Analyze Figma designs. Score how dev-friendly and AI-friendly they are. Get actionable issues before writing code.</p>
17
+ <p align="center">The design linter that scores how easily your Figma design can be implemented by AI or developers — before a single line of code is written.</p>
18
+
19
+ <p align="center">No AI tokens consumed per analysis. Rules run deterministically — AI only validated the scores during development.</p>
17
20
 
18
21
  <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
22
 
20
23
  <p align="center"><strong><a href="https://let-sunny.github.io/canicode/">Try it in your browser</a></strong> — no install needed.</p>
21
24
 
22
25
  <p align="center">
23
- <img src="docs/images/screenshot.png" alt="CanICode Report" width="720">
26
+ <img src="docs/images/screenshot.gif" alt="CanICode Report" width="720">
24
27
  </p>
25
28
 
26
29
  ---
27
30
 
28
31
  ## How It Works
29
32
 
30
- 39 rules across 6 categories check every node in the Figma tree:
33
+ 39 rules. 6 categories. Every node in the Figma tree.
31
34
 
32
35
  | Category | Rules | What it checks |
33
36
  |----------|-------|----------------|
@@ -40,53 +43,110 @@
40
43
 
41
44
  Each issue is classified: **Blocking** > **Risk** > **Missing Info** > **Suggestion**.
42
45
 
43
- Scores use density + diversity weighting per category, combined into an overall grade (S/A+/A/B+/B/C+/C/D/F). Rule scores are calibrated against actual code conversion difficulty — see [`docs/CALIBRATION.md`](docs/CALIBRATION.md).
46
+ ### Rule Scores Validated by AI
47
+
48
+ Rule scores aren't guesswork. They're validated through a 4-agent debate pipeline that converts real Figma nodes to code and measures actual implementation difficulty.
49
+
50
+ 1. **Runner** analyzes the design and flags issues
51
+ 2. **Converter** converts the flagged nodes to actual code
52
+ 3. **Critic** challenges whether the scores match the real difficulty
53
+ 4. **Arbitrator** makes the final call — adjust or keep
54
+
55
+ - A node that's hard to implement → rule score goes up
56
+ - A node that's easy to implement despite the flag → rule score goes down
57
+
58
+ The rules themselves run deterministically on every analysis — no tokens consumed. The AI debate validates scores when new fixtures are added, not on every run. See [`docs/CALIBRATION.md`](docs/CALIBRATION.md).
44
59
 
45
60
  ---
46
61
 
47
62
  ## Getting Started
48
63
 
49
- Five ways to use CanICode. Pick one.
64
+ | If you want to... | Use |
65
+ |---|---|
66
+ | Just try it | **[Web App](https://let-sunny.github.io/canicode/)** — paste a URL, no install |
67
+ | Analyze inside Figma | **[Figma Plugin](https://www.figma.com/community/plugin/1617144221046795292/canicode)** (under review) |
68
+ | Use with Claude Code / Cursor | **MCP Server** or **Skill** — see below |
69
+ | Add to CI/CD | **[GitHub Action](https://github.com/marketplace/actions/canicode-action)** |
70
+ | Full control | **CLI** |
50
71
 
51
- ### 1. CLI
72
+ <details>
73
+ <summary><strong>CLI</strong></summary>
52
74
 
53
75
  ```bash
54
76
  npx canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
55
77
  ```
56
78
 
57
- Setup:
58
- ```bash
59
- canicode init --token figd_xxxxxxxxxxxxx
60
- ```
79
+ Setup: `canicode init --token figd_xxxxxxxxxxxxx`
61
80
 
62
81
  > **Get your token:** Figma → Settings → Security → Personal access tokens → Generate new token
63
82
 
64
- ### 2. MCP Server (Claude Code / Cursor / Claude Desktop)
83
+ **Figma API Rate Limits** Rate limits depend on **where the file lives**, not just your plan.
84
+
85
+ | Seat | File in Starter plan | File in Pro/Org/Enterprise |
86
+ |------|---------------------|---------------------------|
87
+ | View, Collab | 6 req/month | 6 req/month |
88
+ | Dev, Full | 6 req/month | 10–20 req/min |
89
+
90
+ Hitting 429 errors? Make sure the file is in a paid workspace. Or use MCP (no token, separate rate limit pool). Or `save-fixture` once and analyze locally. [Full details](https://developers.figma.com/docs/rest-api/rate-limits/)
91
+
92
+ </details>
93
+
94
+ <details>
95
+ <summary><strong>MCP Server</strong> (Claude Code / Cursor / Claude Desktop)</summary>
65
96
 
66
- **Claude Code (recommended — with official Figma MCP, no token needed):**
67
97
  ```bash
68
98
  claude mcp add canicode -- npx -y -p canicode canicode-mcp
69
99
  claude mcp add -s project -t http figma https://mcp.figma.com/mcp
70
100
  ```
71
101
 
72
- **Claude Code (with Figma API token):**
102
+ Then ask: *"Analyze this Figma design: https://www.figma.com/design/..."*
103
+
104
+ canicode's rule engine analyzes the design data — the AI assistant just orchestrates the calls.
105
+
106
+ Or with a Figma API token (no Figma MCP needed):
73
107
  ```bash
74
108
  claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y -p canicode canicode-mcp
75
109
  ```
76
110
 
77
111
  For Cursor / Claude Desktop config, see [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md).
78
112
 
79
- Then ask: *"Analyze this Figma design: https://www.figma.com/design/..."*
113
+ **Figma MCP Rate Limits**
114
+
115
+ | Plan | Limit |
116
+ |------|-------|
117
+ | Starter | 6 tool calls/month |
118
+ | Pro / Org — Full or Dev seat | 200 tool calls/day |
119
+ | Enterprise — Full or Dev seat | 600 tool calls/day |
120
+
121
+ MCP and CLI use separate rate limit pools — switching to MCP won't affect your CLI quota. [Full details](https://developers.figma.com/docs/figma-mcp-server/plans-access-and-permissions/)
122
+
123
+ </details>
80
124
 
81
- > **Note:** MCP/Skill path extracts style data from Figma MCP's generated code (React + Tailwind), not raw Figma node properties. Results may differ slightly from CLI — see [`docs/MCP-VS-CLI.md`](docs/MCP-VS-CLI.md) for a detailed comparison. For the most accurate analysis, use the CLI with a Figma API token.
125
+ <details>
126
+ <summary><strong>Claude Code Skill</strong> (lightweight, no MCP install)</summary>
82
127
 
83
- ### 3. Web (no install)
128
+ ```bash
129
+ cp -r .claude/skills/canicode /your-project/.claude/skills/
130
+ ```
131
+
132
+ Requires the official Figma MCP. Then use `/canicode` with a Figma URL.
84
133
 
85
- Go to **[let-sunny.github.io/canicode](https://let-sunny.github.io/canicode/)**, paste a Figma URL, and get results instantly in your browser.
134
+ </details>
135
+
136
+ <details>
137
+ <summary><strong>GitHub Action</strong></summary>
138
+
139
+ ```yaml
140
+ - uses: let-sunny/canicode-action@v0.1.0
141
+ with:
142
+ figma_url: 'https://www.figma.com/design/ABC123/MyDesign?node-id=1-234'
143
+ figma_token: ${{ secrets.FIGMA_TOKEN }}
144
+ min_score: 70
145
+ ```
86
146
 
87
- ### 4. Figma Plugin (under review)
147
+ Posts analysis as a PR comment. Fails if score is below threshold. See [**canicode-action**](https://github.com/marketplace/actions/canicode-action) on Marketplace.
88
148
 
89
- Install from **[Figma Community](https://www.figma.com/community/plugin/1617144221046795292/canicode)** — analyze directly inside Figma. No tokens needed.
149
+ </details>
90
150
 
91
151
  ---
92
152
 
@@ -126,12 +186,14 @@ For architecture details, see [`CLAUDE.md`](CLAUDE.md). For calibration pipeline
126
186
  - [x] **Phase 3** — Config overrides, MCP server, Claude Skills
127
187
  - [x] **Phase 4** — Figma comment from report (per-issue "Comment" button in HTML report, posts to Figma node via API)
128
188
  - [x] **Phase 5** — Custom rules with pattern matching (node name/type/attribute conditions)
129
- - [ ] **Phase 6** — Screenshot comparison (Figma vs AI-generated code, visual diff)
189
+ - [x] **Phase 6** — Screenshot comparison (`visual-compare` CLI: Figma vs AI-generated code, pixel-level diff)
190
+ - [x] **Phase 7** — Calibration pipeline upgrade (visual-compare + Gap Analyzer for objective score validation)
191
+ - [x] **Phase 8** — Rule discovery pipeline (6-agent debate: researcher → designer → implementer → A/B visual validation → evaluator → critic)
192
+ - [ ] **Ongoing** — Rule refinement via calibration + gap analysis on community fixtures
130
193
 
131
194
  ## Support
132
195
 
133
- - **Bug reports:** [GitHub Issues](https://github.com/let-sunny/canicode/issues)
134
- - **Questions and discussions:** [GitHub Issues](https://github.com/let-sunny/canicode/issues)
196
+ - **Bugs and questions:** [GitHub Issues](https://github.com/let-sunny/canicode/issues)
135
197
  - **Privacy:** See [PRIVACY.md](PRIVACY.md) for details on data collection and how to opt out
136
198
 
137
199
  ## License
package/dist/cli/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
- import { writeFile, readFile, appendFile } from 'fs/promises';
4
3
  import { join, resolve, dirname, basename } from 'path';
4
+ import pixelmatch from 'pixelmatch';
5
+ import { PNG } from 'pngjs';
6
+ import { writeFile, readFile, appendFile } from 'fs/promises';
5
7
  import { createRequire } from 'module';
6
8
  import { config } from 'dotenv';
7
9
  import cac from 'cac';
@@ -9,6 +11,108 @@ import { z } from 'zod';
9
11
  import { randomUUID } from 'crypto';
10
12
  import { homedir } from 'os';
11
13
 
14
+ var __defProp = Object.defineProperty;
15
+ var __getOwnPropNames = Object.getOwnPropertyNames;
16
+ var __esm = (fn, res) => function __init() {
17
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+
24
+ // src/core/engine/visual-compare.ts
25
+ var visual_compare_exports = {};
26
+ __export(visual_compare_exports, {
27
+ visualCompare: () => visualCompare
28
+ });
29
+ async function fetchFigmaScreenshot(fileKey, nodeId, token, outputPath) {
30
+ const res = await fetch(
31
+ `https://api.figma.com/v1/images/${fileKey}?ids=${nodeId}&format=png&scale=2`,
32
+ { headers: { "X-Figma-Token": token } }
33
+ );
34
+ if (!res.ok) throw new Error(`Figma Images API: ${res.status} ${res.statusText}`);
35
+ const data = await res.json();
36
+ const imgUrl = data.images[nodeId];
37
+ if (!imgUrl) throw new Error(`No image returned for node ${nodeId}`);
38
+ const imgRes = await fetch(imgUrl);
39
+ if (!imgRes.ok) throw new Error(`Failed to download Figma screenshot: ${imgRes.status}`);
40
+ const buffer = Buffer.from(await imgRes.arrayBuffer());
41
+ mkdirSync(dirname(outputPath), { recursive: true });
42
+ writeFileSync(outputPath, buffer);
43
+ }
44
+ async function renderCodeScreenshot(codePath, outputPath, viewport) {
45
+ const { chromium } = await import('playwright');
46
+ const browser = await chromium.launch();
47
+ const page = await browser.newPage({ viewport });
48
+ await page.goto(`file://${resolve(codePath)}`, {
49
+ waitUntil: "networkidle",
50
+ timeout: 3e4
51
+ });
52
+ await page.waitForTimeout(1e3);
53
+ await page.screenshot({ path: outputPath, fullPage: true });
54
+ await browser.close();
55
+ }
56
+ function resizePng(png, targetWidth, targetHeight) {
57
+ const resized = new PNG({ width: targetWidth, height: targetHeight });
58
+ for (let y = 0; y < targetHeight; y++) {
59
+ for (let x = 0; x < targetWidth; x++) {
60
+ const srcX = Math.floor(x / targetWidth * png.width);
61
+ const srcY = Math.floor(y / targetHeight * png.height);
62
+ const srcIdx = (srcY * png.width + srcX) * 4;
63
+ const dstIdx = (y * targetWidth + x) * 4;
64
+ resized.data[dstIdx] = png.data[srcIdx];
65
+ resized.data[dstIdx + 1] = png.data[srcIdx + 1];
66
+ resized.data[dstIdx + 2] = png.data[srcIdx + 2];
67
+ resized.data[dstIdx + 3] = png.data[srcIdx + 3];
68
+ }
69
+ }
70
+ return resized;
71
+ }
72
+ function compareScreenshots(path1, path2, diffOutputPath) {
73
+ const raw1 = PNG.sync.read(readFileSync(path1));
74
+ const raw2 = PNG.sync.read(readFileSync(path2));
75
+ const width = Math.max(raw1.width, raw2.width);
76
+ const height = Math.max(raw1.height, raw2.height);
77
+ const img1 = raw1.width !== width || raw1.height !== height ? resizePng(raw1, width, height) : raw1;
78
+ const img2 = raw2.width !== width || raw2.height !== height ? resizePng(raw2, width, height) : raw2;
79
+ const diff = new PNG({ width, height });
80
+ const diffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
81
+ threshold: 0.1
82
+ });
83
+ mkdirSync(dirname(diffOutputPath), { recursive: true });
84
+ writeFileSync(diffOutputPath, PNG.sync.write(diff));
85
+ const totalPixels = width * height;
86
+ const similarity = Math.round((1 - diffPixels / totalPixels) * 100);
87
+ return { similarity, diffPixels, totalPixels, width, height };
88
+ }
89
+ async function visualCompare(options) {
90
+ const outputDir = options.outputDir ?? "/tmp/canicode-visual-compare";
91
+ const figmaScreenshotPath = resolve(outputDir, "figma.png");
92
+ const codeScreenshotPath = resolve(outputDir, "code.png");
93
+ const diffPath = resolve(outputDir, "diff.png");
94
+ const urlMatch = options.figmaUrl.match(/\/design\/([^/]+)\//);
95
+ const fileKey = urlMatch?.[1];
96
+ if (!fileKey) throw new Error("Invalid Figma URL \u2014 could not extract file key");
97
+ const nodeIdMatch = options.figmaUrl.match(/node-id=([^&\s]+)/);
98
+ const nodeId = nodeIdMatch?.[1]?.replace(/-/g, ":");
99
+ if (!nodeId) throw new Error("Invalid Figma URL \u2014 missing node-id");
100
+ await fetchFigmaScreenshot(fileKey, nodeId, options.figmaToken, figmaScreenshotPath);
101
+ const figmaPng = PNG.sync.read(readFileSync(figmaScreenshotPath));
102
+ const viewport = options.viewport ?? { width: figmaPng.width, height: figmaPng.height };
103
+ await renderCodeScreenshot(options.codePath, codeScreenshotPath, viewport);
104
+ const result = compareScreenshots(figmaScreenshotPath, codeScreenshotPath, diffPath);
105
+ return {
106
+ ...result,
107
+ figmaScreenshotPath,
108
+ codeScreenshotPath,
109
+ diffPath
110
+ };
111
+ }
112
+ var init_visual_compare = __esm({
113
+ "src/core/engine/visual-compare.ts"() {
114
+ }
115
+ });
12
116
  z.object({
13
117
  fileKey: z.string(),
14
118
  nodeId: z.string().optional(),
@@ -217,7 +321,7 @@ var RULE_CONFIGS = {
217
321
  }
218
322
  },
219
323
  // ============================================
220
- // Component (6 rules)
324
+ // Component (7 rules)
221
325
  // ============================================
222
326
  "missing-component": {
223
327
  severity: "risk",
@@ -252,6 +356,11 @@ var RULE_CONFIGS = {
252
356
  score: -1,
253
357
  enabled: true
254
358
  },
359
+ "missing-component-description": {
360
+ severity: "missing-info",
361
+ score: -2,
362
+ enabled: true
363
+ },
255
364
  // ============================================
256
365
  // Naming (5 rules)
257
366
  // ============================================
@@ -834,6 +943,20 @@ function transformNode(node) {
834
943
  }
835
944
  return base;
836
945
  }
946
+ function transformFileNodesResponse(fileKey, response) {
947
+ const entries = Object.values(response.nodes);
948
+ const first = entries[0];
949
+ if (!first) throw new Error("No nodes returned from Figma API");
950
+ return {
951
+ fileKey,
952
+ name: response.name,
953
+ lastModified: response.lastModified,
954
+ version: response.version,
955
+ document: transformNode(first.document),
956
+ components: transformComponents(first.components),
957
+ styles: transformStyles(first.styles)
958
+ };
959
+ }
837
960
  function transformComponents(components) {
838
961
  const result = {};
839
962
  for (const [id, component] of Object.entries(components)) {
@@ -967,6 +1090,13 @@ async function loadFromApi(fileKey, nodeId, token) {
967
1090
  );
968
1091
  }
969
1092
  const client = new FigmaClient({ token: figmaToken });
1093
+ if (nodeId) {
1094
+ const response2 = await client.getFileNodes(fileKey, [nodeId.replace(/-/g, ":")]);
1095
+ return {
1096
+ file: transformFileNodesResponse(fileKey, response2),
1097
+ nodeId
1098
+ };
1099
+ }
970
1100
  const response = await client.getFile(fileKey);
971
1101
  return {
972
1102
  file: transformFigmaResponse(fileKey, response),
@@ -975,7 +1105,7 @@ async function loadFromApi(fileKey, nodeId, token) {
975
1105
  }
976
1106
 
977
1107
  // package.json
978
- var version = "0.8.4";
1108
+ var version = "0.8.5";
979
1109
  var AnalysisNodeTypeSchema = z.enum([
980
1110
  "DOCUMENT",
981
1111
  "CANVAS",
@@ -1788,6 +1918,35 @@ defineRule({
1788
1918
  definition: singleUseComponentDef,
1789
1919
  check: singleUseComponentCheck
1790
1920
  });
1921
+ var seenMissingDescriptionComponentIds = /* @__PURE__ */ new Set();
1922
+ var missingComponentDescriptionDef = {
1923
+ id: "missing-component-description",
1924
+ name: "Missing Component Description",
1925
+ category: "component",
1926
+ why: "Component descriptions in Figma are the primary channel for communicating intent, usage guidelines, and prop expectations to developers. Without them, developers must reverse-engineer purpose from visual appearance alone.",
1927
+ impact: "Increases implementation ambiguity, especially for icon-only components, compound components with multiple variants, and components whose names are variant key strings that give no prose context.",
1928
+ fix: "Open the component in Figma, select it, and add a description in the right-hand panel under the component's properties. Include: what the component is, when to use it, any accessibility or interaction notes, and the owning team or design token set if applicable."
1929
+ };
1930
+ var missingComponentDescriptionCheck = (node, context) => {
1931
+ if (node.type !== "INSTANCE") return null;
1932
+ const componentId = node.componentId;
1933
+ if (!componentId) return null;
1934
+ const componentMeta = context.file.components[componentId];
1935
+ if (!componentMeta) return null;
1936
+ if (componentMeta.description.trim() !== "") return null;
1937
+ if (seenMissingDescriptionComponentIds.has(componentId)) return null;
1938
+ seenMissingDescriptionComponentIds.add(componentId);
1939
+ return {
1940
+ ruleId: missingComponentDescriptionDef.id,
1941
+ nodeId: node.id,
1942
+ nodePath: context.path.join(" > "),
1943
+ message: `Component "${componentMeta.name}" has no description. Descriptions help developers understand purpose and usage.`
1944
+ };
1945
+ };
1946
+ defineRule({
1947
+ definition: missingComponentDescriptionDef,
1948
+ check: missingComponentDescriptionCheck
1949
+ });
1791
1950
 
1792
1951
  // src/core/rules/naming/index.ts
1793
1952
  var DEFAULT_NAME_PATTERNS = [
@@ -3018,13 +3177,19 @@ function hasTextDescendant(node) {
3018
3177
  }
3019
3178
  var MIN_WIDTH = 200;
3020
3179
  var MIN_HEIGHT = 200;
3180
+ var FILTER_THRESHOLD = 500;
3021
3181
  var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
3022
3182
  "FRAME",
3023
3183
  "COMPONENT",
3024
3184
  "INSTANCE"
3025
3185
  ]);
3026
3186
  function filterConversionCandidates(summaries, documentRoot) {
3027
- return summaries.filter((summary) => {
3187
+ const visibleSummaries = summaries.filter((summary) => {
3188
+ const node = findNode(documentRoot, summary.nodeId);
3189
+ return node ? node.visible !== false : false;
3190
+ });
3191
+ if (visibleSummaries.length <= FILTER_THRESHOLD) return visibleSummaries;
3192
+ return visibleSummaries.filter((summary) => {
3028
3193
  const node = findNode(documentRoot, summary.nodeId);
3029
3194
  if (!node) return false;
3030
3195
  if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
@@ -4695,6 +4860,50 @@ cli.command(
4695
4860
  process.exit(1);
4696
4861
  }
4697
4862
  });
4863
+ cli.command(
4864
+ "visual-compare <codePath>",
4865
+ "Compare rendered code against Figma screenshot (pixel-level similarity)"
4866
+ ).option("--figma-url <url>", "Figma URL with node-id (required)").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option("--output <dir>", "Output directory for screenshots and diff (default: /tmp/canicode-visual-compare)").option("--width <px>", "Viewport width (default: 1440)").option("--height <px>", "Viewport height (default: 900)").example(" canicode visual-compare ./generated/index.html --figma-url 'https://www.figma.com/design/ABC/File?node-id=1-234'").action(async (codePath, options) => {
4867
+ try {
4868
+ if (!options.figmaUrl) {
4869
+ console.error("Error: --figma-url is required");
4870
+ process.exit(1);
4871
+ }
4872
+ const token = options.token ?? getFigmaToken();
4873
+ if (!token) {
4874
+ console.error("Error: Figma token required. Use --token or set FIGMA_TOKEN env var.");
4875
+ process.exit(1);
4876
+ }
4877
+ const { visualCompare: visualCompare2 } = await Promise.resolve().then(() => (init_visual_compare(), visual_compare_exports));
4878
+ console.log("Comparing...");
4879
+ const result = await visualCompare2({
4880
+ figmaUrl: options.figmaUrl,
4881
+ figmaToken: token,
4882
+ codePath: resolve(codePath),
4883
+ outputDir: options.output,
4884
+ viewport: {
4885
+ width: options.width ?? 1440,
4886
+ height: options.height ?? 900
4887
+ }
4888
+ });
4889
+ console.log(JSON.stringify({
4890
+ similarity: result.similarity,
4891
+ diffPixels: result.diffPixels,
4892
+ totalPixels: result.totalPixels,
4893
+ width: result.width,
4894
+ height: result.height,
4895
+ figmaScreenshot: result.figmaScreenshotPath,
4896
+ codeScreenshot: result.codeScreenshotPath,
4897
+ diff: result.diffPath
4898
+ }, null, 2));
4899
+ } catch (error) {
4900
+ console.error(
4901
+ "\nError:",
4902
+ error instanceof Error ? error.message : String(error)
4903
+ );
4904
+ process.exit(1);
4905
+ }
4906
+ });
4698
4907
  cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <token>", "Save Figma API token to ~/.canicode/").option("--mcp", "Show Figma MCP setup instructions").action((options) => {
4699
4908
  try {
4700
4909
  if (options.token) {